Feature lifecycle: harness features listing, receipts, self-allowing add, and remove with purge #144
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#144
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Give feature bundles a full, machine-readable lifecycle: a
harness featureslisting, aharness addthat leaves the feature usable and records what it did, and a newharness removethat undoes it. deckle-hosted will drive all three (at site creation and at any later point) and render its Features UI from the listing, so it stops hardcoding anything aboutnews.Current behavior
scaffold/features/<name>with no metadata.addcmd.Features()lists their names; the only way to see them from the CLI is the usage error ofharness add(stderr, exit 2).harness add <feature>copies the bundle into the site (all-or-nothing, never overwriting) and runs Go config steps keyed by feature name (featureSteps; for news:buildFuture, the categories taxonomy edits inhugo.toml).children: [page, section], so afterharness add newsthe editor has nowhere to offer a news section until someone hand-edits e.g.editor/pagetypes/home.yaml(featureNextStepsprints that as a manual step).harness add newsfails with aConflictError; nothing can tell whether a site has a feature installed.Desired behavior
Bundle manifest
Each bundle carries one manifest file at its root (e.g.
scaffold/features/news/feature.yaml) with:label,description(plain English, shown next to a checkbox), an integerversion, and the page-type allowance it wants (for news: allow page typenewsas a child of page typehome). The manifest is never copied into a site and is validated by a test for every embedded bundle.harness features [--site <dir>] [--json]--site: the catalogue — every embedded feature's name, label, description, version.--site: the same, plus per feature whether it is installed on that site and at which version (from the receipt, see below).--jsonprints one JSON object on stdout.harness add <feature> [--site <dir>] [--json]On top of today's vendoring and config steps:
childrenlist in the site's sidecar. The edit preserves the file's comments and formatting and is idempotent. If the target sidecar is missing, unparsable, or has no explicitchildrenlist to extend, add still succeeds, reports the allowance as skipped, and prints the manual step. When the allowance was applied, the "allow the section somewhere" lines disappear from the printed next steps.editor/features/<name>.yamlin the site, recording: the feature name and bundle version, every vendored path with a content hash of the bytes written, which config edits actually changed something (an edit that found its result already in place is not recorded), and which allowance was applied.harness remove <feature> [--site <dir>] [--purge] [--json]Driven by the receipt.
--purge: nothing changes at all — not one byte of the tree. The command reports what a purge would delete and exits 1 (statusblocked). "In use" means any of:newssections andnews-itempages), including everything beneath them;news-list), at any nesting depth;--purge: removetaxonomy/termkinds and declaredcategories; if the owner has since added atagstaxonomy, remove takes out only thecategoriesentry and leaves taxonomies enabled. A config edit that is not in the receipt (it was already in place before the add) is never reverted;childrenlist of every page-type sidecar in the site (not only the one add edited), comment-preserving;assets/media/) are never deleted.JSON contract (the surface deckle-hosted consumes)
With
--json, stdout carries exactly one JSON object and nothing else. Every object has a top-level format version so fields can be added without breaking consumers. Exit codes keep the existing convention:0the requested change happened or was already in place,1it did not (the JSON says why),2usage error (stderr, no JSON).Illustrative shapes — field names are the implementer's call, the information content is the contract:
blockedcarries the usage preview;removedcarries what was actually purged and which config edits were kept and why.installed/installedVersionappear only when--sitewas given.Decisions
harness addedits the site's page-type sidecar — reverses the old "never touches the site's OWN page-type sidecars" rule. "Vendored but nowhere to create it" is a half-finished state nobody wants; hosted was about to automate the same edit from outside. On by default for CLI users too.home.yamlmust not make an otherwise valid add fail; the manual step is printed instead.buildFuture = truepredates the add.featuresis its own command, not a flag onadd— it needs no feature argument and has a per-site mode.Key interfaces
internal/addcmd(vendoring,Features(),featureSteps,featureNextSteps, thehugo.tomltransforms),cmd/harness/main.go(subcommand wiring, usage text, exit-code convention),scaffold/features/news,scaffold/embed.go.buildPlanmust not vendor the manifest;harness initkeeps skippingfeatures/.internal/pagepath, used byinternal/pageops), not a raw front-matter match.internal/content,internal/elemops) so canonicalization rules hold; inbound-link discovery exists ininternal/linkrefs(References).editor/pagetypes/*.yaml,editor/elements/*.yaml).scaffold/site/AGENTS.mdandREADME.mddocumentharness addtoday; both must describe the three commands, the receipt, the allowance and the purge semantics (project rule: the blueprint AGENTS.md must track the engine in the same change).Acceptance criteria
harness featuresprints name, label, description and version for every bundle;--jsonprints one versioned JSON object;--siteadds installed state and installed version per feature.harness addwith no feature still exits 2 and still names the available features.harness init+harness add news,editor/pagetypes/home.yamllistsnewsinchildren, its comments are intact, and a news section can be created at the content root without any manual edit.home.yamlthat is missing, or has no editablechildrenlist, yields a successful add that reports the allowance as skipped and prints the manual step.editor/features/news.yamllisting every vendored path with its hash, the bundle version, the config edits that changed something, and the applied allowance. On a site that already hadbuildFuture = true, the receipt does not record that edit.harness add newsexits 0, reports already installed, and leaves the tree byte-identical.harness remove newson a site without the receipt exits 0 and changes nothing.harness remove newsright afterharness init+harness add news(nothing in use) restores the tree byte-for-byte to its pre-add state, includinghugo.tomlandhome.yaml.news-listelement on another page, or a customised vendored file present,harness remove newsexits 1 with status blocked, lists each of them, and leaves the tree byte-identical.harness remove news --purgedeletes the news pages and their subtrees, stripsnews-listelements (including nested ones) while leaving the rest of those pages intact, deletes customised vendored files, and the resulting site builds with Hugo.tagstaxonomy) survives remove and is reported as kept; a setting that predates the add survives remove.children, including sidecars the owner edited by hand.assets/media/is deleted.--jsonoutput is a single JSON object on stdout for every status of add and remove, with the documented exit codes.scaffold/site/AGENTS.mdandREADME.mddescribe the new commands, flags, receipt, allowance and purge behaviour; the drift test still passes.How to verify
make test(builds the web bundle, thengo test ./...).harness init,harness features --json,harness add news --json,harness features --site . --json, create a news section with an item and place a "Latest news" element on the home page,harness remove news --json(blocked, tree unchanged),harness remove news --purge --json, then a Hugo build of the result.harness init, add then remove with nothing in use, diff against the snapshot — empty.Assumptions to check
childrenkey means in a page-type sidecar (everything allowed vs nothing) — it decides when the allowance is "nothing to do" versus "skipped".childrenentry naming a type with no sidecar — it decides how strictly remove must clean every list.harness serveor the sidecar loaders treats a neweditor/features/directory as sidecars or trips over it.hugo.tomltransforms can each report precisely whether they changed something, and that each has a well-defined inverse.children: [page, section]) is achievable with the YAML library already in use.Out of scope
Blocked by
None - can start immediately