feat(harness): feature lifecycle — features listing, receipts, self-allowing add, remove with purge #146

Merged
dominik.polakovics merged 1 commit from afk/144 into main 2026-10-04 19:48:05 +02:00

Closes #144

What

Feature bundles get a full, machine-readable lifecycle so deckle-hosted can drive them and render its Features UI from the listing.

  • Bundle manifest scaffold/features/<name>/feature.yaml: label, description, integer bundle version, allow: [{pageType, childOf}]. Validated by a test for every embedded bundle; never vendored into a site.
  • harness features [--site <dir>] [--json]: the catalogue; with --site, installed state and version per feature, read from the receipt.
  • harness add <feature> [--site] [--json] now also
    • applies the manifest allowance: adds the feature's page type to the target sidecar's children list (home.yaml → children: [page, section, news]), comment-preserving, idempotent, soft on a missing/unparsable/unmodellable sidecar (reported as skipped, manual step printed);
    • writes a receipt editor/features/<name>.yaml: every vendored path with its SHA-256, the hugo.toml edits that actually changed something (verbatim lines, so they can be undone exactly), and the applied allowances;
    • treats an existing receipt as success (already-installed, exit 0, nothing changes); refuses collisions and an unmodellable hugo.toml before writing a byte.
  • harness remove <feature> [--site] [--purge] [--json], driven by the receipt:
    • not installed → exit 0, nothing changes;
    • in use without --purge → blocked, exit 1, not one byte changes; the preview lists feature pages (effective type, with their subtrees), feature elements on other pages at any depth, customised vendored files, pages that mention the feature but cannot be parsed, and pages linking to purged pages;
    • otherwise deletes the pages (whole bundles), strips the elements through the content model (canonical write-back), deletes every vendored path and prunes emptied directories, reverts each hugo.toml edit only if still exactly as add left it (else kept and reported — an owner-added tags taxonomy keeps the kinds enabled), removes the types from every page-type sidecar's children, deletes the receipt;
    • never touches git or assets/media/; identical on clean, dirty and non-git trees.
  • init → add news → remove news restores the tree byte for byte, directories included.
  • --json emits exactly one object with formatVersion: 1 on stdout; exit codes keep the 0/1/2 convention. Statuses: add added | already-installed | conflict | error; remove removed | not-installed | blocked | error.
  • hugo.toml config steps are now apply/revert pairs of pure text functions; CRLF files and Hugo's case-insensitive keys are handled; inline/dotted outputs is refused rather than producing invalid TOML.
  • README and scaffold/site/AGENTS.md describe the three commands, the receipt, the allowance and the purge semantics (drift test passes).

Layout

  • internal/addcmd: manifest/catalogue, receipt, reversible hugo.toml edits (ApplyConfig/RevertConfig), comment-preserving children edits, the add flow.
  • internal/removecmd: content usage scan and purge (Scan/Purge) and the remove orchestration (Run).
  • cmd/harness/features.go: the three commands, text and JSON rendering.

Verification

  • go vet ./..., go test ./... (26 packages, with Hugo 0.163.3 on PATH so the Hugo-build tests run), web: npm test (1556 tests) — all green.
  • End to end with the built binary in a scratch dir: init, features --json, add news --json, features --site --json, news section + item + news-list on home, remove news --json (blocked, tree hash unchanged), remove news --purge --json, Hugo build of the result — OK. Round trip (init → add → remove) diffed against the post-init snapshot: identical.
  • An independent review pass of the internal packages found edge defects (unparsable page with a feature element, stray index.md purge, CRLF hugo.toml, inline/dotted tables, symlinked ancestors, unknown receipt edit IDs); all are fixed and covered by tests.

Not in scope (per the issue)

Bundles outside the binary, updating an installed feature, adopting pre-receipt installs, git operations, restoring purged content, the hosted side.

🤖 Generated with Claude Code

