Standard feature bundles: installed by harness init, marked in the features JSON, not removable #157

Closed
opened 2026-10-06 23:42:12 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

Let a feature bundle be marked standard in its manifest. harness init installs every standard bundle. harness features marks them with a standard field in --json, so a host can leave them out of its feature checkboxes. harness remove refuses to remove one.

categories becomes the first standard bundle, so every new site has categories. Since nothing can remove categories any more, the remove code that only the removal of categories used is deleted.

Current behavior

  • A bundle's manifest, feature.yaml, has label, description, version and allow. It is decoded strictly, so an unknown key is an error (LoadManifest and parseManifest in internal/addcmd).
  • harness init copies the starter site and skips features/ (buildPlan in internal/initcmd). A new site has no bundle until someone runs harness add.
  • harness add <feature> vendors the bundle, applies its hugo.toml edits and page-type allowances, and writes the receipt editor/features/<feature>.yaml. On an installed feature it fills gaps, or reports already-installed.
  • harness features [--site <dir>] [--json] lists name, label, description and version per bundle, sorted by name. With --site it adds installed and installedVersion.
  • harness remove <feature> is driven by the receipt and removes any installed feature. Three parts of it exist only for the categories bundle:
    • Shipped pages. A content page the bundle vendored and nobody changed (the /categories/ page, content/categories/_index.md) is not "in use", so a remove right after the add needs no --purge. See shippedPages and the "Shipped pages" rule in internal/removecmd.
    • Category rewrites. A purge that deletes categories removes their slugs from the remaining pages, and the preview and result list those pages. This covers categoryChanges, the CategoriesRemovedFrom fields of ContentUsage and PurgeReport, usage.categoriesRemovedFrom in --json, and the "deleted categories removed from" lines of the text output.
    • Config reverts. The revert halves of the categories bundle's hugo.toml steps disableKinds, taxonomies and outputs (internal/addcmd/taxonomy.go), with revertContext and taxonomiesAfterRevert in internal/addcmd/config.go.
  • The next steps harness add news prints tell the user to run harness add categories.
  • deckle-hosted runs harness features --json once at startup and shows one checkbox per feature (#145). It ignores fields of formatVersion 1 that it doesn't know.

Desired behavior

Standard bundles

  • The manifest accepts standard: true. Without it a bundle is optional, as today. The categories manifest sets it, and news stays optional.
  • harness init installs every standard bundle after it writes the starter site, in name order, exactly as harness add does: files, hugo.toml edits, page-type allowances and the receipt. Its output names each feature it installed.
  • If installing a standard bundle fails, harness init exits 1 with an error that names the feature. Files already written stay, and harness add <feature> completes the install.
  • harness features marks standard features. In --json, every entry has a standard boolean, with and without --site. The text form marks them too, for example [standard]. The field is additive, so formatVersion stays 1.
  • Only harness add installs a standard feature on an existing site. A site without one lists it with "standard": true, "installed": false.

A standard feature can't be removed

  • harness remove <feature> on an installed standard feature changes nothing. It exits 1 with the existing status error and a message saying the feature is standard and cannot be removed. --purge makes no difference.
  • On a site without the feature's receipt, it still reports not-installed and exits 0.
  • Whether a feature is standard comes from the running harness's bundle manifests. A feature this harness doesn't ship is never standard, and removing it works as today.

Deleting the code only the removal of categories used

Delete, with their tests, the three parts of remove listed under Current behavior: shipped pages, category rewrites and the three config reverts. Keep these:

  • usage.categoriesRemovedFrom in harness remove --json, always [], so the JSON keeps its shape.
  • The apply halves of the three config steps, their step IDs and the receipt format, because harness add categories still installs the bundle on a site that lacks it.
  • RevertConfig's rule that a recorded edit it can't act on is kept rather than being an error. An edit whose step has no revert counts as one.
  • The editor's own handling of categories (pageops.Delete). Deleting a category in the Categories module still removes its slug from every page and element.

Hints and docs

  • harness add news no longer tells the user to run harness add categories.
  • Update the README and scaffold/site/AGENTS.md, as the CLAUDE.md rule requires:
    • Describe standard features: the manifest key, what init installs, the standard JSON field, and the refusal to remove one.
    • Describe categories as installed on every new site, and stop describing how to remove them.
    • Passages to update include these README sections: "Scaffolding", "Feature bundles" ("harness init skips them"), "The manifest", "Removing: harness remove", "Machine-readable output", "Removing." under "The categories bundle", and "Not covered yet". In AGENTS.md: the opening ("Optional feature bundles"), "Layout" and "Feature bundles".

Decisions

  • A standard feature is still a bundle, with a receipt, harness add and gap filling, so a site created before a feature became standard can get it with harness add. (Maintainer's choice.)
  • The JSON marks standard features so deckle-hosted can hide them. (Maintainer's requirement.) The field is additive and formatVersion stays 1, because deckle-hosted ignores version-1 fields it doesn't know.
  • A standard feature cannot be removed, like a protected TYPO3 system extension. (Maintainer's choice; rejected: removable like any other feature.) The refusal uses the existing error status because deckle-hosted's status switch is exhaustive: it shows the harness's message for error, and it would fail on a new status.
  • The code only the removal of categories used is deleted, not kept unused. (Maintainer's choice; rejected: keeping it so that making categories optional again would need no new code.) If categories ever become removable again, their removal is rebuilt.
  • Existing sites get a standard feature only through harness add. (Maintainer's choice.) harness serve must not install one: the editor's publish carries only content/ and assets/media/, so files it wrote would never reach main. No upgrade command is needed while no deckle sites exist (as of 2026-10-06).
  • A failed standard install fails init without rolling back. (Maintainer's choice.) On an empty target directory only I/O errors remain, and a test proves that every standard bundle installs on the starter site.
  • deckle-hosted is not changed here. Until it hides standard features, it breaks in the ways listed under Out of scope. (The maintainer accepted this because no sites exist yet.)
  • One issue, one pull request.

Key interfaces

  • addcmd.Manifest and parseManifest (internal/addcmd/manifest.go). Catalogue() and addcmd.Info carry the flag to the CLI.
  • featureEntryOutput and printFeatures (cmd/harness/features.go).
  • harness init: initSite (cmd/harness/main.go), initcmd.Run, and addcmd.Run as the install path.
  • harness remove: removeCmd (cmd/harness/features.go) and internal/removecmd (Run, scan, shippedPages, categoryChanges, Purge, ContentUsage, PurgeReport).
  • RevertConfig, configOp, featureConfig, revertContext and taxonomiesAfterRevert (internal/addcmd/config.go), and the revert functions in internal/addcmd/taxonomy.go.
  • featureHints (internal/addcmd/add.go).
  • Tests that remove categories today, as a starting list:
    • TestRunCategories* (internal/removecmd/run_test.go);
    • TestPurgeStripsBeforeDeletingCategories (purge_test.go);
    • the CategoriesRemovedFrom expectations in usage_test.go;
    • TestRemoveCmdCategoriesRemovedFrom and TestCategoriesCmdRoundTrip (cmd/harness/features_test.go);
    • the cases in internal/addcmd/config_test.go and taxonomy_test.go that revert the categories steps. The tests of the apply halves stay.

Acceptance criteria

  • The categories manifest has standard: true. A manifest with an unknown key is still a load error.
  • harness init in an empty directory writes the starter site with the categories bundle installed, including its hugo.toml edits and editor/features/categories.yaml. Its output names categories.
  • A site from harness init builds with Hugo and renders /categories/.
  • A test fails when a starter-site file lies at a path that a standard bundle ships.
  • harness add categories right after harness init exits 0 with already-installed and changes nothing.
  • harness features --json gives every entry a standard boolean, with and without --site: true for categories and false for news, with formatVersion 1. The text form marks categories as standard.
  • harness remove categories on a site from harness init, with and without --purge, exits 1, reports status error in --json with a message naming it a standard feature, and leaves the tree byte-identical.
  • harness remove categories on a site without its receipt reports not-installed and exits 0.
  • harness add news and harness remove news behave as before, and usage.categoriesRemovedFrom is present and [] in every harness remove --json object.
  • harness add news no longer mentions harness add categories.
  • The shipped-pages rule, the category rewrites in remove and the three config reverts are gone, with their tests. RevertConfig reports a recorded edit with no revert as kept.
  • Deleting a category in the Categories module still removes its slug from every page and element.
  • The README and scaffold/site/AGENTS.md document standard features and the refusal to remove one, describe categories as installed by harness init, and no longer describe removing categories. The AGENTS.md drift test passes.

How to verify

  • make test. It builds web/dist first, which go test ./... needs. The theme's Hugo build tests run only with hugo on PATH (CI installs 0.163.3).
  • cd web && npm test.
  • In a scratch directory:
    1. harness init.
    2. harness features --site . --json shows categories as standard and installed.
    3. harness add categories --json reports already-installed.
    4. harness remove categories --json and harness remove categories --purge --json both report status error, and the tree is unchanged.
    5. harness add news --json, then harness remove news --json, behave as before.
    6. hugo builds the site.

Assumptions to check

  • internal/addcmd's tests are in package addcmd and import internal/initcmd. If initcmd imports addcmd, those tests stop compiling because of an import cycle. Check this before deciding where init installs standard bundles.
  • Several test helpers build their site with initcmd.Run and then add or remove categories, expecting a starter site without categories. Which of them change depends on where the install lives.
  • Nothing except removing categories reaches the shipped-pages rule: news ships no content page. Confirm before deleting it.

Out of scope

  • deckle-hosted. Until it reads standard, the maintainer accepts the following:
    • Its new-site dialog still offers Categories, and ticking it fails site creation, because its scaffold treats already-installed as an error (addFeature in gitserve/scaffold.go).
    • Its site Features page still offers Remove for Categories, which shows the harness's refusal.
    • TestRealHarnessLifecycle (harness/real_test.go) and TestScaffoldRealBinaries (gitserve/scaffold_test.go) fail, because they take the first feature listed, which is now categories. Both run only with DECKLE_HOSTED_TEST_HARNESS set. Its other real-binary tests were not checked.
  • Installing standard features on existing sites automatically.
  • An init option to skip standard features.
  • Making news standard.
  • Changing the categories bundle's files or version. The manifest is never copied into a site, so marking the bundle standard changes no site file.

Blocked by

None - can start immediately

> *This was generated by AI during triage.* ## Summary Let a feature bundle be marked **standard** in its manifest. `harness init` installs every standard bundle. `harness features` marks them with a `standard` field in `--json`, so a host can leave them out of its feature checkboxes. `harness remove` refuses to remove one. `categories` becomes the first standard bundle, so every new site has categories. Since nothing can remove categories any more, the remove code that only the removal of categories used is deleted. ## Current behavior - A bundle's manifest, `feature.yaml`, has `label`, `description`, `version` and `allow`. It is decoded strictly, so an unknown key is an error (`LoadManifest` and `parseManifest` in `internal/addcmd`). - `harness init` copies the starter site and skips `features/` (`buildPlan` in `internal/initcmd`). A new site has no bundle until someone runs `harness add`. - `harness add <feature>` vendors the bundle, applies its `hugo.toml` edits and page-type allowances, and writes the receipt `editor/features/<feature>.yaml`. On an installed feature it fills gaps, or reports `already-installed`. - `harness features [--site <dir>] [--json]` lists `name`, `label`, `description` and `version` per bundle, sorted by name. With `--site` it adds `installed` and `installedVersion`. - `harness remove <feature>` is driven by the receipt and removes any installed feature. Three parts of it exist only for the categories bundle: - **Shipped pages.** A content page the bundle vendored and nobody changed (the `/categories/` page, `content/categories/_index.md`) is not "in use", so a remove right after the add needs no `--purge`. See `shippedPages` and the "Shipped pages" rule in `internal/removecmd`. - **Category rewrites.** A purge that deletes categories removes their slugs from the remaining pages, and the preview and result list those pages. This covers `categoryChanges`, the `CategoriesRemovedFrom` fields of `ContentUsage` and `PurgeReport`, `usage.categoriesRemovedFrom` in `--json`, and the "deleted categories removed from" lines of the text output. - **Config reverts.** The revert halves of the categories bundle's `hugo.toml` steps `disableKinds`, `taxonomies` and `outputs` (`internal/addcmd/taxonomy.go`), with `revertContext` and `taxonomiesAfterRevert` in `internal/addcmd/config.go`. - The next steps `harness add news` prints tell the user to run `harness add categories`. - deckle-hosted runs `harness features --json` once at startup and shows one checkbox per feature (#145). It ignores fields of `formatVersion` 1 that it doesn't know. ## Desired behavior ### Standard bundles - The manifest accepts `standard: true`. Without it a bundle is optional, as today. The categories manifest sets it, and news stays optional. - `harness init` installs every standard bundle after it writes the starter site, in name order, exactly as `harness add` does: files, `hugo.toml` edits, page-type allowances and the receipt. Its output names each feature it installed. - If installing a standard bundle fails, `harness init` exits 1 with an error that names the feature. Files already written stay, and `harness add <feature>` completes the install. - `harness features` marks standard features. In `--json`, every entry has a `standard` boolean, with and without `--site`. The text form marks them too, for example `[standard]`. The field is additive, so `formatVersion` stays 1. - Only `harness add` installs a standard feature on an existing site. A site without one lists it with `"standard": true, "installed": false`. ### A standard feature can't be removed - `harness remove <feature>` on an installed standard feature changes nothing. It exits 1 with the existing status `error` and a message saying the feature is standard and cannot be removed. `--purge` makes no difference. - On a site without the feature's receipt, it still reports `not-installed` and exits 0. - Whether a feature is standard comes from the running harness's bundle manifests. A feature this harness doesn't ship is never standard, and removing it works as today. ### Deleting the code only the removal of categories used Delete, with their tests, the three parts of remove listed under Current behavior: shipped pages, category rewrites and the three config reverts. Keep these: - `usage.categoriesRemovedFrom` in `harness remove --json`, always `[]`, so the JSON keeps its shape. - The apply halves of the three config steps, their step IDs and the receipt format, because `harness add categories` still installs the bundle on a site that lacks it. - `RevertConfig`'s rule that a recorded edit it can't act on is kept rather than being an error. An edit whose step has no revert counts as one. - The editor's own handling of categories (`pageops.Delete`). Deleting a category in the Categories module still removes its slug from every page and element. ### Hints and docs - `harness add news` no longer tells the user to run `harness add categories`. - Update the README and `scaffold/site/AGENTS.md`, as the CLAUDE.md rule requires: - Describe standard features: the manifest key, what `init` installs, the `standard` JSON field, and the refusal to remove one. - Describe categories as installed on every new site, and stop describing how to remove them. - Passages to update include these README sections: "Scaffolding", "Feature bundles" ("`harness init` skips them"), "The manifest", "Removing: `harness remove`", "Machine-readable output", "Removing." under "The categories bundle", and "Not covered yet". In AGENTS.md: the opening ("Optional feature bundles"), "Layout" and "Feature bundles". ## Decisions - **A standard feature is still a bundle**, with a receipt, `harness add` and gap filling, so a site created before a feature became standard can get it with `harness add`. (Maintainer's choice.) - **The JSON marks standard features so deckle-hosted can hide them.** (Maintainer's requirement.) The field is additive and `formatVersion` stays 1, because deckle-hosted ignores version-1 fields it doesn't know. - **A standard feature cannot be removed**, like a protected TYPO3 system extension. (Maintainer's choice; rejected: removable like any other feature.) The refusal uses the existing `error` status because deckle-hosted's status switch is exhaustive: it shows the harness's message for `error`, and it would fail on a new status. - **The code only the removal of categories used is deleted, not kept unused.** (Maintainer's choice; rejected: keeping it so that making categories optional again would need no new code.) If categories ever become removable again, their removal is rebuilt. - **Existing sites get a standard feature only through `harness add`.** (Maintainer's choice.) `harness serve` must not install one: the editor's publish carries only `content/` and `assets/media/`, so files it wrote would never reach `main`. No upgrade command is needed while no deckle sites exist (as of 2026-10-06). - **A failed standard install fails `init` without rolling back.** (Maintainer's choice.) On an empty target directory only I/O errors remain, and a test proves that every standard bundle installs on the starter site. - **deckle-hosted is not changed here.** Until it hides standard features, it breaks in the ways listed under Out of scope. (The maintainer accepted this because no sites exist yet.) - **One issue, one pull request.** ## Key interfaces - `addcmd.Manifest` and `parseManifest` (`internal/addcmd/manifest.go`). `Catalogue()` and `addcmd.Info` carry the flag to the CLI. - `featureEntryOutput` and `printFeatures` (`cmd/harness/features.go`). - `harness init`: `initSite` (`cmd/harness/main.go`), `initcmd.Run`, and `addcmd.Run` as the install path. - `harness remove`: `removeCmd` (`cmd/harness/features.go`) and `internal/removecmd` (`Run`, `scan`, `shippedPages`, `categoryChanges`, `Purge`, `ContentUsage`, `PurgeReport`). - `RevertConfig`, `configOp`, `featureConfig`, `revertContext` and `taxonomiesAfterRevert` (`internal/addcmd/config.go`), and the revert functions in `internal/addcmd/taxonomy.go`. - `featureHints` (`internal/addcmd/add.go`). - Tests that remove categories today, as a starting list: - `TestRunCategories*` (`internal/removecmd/run_test.go`); - `TestPurgeStripsBeforeDeletingCategories` (`purge_test.go`); - the `CategoriesRemovedFrom` expectations in `usage_test.go`; - `TestRemoveCmdCategoriesRemovedFrom` and `TestCategoriesCmdRoundTrip` (`cmd/harness/features_test.go`); - the cases in `internal/addcmd/config_test.go` and `taxonomy_test.go` that revert the categories steps. The tests of the apply halves stay. ## Acceptance criteria - [ ] The categories manifest has `standard: true`. A manifest with an unknown key is still a load error. - [ ] `harness init` in an empty directory writes the starter site with the categories bundle installed, including its `hugo.toml` edits and `editor/features/categories.yaml`. Its output names `categories`. - [ ] A site from `harness init` builds with Hugo and renders `/categories/`. - [ ] A test fails when a starter-site file lies at a path that a standard bundle ships. - [ ] `harness add categories` right after `harness init` exits 0 with `already-installed` and changes nothing. - [ ] `harness features --json` gives every entry a `standard` boolean, with and without `--site`: `true` for `categories` and `false` for `news`, with `formatVersion` 1. The text form marks `categories` as standard. - [ ] `harness remove categories` on a site from `harness init`, with and without `--purge`, exits 1, reports status `error` in `--json` with a message naming it a standard feature, and leaves the tree byte-identical. - [ ] `harness remove categories` on a site without its receipt reports `not-installed` and exits 0. - [ ] `harness add news` and `harness remove news` behave as before, and `usage.categoriesRemovedFrom` is present and `[]` in every `harness remove --json` object. - [ ] `harness add news` no longer mentions `harness add categories`. - [ ] The shipped-pages rule, the category rewrites in remove and the three config reverts are gone, with their tests. `RevertConfig` reports a recorded edit with no revert as kept. - [ ] Deleting a category in the Categories module still removes its slug from every page and element. - [ ] The README and `scaffold/site/AGENTS.md` document standard features and the refusal to remove one, describe categories as installed by `harness init`, and no longer describe removing categories. The AGENTS.md drift test passes. ## How to verify - `make test`. It builds `web/dist` first, which `go test ./...` needs. The theme's Hugo build tests run only with `hugo` on PATH (CI installs 0.163.3). - `cd web && npm test`. - In a scratch directory: 1. `harness init`. 2. `harness features --site . --json` shows categories as standard and installed. 3. `harness add categories --json` reports `already-installed`. 4. `harness remove categories --json` and `harness remove categories --purge --json` both report status `error`, and the tree is unchanged. 5. `harness add news --json`, then `harness remove news --json`, behave as before. 6. `hugo` builds the site. ## Assumptions to check - `internal/addcmd`'s tests are in `package addcmd` and import `internal/initcmd`. If `initcmd` imports `addcmd`, those tests stop compiling because of an import cycle. Check this before deciding where `init` installs standard bundles. - Several test helpers build their site with `initcmd.Run` and then add or remove categories, expecting a starter site without categories. Which of them change depends on where the install lives. - Nothing except removing categories reaches the shipped-pages rule: news ships no content page. Confirm before deleting it. ## Out of scope - deckle-hosted. Until it reads `standard`, the maintainer accepts the following: - Its new-site dialog still offers Categories, and ticking it fails site creation, because its scaffold treats `already-installed` as an error (`addFeature` in `gitserve/scaffold.go`). - Its site Features page still offers Remove for Categories, which shows the harness's refusal. - `TestRealHarnessLifecycle` (`harness/real_test.go`) and `TestScaffoldRealBinaries` (`gitserve/scaffold_test.go`) fail, because they take the first feature listed, which is now `categories`. Both run only with `DECKLE_HOSTED_TEST_HARNESS` set. Its other real-binary tests were not checked. - Installing standard features on existing sites automatically. - An `init` option to skip standard features. - Making news standard. - Changing the categories bundle's files or `version`. The manifest is never copied into a site, so marking the bundle standard changes no site file. ## Blocked by None - can start immediately
dominik.polakovics changed title from Standard feature bundles: installed automatically, marked in the features JSON to Standard feature bundles: installed by harness init, marked in the features JSON, not removable 2026-10-07 12:41:21 +02:00
Sign in to join this conversation.
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#157
No description provided.