Closes #144 ## What Feature bundles get a full, machine-readable lifecycle so deckle-hosted can drive them and render its Features UI from the listing. - **Bundle manifest** `scaffold/features/<name>/feature.yaml`: `label`, `description`, integer bundle `version`, `allow: [{pageType, childOf}]`. Validated by a test for every embedded bundle; never vendored into a site. - **`harness features [--site <dir>] [--json]`**: the catalogue; with `--site`, installed state and version per feature, read from the receipt. - **`harness add <feature> [--site] [--json]`** now also - applies the manifest allowance: adds the feature's page type to the target sidecar's `children` list (`home.yaml` → `children: [page, section, news]`), comment-preserving, idempotent, soft on a missing/unparsable/unmodellable sidecar (reported as skipped, manual step printed); - writes a receipt `editor/features/<name>.yaml`: every vendored path with its SHA-256, the `hugo.toml` edits that actually changed something (verbatim lines, so they can be undone exactly), and the applied allowances; - treats an existing receipt as success (`already-installed`, exit 0, nothing changes); refuses collisions and an unmodellable `hugo.toml` before writing a byte. - **`harness remove <feature> [--site] [--purge] [--json]`**, driven by the receipt: - not installed → exit 0, nothing changes; - in use without `--purge` → `blocked`, exit 1, not one byte changes; the preview lists feature pages (effective type, with their subtrees), feature elements on other pages at any depth, customised vendored files, pages that mention the feature but cannot be parsed, and pages linking to purged pages; - otherwise deletes the pages (whole bundles), strips the elements through the content model (canonical write-back), deletes every vendored path and prunes emptied directories, reverts each `hugo.toml` edit only if still exactly as add left it (else kept and reported — an owner-added `tags` taxonomy keeps the kinds enabled), removes the types from every page-type sidecar's `children`, deletes the receipt; - never touches git or `assets/media/`; identical on clean, dirty and non-git trees. - `init → add news → remove news` restores the tree byte for byte, directories included. - `--json` emits exactly one object with `formatVersion: 1` on stdout; exit codes keep the 0/1/2 convention. Statuses: add `added | already-installed | conflict | error`; remove `removed | not-installed | blocked | error`. - `hugo.toml` config steps are now apply/revert pairs of pure text functions; CRLF files and Hugo's case-insensitive keys are handled; inline/dotted `outputs` is refused rather than producing invalid TOML. - README and `scaffold/site/AGENTS.md` describe the three commands, the receipt, the allowance and the purge semantics (drift test passes). ## Layout - `internal/addcmd`: manifest/catalogue, receipt, reversible `hugo.toml` edits (`ApplyConfig`/`RevertConfig`), comment-preserving `children` edits, the add flow. - `internal/removecmd`: content usage scan and purge (`Scan`/`Purge`) and the remove orchestration (`Run`). - `cmd/harness/features.go`: the three commands, text and JSON rendering. ## Verification - `go vet ./...`, `go test ./...` (26 packages, with Hugo 0.163.3 on PATH so the Hugo-build tests run), `web: npm test` (1556 tests) — all green. - End to end with the built binary in a scratch dir: `init`, `features --json`, `add news --json`, `features --site --json`, news section + item + `news-list` on home, `remove news --json` (blocked, tree hash unchanged), `remove news --purge --json`, Hugo build of the result — OK. Round trip (init → add → remove) diffed against the post-init snapshot: identical. - An independent review pass of the internal packages found edge defects (unparsable page with a feature element, stray `index.md` purge, CRLF `hugo.toml`, inline/dotted tables, symlinked ancestors, unknown receipt edit IDs); all are fixed and covered by tests. ## Not in scope (per the issue) Bundles outside the binary, updating an installed feature, adopting pre-receipt installs, git operations, restoring purged content, the hosted side. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(harness): feature lifecycle — features listing, receipts, self-allowing add, remove with purge
All checks were successful
ci / build-and-test (pull_request) Successful in 7m35s
0b5423ada0
Give feature bundles a machine-readable lifecycle so deckle-hosted can
drive them and stop hardcoding anything about news.

- Bundle manifest `feature.yaml` (label, description, bundle version,
  page-type allowances); validated for every embedded bundle, never
  vendored.
- `harness features [--site] [--json]`: the catalogue, plus installed
  state and version per feature when a site is given.
- `harness add`: applies the manifest allowance (adds the feature's page
  type to the target sidecar's `children`, comment-preserving, soft on
  failure), writes a receipt at editor/features/<name>.yaml (vendored
  paths with hashes, the hugo.toml edits that changed something, applied
  allowances), treats an existing receipt as success, refuses collisions
  and unmodellable hugo.toml before writing a byte.
- `harness remove [--purge]`: receipt-driven; blocked (exit 1, no bytes
  changed) while pages, elements, customised or unparsable pages use the
  feature; otherwise deletes the pages, strips the elements through the
  content model, deletes vendored files, reverts hugo.toml edits that
  still read as add left them (kept and reported otherwise), removes the
  types from every sidecar's `children`, deletes the receipt. Never
  touches git or assets/media. init → add → remove is byte-identical.
- hugo.toml config steps are now apply/revert pairs of pure text
  functions recording verbatim lines, with CRLF and case-insensitive
  key handling.
- `--json` on all three commands: one versioned object on stdout.
- README and scaffold/site/AGENTS.md document the commands, receipt,
  allowance and purge semantics.

Closes #144

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Verdict: PASS (with non-blocking CONCERNS)

Signal relied on: Forgejo Actions ci / build-and-test run 132 is green (web build + npm test, go vet, go test ./... with Hugo 0.163.3, go build). Not re-run. The branch sits directly on current main (no conflict).

Checked: Conventional Commits title; body carries Closes #144; diff scope matches issue #144 (manifest, harness features, receipt-writing add with allowance, receipt-driven remove --purge, JSON contract); scaffold/site/AGENTS.md and README updated per CLAUDE.md; no drive-by changes in cmd/harness/main.go. Two independent review passes over internal/addcmd, internal/removecmd and cmd/harness confirmed: the blocked/preview path is read-only (tree-hash tests), path-traversal and assets/media/.git guards are present and tested, element stripping works at arbitrary depth, round trip init→add→remove is byte-exact at both package and CLI level.

CONCERNS (edge cases, worth a follow-up issue):

  1. internal/addcmd/children.go sweepChildTypes: checks whether each sidecar file is a symlink but not whether editor/pagetypes itself is a symlinked directory, so remove --purge writes the children edit through the link (outside the site) while the same run refuses to delete vendored files behind it. Only reachable if the owner symlinks the directory after the add.
  2. internal/addcmd/receipt.go / config.go: a recorded hugo.toml owner line containing a TAB (e.g. disableKinds\t= [...]) is emitted by the YAML encoder as a plain scalar and loses the tab on read-back, so the "verbatim" revert re-inserts a slightly altered line while reporting reverted. Still valid TOML; fix is to quote recorded lines or assert Marshal→Unmarshal fidelity in WriteReceipt.
  3. No test covers removecmd.Run's mid-write failure path (partial-report JSON with status: error and the "receipt still in place; re-run" hint).
[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Verdict: PASS (with non-blocking CONCERNS)** Signal relied on: Forgejo Actions `ci / build-and-test` run 132 is green (web build + `npm test`, `go vet`, `go test ./...` with Hugo 0.163.3, `go build`). Not re-run. The branch sits directly on current `main` (no conflict). Checked: Conventional Commits title; body carries `Closes #144`; diff scope matches issue #144 (manifest, `harness features`, receipt-writing `add` with allowance, receipt-driven `remove --purge`, JSON contract); `scaffold/site/AGENTS.md` and README updated per CLAUDE.md; no drive-by changes in `cmd/harness/main.go`. Two independent review passes over `internal/addcmd`, `internal/removecmd` and `cmd/harness` confirmed: the blocked/preview path is read-only (tree-hash tests), path-traversal and `assets/media`/`.git` guards are present and tested, element stripping works at arbitrary depth, round trip init→add→remove is byte-exact at both package and CLI level. CONCERNS (edge cases, worth a follow-up issue): 1. `internal/addcmd/children.go` `sweepChildTypes`: checks whether each sidecar *file* is a symlink but not whether `editor/pagetypes` itself is a symlinked directory, so `remove --purge` writes the children edit through the link (outside the site) while the same run refuses to delete vendored files behind it. Only reachable if the owner symlinks the directory after the add. 2. `internal/addcmd/receipt.go` / `config.go`: a recorded `hugo.toml` owner line containing a TAB (e.g. `disableKinds\t= [...]`) is emitted by the YAML encoder as a plain scalar and loses the tab on read-back, so the "verbatim" revert re-inserts a slightly altered line while reporting `reverted`. Still valid TOML; fix is to quote recorded lines or assert Marshal→Unmarshal fidelity in `WriteReceipt`. 3. No test covers `removecmd.Run`'s mid-write failure path (partial-report JSON with `status: error` and the "receipt still in place; re-run" hint).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!146
No description provided.