From e2b90e483aa2a7d8b303031421c2b1b35930857d Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:05:45 +0100 Subject: [PATCH] refactor: adopt the .abcd/development namespace for developer docs (v0.7.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align ferry with the sibling abcd-cli repository so that docs/ is user-facing only. Developer-facing records move out of the user-facing docs/ tree into a new .abcd/development/ namespace, and ferry's repo scaffolding now stamps that layout. - Move docs/plans, docs/research, docs/decisions -> .abcd/development/{plans, research,decisions}; docs/ now holds only the four Diátaxis directories. - Scaffold generator: internal/agents/scaffold.go now creates .abcd/development/{plans,research,decisions} instead of docs/* for those dev dirs (docs/ still seeds its README); golden tests updated accordingly. - Release + lint tooling repointed: check-plan-shipped.sh, release.yml, consistency-lint.sh. - Docs/agent text repointed: docs/README.md, CONTRIBUTING.md, the two release/ scaffold how-tos, AGENTS.md. - Add .abcd/ structure READMEs; record the CLI-surface and docs-structure research under .abcd/development/research/. Working memory (.work/, .work.local/) stays at the repo root; folding it under .abcd/ is a follow-up phase. The cross-repo convention (ABCDevelopment/CLAUDE.md) is updated to match. CHANGELOG: [0.7.1] Changed. --- .abcd/README.md | 14 + .abcd/development/README.md | 17 + .../0001-adr-naming-sequential-madr.md | 3 +- .../0002-work-memory-public-private-split.md | 0 .abcd/development/decisions/README.md | 11 + .../development}/plans/2026-07-05-v0.4.0.md | 0 .../development}/plans/2026-07-05-v0.5.0.md | 0 .../development}/plans/2026-07-06-v0.6.0.md | 0 .../development}/plans/2026-07-06-v0.7.0.md | 0 .abcd/development/plans/2026-07-08-v0.7.1.md | 45 +++ {docs => .abcd/development}/plans/README.md | 2 +- .../2026-07-08-cli-surface-vs-sota.md | 341 ++++++++++++++++++ .../2026-07-08-docs-structure-proposal.md | 337 +++++++++++++++++ .abcd/development/research/README.md | 9 + .github/workflows/release.yml | 2 +- .work/DECISIONS.md | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 12 + CONTRIBUTING.md | 17 +- docs/README.md | 10 +- docs/how-to/cutting-a-release.md | 2 +- docs/how-to/scaffold-a-repo.md | 4 +- internal/agents/scaffold.go | 30 +- internal/agents/scaffold_golden_test.go | 16 +- internal/agents/scaffold_test.go | 14 +- scripts/check-plan-shipped.sh | 4 +- scripts/consistency-lint.sh | 9 +- 27 files changed, 852 insertions(+), 51 deletions(-) create mode 100644 .abcd/README.md create mode 100644 .abcd/development/README.md rename {docs => .abcd/development}/decisions/0001-adr-naming-sequential-madr.md (95%) rename {docs => .abcd/development}/decisions/0002-work-memory-public-private-split.md (100%) create mode 100644 .abcd/development/decisions/README.md rename {docs => .abcd/development}/plans/2026-07-05-v0.4.0.md (100%) rename {docs => .abcd/development}/plans/2026-07-05-v0.5.0.md (100%) rename {docs => .abcd/development}/plans/2026-07-06-v0.6.0.md (100%) rename {docs => .abcd/development}/plans/2026-07-06-v0.7.0.md (100%) create mode 100644 .abcd/development/plans/2026-07-08-v0.7.1.md rename {docs => .abcd/development}/plans/README.md (93%) create mode 100644 .abcd/development/research/2026-07-08-cli-surface-vs-sota.md create mode 100644 .abcd/development/research/2026-07-08-docs-structure-proposal.md create mode 100644 .abcd/development/research/README.md diff --git a/.abcd/README.md b/.abcd/README.md new file mode 100644 index 0000000..3e3dd8e --- /dev/null +++ b/.abcd/README.md @@ -0,0 +1,14 @@ +# .abcd/ + +ferry's development namespace. Everything here is developer-facing material — it +stays in the repo (transparent) but never ships to a ferry user. User-facing +documentation lives under [`../docs/`](../docs/), which is the only dev-adjacent +tree written for the reader of the CLI. + +- **[`development/`](development/)** — the durable record (committed): plans, + research, and architecture decisions. The specification the build works from. + +Working memory (`.work/`, committed shared orientation, and `.work.local/`, +git-ignored local ephemera) currently sits at the repo root; a phase-2 migration +folds it under this namespace (`.abcd/work/`, `.abcd/.work.local/`). Until then, +see [`../AGENTS.md`](../AGENTS.md) for the working-memory split. diff --git a/.abcd/development/README.md b/.abcd/development/README.md new file mode 100644 index 0000000..50dad8c --- /dev/null +++ b/.abcd/development/README.md @@ -0,0 +1,17 @@ +# Development record + +ferry's durable design record — the "what / why" the build works from. Kept in +the repo (transparent) but developer-facing, never shipped to a ferry user; +user-facing documentation lives under [`../../docs/`](../../docs/). Organised +flat by artefact type, one canonical home per concept: + +| Folder | What it holds | Naming | +|---|---|---| +| [`plans/`](plans) | Dated design / implementation plans — "here's how we will build X". | `YYYY-MM-DD-topic.md` | +| [`research/`](research) | Investigations that inform design without being decisions — SOTA surveys, spikes, prior-art reviews. | `YYYY-MM-DD-topic.md` | +| [`decisions/`](decisions) | Architecture Decision Records (MADR) — settled decisions, the *why* plus rejected alternatives. | `NNNN-title.md` | + +**Conventions.** Plans and research notes are date-prefixed (chronological); +ADRs use a sequential `NNNN` prefix (stable cross-reference handles). Present +tense only — history lives in git. A decision extracted from a plan graduates to +an ADR in [`decisions/`](decisions). diff --git a/docs/decisions/0001-adr-naming-sequential-madr.md b/.abcd/development/decisions/0001-adr-naming-sequential-madr.md similarity index 95% rename from docs/decisions/0001-adr-naming-sequential-madr.md rename to .abcd/development/decisions/0001-adr-naming-sequential-madr.md index 7ac4058..26d5836 100644 --- a/docs/decisions/0001-adr-naming-sequential-madr.md +++ b/.abcd/development/decisions/0001-adr-naming-sequential-madr.md @@ -7,7 +7,8 @@ ## Context and problem statement -ferry records architecture decisions as MADR files under `docs/decisions/`. We +ferry records architecture decisions as MADR files under +`.abcd/development/decisions/`. We need one naming convention that sorts stably, reads unambiguously, and does not force coordination between branches before a decision can be written down. A superseded draft proposed a `--` scheme (for example diff --git a/docs/decisions/0002-work-memory-public-private-split.md b/.abcd/development/decisions/0002-work-memory-public-private-split.md similarity index 100% rename from docs/decisions/0002-work-memory-public-private-split.md rename to .abcd/development/decisions/0002-work-memory-public-private-split.md diff --git a/.abcd/development/decisions/README.md b/.abcd/development/decisions/README.md new file mode 100644 index 0000000..d801b93 --- /dev/null +++ b/.abcd/development/decisions/README.md @@ -0,0 +1,11 @@ +# Decision records + +Architecture Decision Records (ADRs) in [MADR](https://adr.github.io/madr/) +format — retrospective, settled decisions that record the *why* and the +alternatives rejected. + +Each ADR is named `NNNN-title.md` with a sequential `NNNN` prefix (stable +cross-reference handles, unlike the date-prefixed plans and research). Authors +take the next free number optimistically on their branch; a collision blocks the +merge so the second branch to land renumbers (see +[ADR 0001](0001-adr-naming-sequential-madr.md)). Present tense only. diff --git a/docs/plans/2026-07-05-v0.4.0.md b/.abcd/development/plans/2026-07-05-v0.4.0.md similarity index 100% rename from docs/plans/2026-07-05-v0.4.0.md rename to .abcd/development/plans/2026-07-05-v0.4.0.md diff --git a/docs/plans/2026-07-05-v0.5.0.md b/.abcd/development/plans/2026-07-05-v0.5.0.md similarity index 100% rename from docs/plans/2026-07-05-v0.5.0.md rename to .abcd/development/plans/2026-07-05-v0.5.0.md diff --git a/docs/plans/2026-07-06-v0.6.0.md b/.abcd/development/plans/2026-07-06-v0.6.0.md similarity index 100% rename from docs/plans/2026-07-06-v0.6.0.md rename to .abcd/development/plans/2026-07-06-v0.6.0.md diff --git a/docs/plans/2026-07-06-v0.7.0.md b/.abcd/development/plans/2026-07-06-v0.7.0.md similarity index 100% rename from docs/plans/2026-07-06-v0.7.0.md rename to .abcd/development/plans/2026-07-06-v0.7.0.md diff --git a/.abcd/development/plans/2026-07-08-v0.7.1.md b/.abcd/development/plans/2026-07-08-v0.7.1.md new file mode 100644 index 0000000..601239c --- /dev/null +++ b/.abcd/development/plans/2026-07-08-v0.7.1.md @@ -0,0 +1,45 @@ +# ferry v0.7.1 — developer documentation moves to the `.abcd/` namespace + +**Status:** shipped in v0.7.1 — the durable-record move and its tooling ripple +are delivered. +**Owner:** REPPL. **Type:** implementation plan (Diátaxis: one plan, no mixing). + +## 1. Goal and scope + +Align ferry with the sibling `abcd-cli` model so `docs/` carries user-facing +documentation only and developer-facing records move to a `.abcd/development/` +namespace. This is a structural migration — `git mv` plus text edits, no runtime +behaviour change. + +In scope for v0.7.1 (the durable-record move and its ripple): + +- Move `docs/plans/`, `docs/research/`, and `docs/decisions/` (ADRs) to + `.abcd/development/{plans,research,decisions}/`. +- Add structure READMEs: `.abcd/README.md`, `.abcd/development/README.md`, and + the per-directory READMEs, describing what each area holds and the naming + conventions (`YYYY-MM-DD-topic.md` for plans and research; `NNNN-title.md` + MADR for decisions). +- Change the scaffold generator (`internal/agents/scaffold.go`) so a scaffolded + repo gets `docs/` = Diátaxis only and `.abcd/development/{plans,research, + decisions}` for developer records; regenerate the golden fixtures. +- Repoint the release and consistency tooling (`scripts/check-plan-shipped.sh`, + `scripts/consistency-lint.sh`, `.github/workflows/release.yml`) and the docs + and agent text at the new paths. + +## 2. Out of scope (deferred) + +- Folding working memory (`.work/`, `.work.local/`) under the `.abcd/` + umbrella. That is an approved phase-2 migration handled separately. +- Any cross-repo convention change to the shared `ABCDevelopment/CLAUDE.md`. + +## 3. Approach + +The proposal in +[`.abcd/development/research/2026-07-08-docs-structure-proposal.md`](../research/2026-07-08-docs-structure-proposal.md) +maps both repos and sets the ordered migration ripple. Because the release gate +is tag-triggered on `main`, the moves and the tooling updates land in one change +so `main` is always internally consistent. The scaffold golden tests fail loudly +until their fixtures match the new layout — that lockstep is the safety net. + +The change is fully reversible: `git mv` plus text edits, no data migration and +no release cutover. diff --git a/docs/plans/README.md b/.abcd/development/plans/README.md similarity index 93% rename from docs/plans/README.md rename to .abcd/development/plans/README.md index 33336e0..1b7d302 100644 --- a/docs/plans/README.md +++ b/.abcd/development/plans/README.md @@ -1,6 +1,6 @@ # Release plans -Reference for the design plans under `docs/plans/`. +Reference for the design plans under `.abcd/development/plans/`. Each plan captures the design for one release. A plan is named `YYYY-MM-DD-vX.Y.Z.md`, where the date is when the plan was written and the diff --git a/.abcd/development/research/2026-07-08-cli-surface-vs-sota.md b/.abcd/development/research/2026-07-08-cli-surface-vs-sota.md new file mode 100644 index 0000000..521eccf --- /dev/null +++ b/.abcd/development/research/2026-07-08-cli-surface-vs-sota.md @@ -0,0 +1,341 @@ +# ferry's CLI surface against the state of the art + +Date: 2026-07-08 · Type: explanation / research + +This note audits ferry's command-and-flag surface, measures it against the +leading config/dotfile managers, and judges whether the top level carries more +verbs than it needs. It changes no code: it is a recommendation the maintainer +can accept, reject, or defer, verb by verb. + +The headline: ferry exposes **twelve top-level commands**. That is at the +crowded end of the field but not an outlier — chezmoi carries far more. The +problem is not the raw count; it is that a handful of those verbs earn their +top-level slot weakly — `export` and `import` are a matched pair the field would +nest under one noun, `diff` duplicates `apply --dry-run`, and `sync` has no +precedent in any surveyed tool. The core lifecycle (`apply`, `capture`, +`restore`, `status`, `init`) and the already-nested `agents` noun are all +correctly shaped. A disciplined pass brings the everyday surface down to a +**five-verb core** without losing any capability. + +## Current surface (from the code) + +Enumerated from `cmd/*.go` (`rootCmd.AddCommand(...)` registrations in +`cmd/commands.go`, `cmd/agents.go`, `cmd/export.go`, `cmd/import.go`, +`cmd/version.go`), cross-checked against `docs/reference/commands.md` and the +generated `docs/reference/cli/*.md`. + +Two files that look like commands are **not** commands: `cmd/context.go` is the +internal `loadContext` helper plus the `~/.ssh` path guard, and `cmd/wizard.go` +is the first-run wizard machinery folded into `init`. Neither registers a +verb. So the true surface is twelve top-level verbs (one of which, `agents`, is +a parent noun with two subcommands). + +| Command | Level | Purpose | Flags | +|---|---|---|---| +| `init` | top | First-run setup: locate/clone the config repo, write ferry's config, run the adoption wizard | `--fresh`, `--yes`, `--apply`, `--github`, `--no-wizard`, `--repair`, `--wizard-answers ` | +| `apply` | top | Reconcile this machine to the repo (deploy dotfiles, terminal settings) | `--deps`, `--dry-run`, `--force`, `--skip-wizard` | +| `capture` | top | Pull local changes back into the repo (interactive, selective, secret-gated) | *(none)* | +| `sync` | top | Publish captured changes and pull remote ones for a managed repo | `--message/-m `, `--allow-unmanaged` | +| `status` | top | Report config drift (git-status-like classification) | *(none)* | +| `doctor` | top | Report machine/tool health and check managed-target invariants | *(none)* | +| `diff` | top | Preview what `apply` would change (read-only) | *(none)* | +| `restore` | top | Reverse ferry's changes to the pre-ferry baseline from backup | `--packages`, `--yes`, `--purge-without-recovery` | +| `export` | top | Write a portable, secret-scanned `.zip` bundle of tracked shared files | `--out `, `--include-local` | +| `import ` | top | Ingest a bundle into a fresh config repo, validate it, write ferry's config | `--out `, `--expect-sha256 `, `--include-local` | +| `agents` | top (noun) | Parent for agent-domain companion operations (no direct run) | *(none)* | +| `agents scaffold [name]` | sub | Set a project repo up for the multi-tool agent pipeline | `--private`, `--attribution` (mutually exclusive) | +| `agents adopt ` | sub | Migrate an existing symlink-based instruction directory into ferry | *(none)* | +| `version` | top | Print the version | `--verbose/-v` | + +There are **no persistent/global flags** — every flag is local to its command. +Total distinct flags: 24 across the tree. `init` alone carries 7; `apply` 4. + +## SOTA findings + +Seven leading tools were surveyed, all against tier-1 sources (official docs and +manuals). They span the full spectrum of surface size, and the smaller cores +converge on a common vocabulary. + +**Surface-size spectrum (minimal → maximal):** GNU Stow (one command, three mode +flags) < dotbot (one config-driven runner, no verbs) < mackup (three verbs) < +Nix home-manager (~12) < yadm (~17 own verbs plus all of git) < chezmoi (~50). +ferry's twelve top-level verbs sit between home-manager and yadm — mid-field, +not extreme. + +**The recurring core is a capture / apply / reverse triad plus inspect verbs.** +Reduced to essentials, every tool exposes some form of: + +| Concept | chezmoi | yadm | Stow | dotbot | home-manager | mackup | ferry | +|---|---|---|---|---|---|---|---| +| deploy / apply | `apply` | `git checkout` | *(default)* | `./install` | `switch` | `restore` | `apply` | +| capture local → source | `add` / `re-add` | `git add` | `--adopt` | *(manual git)* | — | `backup` | `capture` | +| reverse / remove | `forget` / `destroy` | `git rm` | `-D` | `clean` | `uninstall` | `uninstall` | `restore` | +| preview diff | `diff` | `git diff` | `-n` | `--dry-run` | — | — | `diff` | +| status | `status` | `git status` | — | — | `generations` | — | `status` | +| init / onboard | `init` | `init`/`clone` | — | — | `init` | — | `init` | + +ferry's `apply` / `capture` / `restore` naming maps almost exactly onto mackup's +`backup` / `restore` / `uninstall` triad while borrowing chezmoi's clearer +intent-based verbs. The two tools closest to ferry's remit (git-backed, +capture + apply + reverse) are **chezmoi** — the rich convention-setter — and +**mackup** — the minimal triad. ferry's core is well within convention. + +Key conventions extracted, each with its tier-1 basis: + +- **`status` and `diff` stay separate.** Every tool that has both keeps them + distinct: chezmoi's `status` is terse and scriptable, its `diff` is a full + unified patch — copied straight from git, which never merged the two. No + surveyed tool merges them. So ferry's having *both* `status` and `diff` is + correct and idiomatic; the redundancy to watch is elsewhere. + (chezmoi command reference; git(1) porcelain-vs-plumbing.) + +- **"What would apply change" is either a `diff` verb or a `--dry-run`/`-n` + flag, never both.** Feature-rich tools give it a verb (`chezmoi diff`); + minimal tools give it a flag (`stow -n`, `dotbot --dry-run`). ferry ships + *both* `diff` and `apply --dry-run` for the same product — the one duplication + the field would not tolerate. (GNU Stow manual; dotbot README; chezmoi + reference.) + +- **`init` absorbs onboarding; no tool has a standalone `wizard`/`setup` + verb.** chezmoi's `init ` clones, prompts via templates, and can apply + in one shot; home-manager's `init [--switch]` generates and optionally + activates. ferry already follows this — its wizard is folded into `init`, not + a separate verb — which the field endorses. (chezmoi reference; home-manager + tools appendix.) + +- **Sync is delegated to git, not reimplemented.** yadm and dotbot expect the + user to run `git push`/`git pull` directly; chezmoi's one sanctioned wrapper + is `update` (= `git pull` + `apply`), named for the user's *intent*, not the + mechanism. No surveyed tool ships a bespoke `sync`/`export`/`import` verb for + remote movement. ferry's `sync` (pull + commit + push, deliberately *without* + apply) is a publish wrapper with no direct field analogue — closest to + chezmoi `update` but inverted (publish rather than update). (yadm manual; + chezmoi reference.) + +- **Archive/bundle I/O is nested under a noun.** git groups `bundle create` / + `verify` / `unbundle` under one `git bundle`; chezmoi keeps `archive` / `dump` + / `import` as archive tooling, and `import` is tarball-only. Top-level flat + `export`/`import` verbs for portable bundles are not the norm. (git(1); + chezmoi reference.) + +- **"Adopt an existing file" has no settled home** [CONTESTED]: Stow makes it a + `--adopt` flag, chezmoi folds it into `add`, home-manager refuses in-place + adoption entirely. The weight of the feature-rich tools is against a dedicated + top-level `adopt` verb — it belongs on the capture path. ferry already keeps + file adoption inside `capture`/`init` and reserves `agents adopt` for the + distinct symlink-migration case, which is consistent with this. (GNU Stow + manual; chezmoi reference; home-manager manual.) + +- **git's porcelain/plumbing split** is the guiding philosophy: keep the top + level a small set of stable, task-focused verbs; push low-level or scripting + operations into a separate, independently-stable tier. (git(1).) + +Sources (all tier 1): chezmoi command reference +(chezmoi.io/reference/commands); yadm manual (yadm-dev/yadm `yadm.md`) and +yadm.io; GNU Stow manual (gnu.org/software/stow); dotbot README +(github.com/anishathalye/dotbot); home-manager manual tools appendix +(home-manager.dev); mackup README (github.com/lra/mackup); git(1) +porcelain-vs-plumbing (git-scm.com/docs/git). + +## Per-command verdicts + +Each verdict is one of KEEP / KEEP-BUT-RENAME / FOLD-INTO-X / DEMOTE-TO-FLAG / +REMOVE, with a rationale grounded in the SOTA conventions above and ferry's own +purpose, plus the migration cost (ferry is pre-1.0, so a breaking CLI change is +allowed but must carry a `Breaking` changelog entry per +`docs/reference/compatibility.md`). + +### `apply` — KEEP (core) + +The irreducible verb: reconcile the machine to the repo. Every tool in the +field has this exact concept under this or a near-identical name (chezmoi +`apply`, home-manager `switch`, mackup `restore`, stow `stow`). No change. + +### `capture` — KEEP (core) + +The reverse of `apply`: pull live edits back into the source of truth. chezmoi +splits this into `add` (new file) and `re-add` (update tracked files); +ferry folds both into one interactive, secret-gated verb, which is the better +shape for ferry's "review each change and route it shared/local" model. Keep +the single verb. The name `capture` is ferry's own; it reads well against +`apply` and is worth keeping over chezmoi's `add`/`re-add` split. + +### `status` — KEEP (core) + +git-style drift report. Universal. No change. + +### `restore` — KEEP (core) + +ferry's differentiator: a backed-up, reversible undo of everything ferry +deployed. No mainstream dotfile manager offers this as a first-class, +baseline-backed operation (mackup's `uninstall` is the nearest, and it is +weaker). This is load-bearing to ferry's "every change is backed up first" +promise. Keep, top-level. + +### `init` — KEEP (secondary, justified) + +Universal bootstrap verb (chezmoi `init`, yadm `clone`, home-manager +`init`). The concern is not the verb but its **7 flags** — the widest in the +tree — which signal that `init` is doing three jobs: clone-or-create, run the +wizard, and optionally apply. See the flag audit below; the verb stays. + +### `diff` — KEEP the verb, DEMOTE the duplicate flag + +`diff` is read-only "what would `apply` change" — and ferry **also** ships +`apply --dry-run` with exactly that contract (`cmd/apply.go` reads a `dry-run` +flag whose own help says "preview changes without writing (see also: ferry +diff)"). Two advertised names for one behaviour is the one duplication the field +would not tolerate: tools give the preview *either* a verb (`chezmoi diff`) *or* +a flag (`stow -n`, `dotbot --dry-run`), never both. Keeping the `diff` **verb** +is correct and idiomatic — chezmoi and git both keep a preview verb distinct +from `status`. So the recommendation is to **keep `diff`** as the canonical, +discoverable preview and **demote `apply --dry-run` to an unadvertised alias** +(or drop the flag), rather than the reverse. Migration cost: low — the flag can +keep working through one deprecation window while docs advertise only `diff`. + +### `doctor` — KEEP (secondary, justified) + +Health/invariant check. Common in mature tools (chezmoi `doctor`, `brew +doctor`, `flutter doctor`). It also verifies ferry's security invariants (no +symlinked targets, nothing under `~/.ssh`, everything inside `$HOME`) +read-only, which is genuinely distinct from `status` (drift) — `doctor` answers +"is the environment and my own footprint sane", `status` answers "what +diverged". Keep. + +### `sync` — KEEP-BUT-SCRUTINISE (secondary, weakly justified) + +`sync` is a convenience wrapper: pull remote, commit captured changes, push — +without ever running `apply` or force-pushing. It does real work the primitives +do not compose for free (a safe, secret-gated, non-force publish sequence), so +it is not pure sugar. But it sits awkwardly against the field: no surveyed tool +ships a bespoke `sync` verb — yadm and dotbot expect the user to run `git +push`/`git pull` directly, and chezmoi's one sanctioned wrapper is `update` +(= `git pull` + `apply`), named for the user's *intent*. ferry's `sync` is the +inverse of chezmoi `update` (it publishes rather than updates, and pointedly +does *not* apply — its own help tells the user to run `apply` afterwards), so it +has no direct precedent and automates half of a two-step ritual. The verb is +defensible for the git-shy target user, but it is the strongest candidate for +eventual folding into a git-passthrough model. Recommendation: **keep for now**, flag for review +once usage data exists. Do not add more flags to it. Migration cost of removal: +medium (it is the everyday-update verb for managed repos; removing it re-exposes +raw git steps). + +### `export` / `import` — FOLD into a `bundle` noun (secondary, justified capability, wrong shape) + +These are a matched pair — write a portable, secret-scanned, reproducible-SHA +`.zip` for an offline move, and ingest one — but they occupy two top-level +slots and share the `--include-local` flag, which is the classic signal of a +noun wanting to own its verbs. The field nests this kind of thing: git puts +`bundle create` / `bundle verify` / `bundle unbundle` under one `git bundle` +noun; chezmoi keeps archive I/O under `chezmoi archive` and `dump`. +Recommendation: **fold to `ferry bundle export` / `ferry bundle import`** (and a +future `ferry bundle verify` for the `--expect-sha256` check has an obvious +home). This reclaims two top-level slots for one clearly-scoped noun and reads +better (`export`/`import` at the top level are ambiguous about *what*). +Migration cost: low–medium (two verbs move under a noun; old spellings can alias +through one deprecation window). If the maintainer values the shorter top-level +spelling for a common offline-move workflow, KEEP-as-is is defensible — this is +a genuine judgement call, flagged below. + +### `agents` — KEEP as a noun, but confirm the boundary (secondary, justified) + +`agents` is already correctly a parent noun with subcommands (`scaffold`, +`adopt`), not a flat verb — this is the shape the field uses for grouped, +domain-specific operations, and it is the right call. The only question is +whether it should sit at the top level or under a broader grouping. It should +**stay top-level**: the agents domain is a first-class ferry concern (it has its +own section in the manifest and rides the normal `apply`/`status`/`diff`/ +`restore` lifecycle), and burying it under a generic `config`/`manage` grouping +would hide a headline feature. No change. + +### `version` — KEEP (universal) + +Standard. No change. (Cobra also provides `--version`; the subcommand is the +conventional redundancy and is fine.) + +## Recommended lean surface + +The everyday core a user must learn shrinks to **five verbs** — +`init`, `apply`, `capture`, `status`, `restore` — with `diff` and `doctor` as +the two secondary verbs they reach for occasionally, and everything else nested +or aliased. + +Before → after (top-level surface): + +| Before (12 top-level) | After (proposed) | +|---|---| +| `init` | `init` | +| `apply` | `apply` (`--dry-run` demoted to an unadvertised alias of `diff`) | +| `capture` | `capture` | +| `status` | `status` | +| `restore` | `restore` | +| `doctor` | `doctor` | +| `diff` | `diff` (kept as the one canonical preview verb) | +| `sync` | `sync` (kept, frozen — no new flags; review later) | +| `export` | `bundle export` | +| `import` | `bundle import` | +| `agents` (+scaffold/adopt) | `agents` (+scaffold/adopt) — unchanged | +| `version` | `version` | + +Net top-level verbs: **12 → 11** — `export` + `import` collapse into one +`bundle` noun; `diff` is kept as the single canonical preview and the duplicate +`apply --dry-run` flag is demoted, so no verb is lost there. The *core a +newcomer must learn* is 5 (`init`, `apply`, `capture`, `status`, `restore`); the +*full everyday set* is 7 (core + `diff` + `doctor`). That lands ferry +comfortably inside the field's norms while keeping every capability reachable. + +### Flag audit + +- **`init` carries 7 flags** — the tree's widest. `--yes`, `--no-wizard`, and + `--wizard-answers` all steer the wizard's interactivity and overlap in intent + (`--yes` implies `--no-wizard`); `--repair` only functions with an interactive + wizard or `--wizard-answers`. This is a lot of surface for one verb. No cut is + urgent, but the cluster is worth a dedicated simplification pass — e.g. a + single `--wizard=off|interactive|answers:` mode flag could replace three + booleans. Flagged as a maintainer decision, not a recommendation. +- **`apply --dry-run` vs `diff`** — the redundancy addressed above; collapse to + one advertised spelling. +- **`--include-local`** appears on both `export` and `import` — expected for a + matched pair, and it moves cleanly under the `bundle` noun. +- **`restore --purge-without-recovery`** is a long, scary, correctly-named + irreversible flag — keep exactly as is. +- No redundant or dead flags found elsewhere; `capture`, `status`, `diff`, + `doctor` are flagless, which is the right restraint. + +## Ranked change list (value of simplification ÷ migration cost) + +1. **Fold `export` + `import` → `bundle export` / `bundle import`.** *(high + value ÷ low–medium cost)* Reclaims two top-level slots, groups a matched + pair, gives `--expect-sha256` a natural `bundle verify` home. Pure rename; + alias the old spellings for one minor release, `Breaking` changelog entry. +2. **Advertise one preview spelling: keep the `diff` verb, demote `apply + --dry-run`.** *(high value ÷ low cost)* Removes a genuine + two-names-one-behaviour redundancy. The field keeps a preview *verb* distinct + from status (git, chezmoi), so `diff` is the idiomatic canonical form; + `apply --dry-run` becomes an unadvertised alias (or is dropped) through one + deprecation window. +3. **Simplify `init`'s wizard flag cluster** (7 → ~4 via a mode flag). *(medium + value ÷ medium cost)* Not a removal, a consolidation; touches the widest verb. + Do it as its own pass, well-tested, since `init` is the first thing a new user + runs. +4. **Freeze `sync`; revisit against usage.** *(low value now ÷ medium cost to + remove)* Keep it, add no flags, and reconsider folding it into a + git-passthrough model once there is evidence of how often it is used versus + users driving git directly. +5. **Leave `agents`, `doctor`, `restore`, `version`, `status`, `capture`, + `apply`, `init` as-is.** *(no change)* These are correctly shaped and + correctly placed. + +## Open decisions for the maintainer + +- **`bundle` noun vs. flat `export`/`import`.** The fold is the field-idiomatic + shape, but flat top-level verbs are shorter to type for a common offline-move. + Genuine judgement call — grouping wins on tidiness, flat wins on ergonomics. +- **Whether to keep `apply --dry-run` at all** once `diff` is the advertised + preview. Retaining it as a silent alias costs nothing; dropping it is a + cleaner surface. Low-stakes either way. +- **`sync`'s long-term place.** Convenience wrapper for the git-shy user, or a + crutch that should give way to a git-passthrough? Needs usage evidence. +- **`init`'s flag consolidation.** Whether the wizard-mode flags are worth + reshaping into one mode flag, given `init` runs once per machine and the + current flags each map to a documented behaviour. diff --git a/.abcd/development/research/2026-07-08-docs-structure-proposal.md b/.abcd/development/research/2026-07-08-docs-structure-proposal.md new file mode 100644 index 0000000..3cd5ac3 --- /dev/null +++ b/.abcd/development/research/2026-07-08-docs-structure-proposal.md @@ -0,0 +1,337 @@ +# Unified repository documentation structure — proposal + +Date: 2026-07-08 · Status: proposal (not adopted) · Type: research + +> This note lives in ferry's current internal research home, `.work/research/`. +> If the proposal below is adopted, this file moves to +> `.dev/development/research/` (or the chosen namespace) along with the rest of +> the durable record. + +The maintainer's goal: **`docs/` carries user-facing text only; developer-facing +text — plans, research, decisions, roadmap, working memory — lives elsewhere.** +The sibling `abcd-cli` repo already does this through a single `.abcd/` +namespace. This note maps both repos, contrasts them, and proposes a concrete +unified structure for ferry plus the exact migration ripple. It changes nothing; +it recommends. + +--- + +## 1. abcd-cli — the target model + +abcd-cli keeps **all** development material under one top-level namespace, +`.abcd/`, and reserves `docs/` for user-facing Diátaxis pages only. `.abcd/` +stays in-tree (transparent) but is excluded from the release artifact. + +### 1.1 The three tiers (durability × sharing) + +Documented in `.abcd/README.md` and `AGENTS.md`: + +| Tier | Path | Committed? | Holds | +|---|---|---|---| +| **Durable record** | `.abcd/development/` | yes | brief, intents, decisions/adrs, plans, research, roadmap, principles — the specification the build works from | +| **Shared working** | `.abcd/work/` | yes | `CONTEXT.md` (current orientation), `DECISIONS.md` (append-only log), `issues/`, `reviews/` | +| **Local ephemeral** | `.abcd/.work.local/` | gitignored | `NEXT.md` handover, `scratch/`, `logs/` — per-worktree, never merge-conflicts | + +### 1.2 Where each dev-doc category lives, and why + +Each `development/` subdirectory carries a README that states what it is for. +Organised **flat by artefact type**, one canonical home per concept: + +| Category | abcd-cli path | Naming | Rationale (from the READMEs) | +|---|---|---|---| +| Plans | `.abcd/development/plans/` | `YYYY-MM-DD-topic.md` | Dated because chronological; "how we will build X". A decision extracted from a plan graduates to an ADR. | +| Research | `.abcd/development/research/` | `notes/` dated + `prompting/`, `spikes/` | Investigations that inform design without being decisions. May seed an intent or ADR. Exempt from record-lint present-tense rules. | +| Decisions (ADRs) | `.abcd/development/decisions/adrs/` | `NNNN-title.md` (MADR) | Retrospective, settled decisions. Stable sequential handles for cross-reference. Plus `notes/`. | +| RFCs | `.abcd/development/roadmap/rfcs/` | — | Prospective, contested decisions; an accepted RFC produces an ADR. | +| Roadmap | `.abcd/development/roadmap/phases/` | `phase-N-slug.md` | Sequencing axis (replaces version language). | +| Intents | `.abcd/development/intents/{drafts,planned,shipped,disciplines,superseded}/` | `itd-N-slug.md` | Press-release-shaped user-facing "why"; lifecycle encoded by directory. | +| Brief | `.abcd/development/brief/` | numbered folders | The living canvas — what abcd IS. | +| Principles | `.abcd/development/principles/` | one per file | Distilled cross-cutting design rules. | +| Working memory | `.abcd/work/` + `.abcd/.work.local/` | `CONTEXT.md`, `DECISIONS.md` / `NEXT.md` | Durable-vs-working-vs-ephemeral split. | + +`docs/` (abcd-cli) is purely the four Diátaxis directories plus `assets/`; its +`docs/README.md` explicitly points development records at `../.abcd/development/`. + +### 1.3 How the split is enforced by tooling + +- **`.abcd/docs-lint.json`** — `"roots": ["docs", "README.md"]`. Bans + change-narration tense and specific harness names in user-facing prose; + enforces `links_resolve` and a `stray_root_docs` allowlist. Runs in CI as + `abcd docs lint` (blocking). +- **`.abcd/record-lint.json`** — `"roots": [".abcd/development"]`. A separate + lint over the *record* (no git metadata, links resolve, lifecycle rules); + `exempt_paths` includes `.abcd/development/research/`. Runs as + `go run ./cmd/record-lint` (currently non-blocking drift gate). +- **`scripts/check-reviews.sh`** — deterministic gate over + `.abcd/work/reviews/`. +- **`.gitignore`** — only `.abcd/.work.local/` is ignored; `development/` and + `work/` are committed. +- **`.githooks/pre-commit`** reads its banlist from + `.abcd/.work.local/private-names.txt`; `pre-push` runs `make preflight`. + +The key move: **two lint roots**, one pointed at `docs/` (user-facing tone) and +one pointed at `.abcd/development/` (record integrity). The path boundary *is* +the enforcement boundary. + +--- + +## 2. ferry — the current state + +ferry mixes user-facing and developer-facing material inside `docs/`, and keeps +working memory at the repo root. + +### 2.1 Current layout + +``` +docs/ +├── tutorials/ how-to/ reference/ explanation/ # user-facing Diátaxis +├── assets/ +├── decisions/ # ADRs, NNNN-title.md ← developer-facing +├── research/ # YYYY-MM-DD-topic.md ← developer-facing +├── plans/ # YYYY-MM-DD-topic.md ← developer-facing +└── README.md # the map (rows for decisions/research/plans at lines 28-30) + +.work/ # committed shared working memory +├── CONTEXT.md DECISIONS.md research/2026-07-08-cli-surface-vs-sota.md +.work.local/ # gitignored: NEXT.md, scratch/, logs/, backups/, history/ +``` + +So ferry already has abcd's `work/` + `.work.local/` split — just at the repo +root, not nested under a namespace — and already keeps `.work/research/`. What +it lacks is a **durable-record home separate from `docs/`**: plans, research, +and ADRs currently sit *inside* `docs/`, contradicting the stated goal. + +### 2.2 Tooling wired to the current `docs/plans` + `docs/research` (+ `docs/decisions`) paths + +Every reference that a migration must update (verified by +`git grep "docs/plans\|docs/research\|docs/decisions"`): + +| # | File | Line(s) | What it does with the path | +|---|---|---|---| +| 1 | `internal/agents/scaffold.go` | 212 | `scaffoldDocsDirs = []string{"decisions", "research", "plans"}` — **the scaffold generator creates these under `docs/` in every repo it stamps** | +| 2 | `internal/agents/scaffold.go` | 206 | `docs-README.md` template (embedded) — the map it writes into scaffolded repos | +| 3 | `internal/agents/scaffold_golden_test.go` | 135-137, 203-205 | golden expectations asserting `docs/decisions`, `docs/plans`, `docs/research` are created | +| 4 | `internal/agents/scaffold_test.go` | 293 | asserts `docs/decisions`, `docs/research`, `docs/plans` exist | +| 5 | `scripts/check-plan-shipped.sh` | 50 | globs `"$ROOT"/docs/plans/*"$VERSION".md` for the release plan-shipped gate | +| 6 | `scripts/consistency-lint.sh` | 21, 47 | `decisions=docs/decisions` (ADR NNNN-uniqueness); exempts `docs/plans/` from the stale-`.work/NEXT.md` sweep | +| 7 | `.github/workflows/release.yml` | 70 | comment describing the `docs/plans/*.md` plan-status backstop (calls script #5) | +| 8 | `docs/README.md` | 28-30 | map rows declaring `decisions/`, `research/`, `plans/` under `docs/` | +| 9 | `CONTRIBUTING.md` | 51, 59-60, 67, 74 | "intent lives in `docs/plans/`"; ADRs "under `docs/decisions/`"; links to ADR 0001/0002 | +| 10 | `docs/how-to/cutting-a-release.md` | 36 | "a plan under `docs/plans/` marked shipped in vX.Y.Z" | +| 11 | `docs/how-to/scaffold-a-repo.md` | 35 | table row: `docs/decisions/`, `docs/research/`, `docs/plans/` created up front | +| 12 | `AGENTS.md` | 72 | link to `docs/decisions/0002-...md` | +| 13 | `.work/DECISIONS.md`, `CHANGELOG.md` | — | prose references to `docs/decisions/` (lower priority; text only) | +| 14 | **cross-repo** `../CLAUDE.md` (ABCDevelopment) | docs section | "`research/` and `plans/` under `docs/`; `decisions/` (ADRs)" — the shared convention that ferry's scaffold **propagates** | + +The load-bearing point: item 1/2/3/4 mean **ferry's convention is not a private +folder choice — the scaffold tool stamps `docs/{decisions,research,plans}` into +every new repo**, and item 14 codifies it across all ABCDevelopment repos. +Changing ferry's layout is therefore a cross-repo convention change. + +--- + +## 3. Compare and contrast + +| Axis | abcd-cli | ferry (today) | Divergence / cost | +|---|---|---|---| +| **What counts as `docs/`** | user-facing Diátaxis only | Diátaxis **plus** decisions/research/plans | ferry leaks dev docs into the shipped/user tree — the exact thing the goal forbids | +| **Durable record home** | `.abcd/development/{plans,research,decisions,roadmap,intents,brief,principles}` | none — scattered inside `docs/` | ferry has no single "the spec the build works from" root | +| **Shared working memory** | `.abcd/work/` (CONTEXT, DECISIONS) | `.work/` (CONTEXT, DECISIONS) | same split, different root — already aligned in spirit | +| **Local ephemeral** | `.abcd/.work.local/` | `.work.local/` | same split, different root — already aligned | +| **ADRs** | under `development/decisions/adrs/` (dev-facing) | under `docs/decisions/` (in the user tree) | ferry treats ADRs as user-facing; abcd treats them as record | +| **Namespace** | one umbrella `.abcd/` | two roots: `docs/` (mixed) + `.work*/` | ferry has no umbrella; dev material spans `docs/` and root | +| **Lint enforcement** | two roots: `docs-lint.json` → `docs/`, `record-lint.json` → `.abcd/development/` | one docs-oriented consistency-lint; ADR/plan gates hard-code `docs/...` paths | the path boundary is not an enforcement boundary in ferry | +| **Propagation** | abcd stamps its own `.abcd/` (it is the brand) | **ferry's scaffold stamps `docs/{decisions,research,plans}` into arbitrary third-party repos** | ferry's choice ripples to every scaffolded repo — the biggest cost of divergence | + +--- + +## 4. Recommended unified structure for ferry + +Adopt abcd's model: a single development-namespace umbrella, `docs/` reserved for +user-facing Diátaxis, the durable record moved out of `docs/`, and a second lint +root pointed at the record. + +### 4.1 Target tree + +``` +docs/ # USER-FACING Diátaxis only (unchanged content) +├── tutorials/ how-to/ reference/ explanation/ assets/ +└── README.md # map with the decisions/research/plans rows removed + +/ # NEW umbrella — namespace = open decision (§7.1) +├── README.md # "abcd-style" explainer: three tiers, what ships +├── development/ # DURABLE RECORD (committed, excluded from release) +│ ├── README.md # flat-by-artefact map (mirror abcd's) +│ ├── plans/ # ← docs/plans/ YYYY-MM-DD-topic.md +│ ├── research/ # ← docs/research/ + .work/research/ +│ ├── decisions/ # ← docs/decisions/ (ADRs; see §4.3) NNNN-title.md +│ └── roadmap/ # (optional; only if ferry wants a phases/rfcs axis) +├── work/ # SHARED WORKING (committed) ← .work/ +│ ├── CONTEXT.md DECISIONS.md +└── .work.local/ # LOCAL EPHEMERAL (gitignored) ← .work.local/ + ├── NEXT.md scratch/ logs/ history/ backups/ +``` + +Location-by-location mapping: + +| Current | Target | +|---|---| +| `docs/plans/` | `/development/plans/` | +| `docs/research/` | `/development/research/` | +| `.work/research/` | `/development/research/` (merge — same category) | +| `docs/decisions/` | `/development/decisions/` (recommended; see §4.3) | +| `.work/CONTEXT.md`, `.work/DECISIONS.md` | `/work/` | +| `.work.local/**` | `/.work.local/` | +| `docs/{tutorials,how-to,reference,explanation,assets}` | unchanged | + +### 4.2 Working-memory recommendation + +ferry's public/private split (`.work/` committed, `.work.local/` gitignored) +**already matches abcd** — only the root differs. Two options: + +- **(A) Fold under the umbrella** — `.work/` → `/work/`, `.work.local/` → + `/.work.local/`. Full abcd parity; one coherent namespace; the scaffold + generator already centralises these paths so doing it in the same pass is + cheap. Costs: touches ADR 0002, the `.git/info/exclude` logic in `scaffold.go`, + and `consistency-lint.sh`'s `.work.local/NEXT.md` rule. +- **(B) Keep at root** — leave `.work/` and `.work.local/` where they are; only + move the durable record out of `docs/`. Less ripple, and it already satisfies + the stated goal (working memory is *not* in `docs/` today). + +**Recommendation: (A) for the final target, staged.** Do the mandatory durable- +record move first (Phase 1 below); fold working memory into the umbrella as a +follow-up (Phase 2) so the two changes review independently. If the maintainer +wants a single PR, (A) in one pass is coherent — just larger. + +### 4.3 ADR location — recommendation: **move to the development namespace** + +abcd puts decisions under `development/decisions/`. The maintainer's own +principle is decisive: **ADRs record architecture rationale and rejected +alternatives — they are developer-facing, not user-facing.** They fail the +"does a ferry *user* read this?" test. Keeping them in `docs/` is the same +category error as plans and research. **Recommend moving `docs/decisions/` → +`/development/decisions/`.** Cost: update the ADR-uniqueness path in +`consistency-lint.sh` (line 21) and the ADR links in `CONTRIBUTING.md` and +`AGENTS.md`. (If the maintainer prefers ADRs remain semi-public, they can stay +in `docs/reference/decisions/` — but that reintroduces dev material into the +shipped tree and is not recommended.) + +### 4.4 Scope note — keep ferry lean + +abcd's `development/` is rich (brief, intents, roadmap/phases/rfcs, principles) +because abcd is an intent-driven product. **ferry need not adopt that whole +surface.** Start with `development/{plans,research,decisions}` — the three +categories ferry actually has — and add `roadmap/` only if ferry later wants a +phases axis. Do not import `intents/`, `brief/`, or `principles/` speculatively. + +--- + +## 5. Migration ripple — ordered checklist + +Ordering rule: **the release gate must never see a half-moved state.** Because +ferry's release is tag-triggered on `main` and PRs gate `main`, do the moves and +the tooling updates in **one PR** so `main` is always internally consistent. The +plan-shipped gate fails *safe* (a missing plan glob → "ok", exit 0), so a +mismatch would silently stop gating rather than go red — another reason to keep +the script update in the same commit as the `git mv`. + +**Phase 0 — decisions (STOP gate; maintainer sign-off before any move):** +1. Choose the namespace name (§7.1). +2. Decide ADR location (§4.3 recommends move). +3. Decide fold-vs-keep for working memory (§4.2 recommends fold, staged). +4. Decide cross-repo CLAUDE.md scope (§7.4). +5. Decide whether *scaffolded* repos get the new namespace or keep `docs/` dev + dirs (§7.5) — this gates the scaffold-generator change. + +**Phase 1 — the durable-record move (mandatory core):** +6. Create `/` skeleton with READMEs (mirror abcd's `.abcd/README.md`, + `development/README.md`, and per-subdir READMEs). No moves yet. +7. `git mv docs/plans/* /development/plans/`, + `docs/research/* /development/research/`, + `.work/research/* /development/research/`, and (if §4.3 accepted) + `docs/decisions/* /development/decisions/`. +8. **Scaffold generator** — `scaffold.go` line 212 `scaffoldDocsDirs` and the + dir-creation at ~line 261; the embedded `docs-README.md` template; then update + goldens: `scaffold_golden_test.go` (135-137, 203-205) and `scaffold_test.go` + (293). Tests fail loudly until goldens match — that is the safety net. +9. **Scripts** — `check-plan-shipped.sh` line 50 glob → new plans path; + `consistency-lint.sh` line 21 `decisions=` → new decisions path, line 47 + exempt `docs/plans/` → new plans path; `release.yml` line 70 comment. +10. **User-facing docs** — `docs/README.md` remove rows 28-30 and add the + "development records live under `/development/`" pointer (mirror abcd's + `docs/README.md`); `docs/how-to/cutting-a-release.md` line 36; + `docs/how-to/scaffold-a-repo.md` line 35. +11. **Agent/contributor docs** — `CONTRIBUTING.md` lines 51, 59-60, 67, 74; + `AGENTS.md` line 72 (ADR link) and its working-memory section; `.work/DECISIONS.md` + and `CHANGELOG.md` prose references. +12. **`.gitignore` / git-exclude** — if working memory folds now, update the + `.work.local/` exclude line and the `excludeWorkLocal` logic in `scaffold.go`; + otherwise leave for Phase 2. +13. **CHANGELOG** — add a user-facing entry (docs layout change). +14. **Validate** — `make build`, `gofmt -l .` (empty), `go vet ./...`, + `go test ./...`, then evals with `FERRY_BIN` set; + `scripts/consistency-lint.sh`; render/link-check the docs. All green = done. + +**Phase 2 — fold working memory (optional, if §4.2-A staged):** +15. `git mv .work/* /work/`, `.work.local/* /.work.local/`; update + ADR 0002, `consistency-lint.sh` NEXT.md rule, scaffold `.work*` paths + goldens, + AGENTS.md working-memory section, the `.git/info/exclude` line. + +**Phase 3 — cross-repo convention (maintainer decision, §7.4):** +16. Update `../CLAUDE.md` (ABCDevelopment) docs section — either scope the + `docs/{plans,research}` rule to "ferry-and-abcd use `/development/`" or + flip it for all repos. STOP for explicit maintainer choice. + +--- + +## 6. Risks, rollback, effort + +**Risks** +- **Broken internal links** — ADR references from `CONTRIBUTING.md`/`AGENTS.md` + and any doc cross-links. Caught by `consistency-lint.sh` links + a render pass. +- **Scaffold golden drift** — if goldens are not updated in lockstep the scaffold + tests fail. This is a *feature* (loud failure), not a hazard, provided step 8 + is atomic with the move. +- **Cross-repo contradiction** — if ferry moves but `../CLAUDE.md` still says + `docs/plans`, ferry's own scaffold output would violate the shared rule. Phase 3 + must land or be explicitly scoped. +- **Namespace collision** — a scaffolded third-party repo that already uses the + chosen namespace dir. Neutral naming (§7.1) and the §7.5 decision mitigate. + +**Rollback** — the whole change is `git mv` plus text edits on a branch. No data +migration, no irreversible step, no release cutover. Rollback = revert the PR (or +delete the branch pre-merge). Rehearse by running the full validation (step 14) +on the branch before requesting merge. + +**Effort: M.** Mechanically simple (moves + find/replace), but the spread — +scaffold generator + two golden test files + four scripts/workflows + six doc +files + the cross-repo convention — puts it above S. No logic changes; no new +dependencies. + +--- + +## 7. Open decisions for the maintainer + +1. **Namespace name.** abcd uses `.abcd/` because that *is* its brand. ferry + scaffolds **arbitrary** repos, and a `.ferry/` dir stamped into a random user + project reads as "ferry-owned" (misleading) and collides conceptually with the + runtime `~/.config/ferry/`. Options: `.ferry/` (direct mirror), or a + tool-neutral umbrella like `.dev/` / `.project/` (better for scaffolded + third-party repos). **Recommendation: a neutral umbrella**, precisely because + the scaffold propagates it — but this is the maintainer's brand call. +2. **ADR location.** Move `docs/decisions/` → `/development/decisions/` + (recommended, §4.3) or keep ADRs semi-public in `docs/`. +3. **Working-memory fold.** Fold `.work/` + `.work.local/` under the umbrella for + full parity (recommended, staged as Phase 2) or keep them at the repo root + (already satisfies the goal; less ripple). +4. **Cross-repo CLAUDE.md scope.** Update the shared + `../CLAUDE.md` convention for **ferry only** (scope the rule) or for **all + ABCDevelopment repos**. Flag: whichever way, ferry's scaffold output must match + the shared rule. +5. **Scaffolded-repo policy.** Should repos ferry *stamps* receive the new + namespace, or keep `docs/{decisions,research,plans}`? This is the highest- + leverage decision — it determines whether the scaffold-generator change (step + 8) happens at all, and whether every future ferry-scaffolded repo carries the + new layout. A scaffolded repo may not want ferry's opinionated umbrella. +6. **Roadmap surface.** Adopt `/development/roadmap/` now, or defer until + ferry actually needs a phases/rfcs axis (recommended: defer — keep lean, §4.4). diff --git a/.abcd/development/research/README.md b/.abcd/development/research/README.md new file mode 100644 index 0000000..e7e42bb --- /dev/null +++ b/.abcd/development/research/README.md @@ -0,0 +1,9 @@ +# Research + +Investigations that inform the design without being decisions themselves — +SOTA surveys, prior-art reviews, and throwaway spikes. A note here may seed a +plan or inform an ADR, but the plan and the ADR are where the forward-looking +record and the settled choice live. + +Notes are dated (`YYYY-MM-DD-topic.md`). For the wider record this sits in, see +the [development map](../README.md). diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32104d8..2edcb95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: - name: Plan-doc shipped check # Docs backstop for scripts/release.sh's local plan gate: if a - # docs/plans/*.md plan exists, its Status: line must read + # .abcd/development/plans/*.md plan exists, its Status: line must read # "shipped in "; a missing plan is fine (not every patch has one). # VERSION comes via env (not inlined into the run script) to keep the # tag ref out of the shell expansion (injection-safe, zizmor). diff --git a/.work/DECISIONS.md b/.work/DECISIONS.md index 335eeeb..bc5b958 100644 --- a/.work/DECISIONS.md +++ b/.work/DECISIONS.md @@ -1,7 +1,7 @@ # Decisions — ferry Append-only log of session decisions. One dated line each. Promote a decision to -a `docs/decisions/` ADR when it shapes architecture or is expensive to reverse. +a `.abcd/development/decisions/` ADR when it shapes architecture or is expensive to reverse. - 2026-07-05: GitHub Actions must stay within the free tier — no reliance on paid minutes. Platform coverage moves to committed .githooks/pre-push preflight gates (run everywhere via the global core.hooksPath dispatcher); private-repo CI runs ubuntu-latest by default, switchable with the CI_RUNNER repo variable if development ever moves off macOS. Rejected: nektos/act (Docker dep, Linux-only emulation); making ragd public (not desired). - 2026-07-05: W4 state-file migration backs up via a write-once sibling copy (`statefile.BackupForMigration`, `.pre-v1.bak`), NOT the existing Backuper the plan text names — routing migrations through the Backuper would inject ferry's own bookkeeping into the restore baseline, which restore would then try to revert. Confirmed by independent review. diff --git a/AGENTS.md b/AGENTS.md index a31be87..a3cb6b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,4 +69,4 @@ on the internal packages, and the full eval suite against a real Linux binary. Load `.work/CONTEXT.md` first — it is the curated, load-first summary of ferry's standing facts and invariants. Record session handoff in `.work.local/NEXT.md` (git-ignored, local-only), not in `.work/`. See -[ADR 0002](docs/decisions/0002-work-memory-public-private-split.md) for the split. +[ADR 0002](.abcd/development/decisions/0002-work-memory-public-private-split.md) for the split. diff --git a/CHANGELOG.md b/CHANGELOG.md index 401006f..42e3b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ called out in a **Breaking** section. See ## [Unreleased] +## [0.7.1] - 2026-07-08 + +### Changed + +- **Developer documentation moves out of the user-facing `docs/` tree into a new + `.abcd/development/` namespace.** ferry's plans, research notes, and + architecture decision records (ADRs) now live under + `.abcd/development/{plans,research,decisions}/`, leaving `docs/` for + user-facing Diátaxis documentation only. `ferry agents scaffold` stamps the + same layout: a scaffolded repo gets `docs/` reserved for Diátaxis content and + `.abcd/development/` for its developer records. + ## [0.7.0] - 2026-07-08 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9117d84..f0e70ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,29 +48,32 @@ Every user-facing change keeps the docs and the CHANGELOG in sync. explanation — and no page mixes types. The [documentation map](docs/README.md) states where each page lives. - Docs are present tense: what *is*, never what was superseded or is planned. - History lives in git; intent lives in `docs/plans/`. + History lives in git; intent lives in `.abcd/development/plans/`. - The repository root carries only `README.md`, `AGENTS.md` (with its `CLAUDE.md`/`GEMINI.md` bridges), `CHANGELOG.md`, `CONTRIBUTING.md`, - `SECURITY.md`, and `LICENSE`. Every other Markdown file lives under `docs/`. + `SECURITY.md`, and `LICENSE`. Every other Markdown file lives under `docs/` + (user-facing) or `.abcd/development/` (developer records). ## Decisions and the ADR workflow Architecture decisions are recorded as [MADR](https://adr.github.io/madr/) files -under `docs/decisions/`, named `NNNN-title.md` with a zero-padded sequential -number (see [ADR 0001](docs/decisions/0001-adr-naming-sequential-madr.md)). +under `.abcd/development/decisions/`, named `NNNN-title.md` with a zero-padded +sequential number +(see [ADR 0001](.abcd/development/decisions/0001-adr-naming-sequential-madr.md)). - To add an ADR, take the **next free number** on your branch — there is no reservation registry. - If two branches mint the same `NNNN`, a duplicate-`NNNN` lint fails CI and the second branch to merge renumbers its ADR. Do not resolve the clash by force. - One-line session decisions live in `.work/DECISIONS.md`; promote a decision to - a `docs/decisions/` ADR when it shapes architecture or is expensive to reverse. + a `.abcd/development/decisions/` ADR when it shapes architecture or is + expensive to reverse. ## Working memory Curated standing facts load first from `.work/CONTEXT.md` (committed). Session handoff, scratch output, and logs live under `.work.local/` (git-ignored via `.git/info/exclude`, never committed). See -[ADR 0002](docs/decisions/0002-work-memory-public-private-split.md) for the -rationale. Because `.work/` is committed, its contents follow the repository's +[ADR 0002](.abcd/development/decisions/0002-work-memory-public-private-split.md) +for the rationale. Because `.work/` is committed, its contents follow the repository's privacy rules: no local absolute paths, secrets, or live data. diff --git a/docs/README.md b/docs/README.md index 2ffbd5a..b15d1ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,16 +22,18 @@ directory holds. Every page is exactly one Diátaxis type — tutorial ## Directories +This tree is user-facing only. Developer records — plans, research, and +architecture decisions — live under [`.abcd/development/`](../.abcd/development/), +outside the documentation a ferry user reads. + | Directory | Contents | Naming | |---|---|---| | `tutorials/`, `how-to/`, `reference/`, `explanation/` | The four Diátaxis types; each directory is created on first use | — | -| `decisions/` | Architecture decision records, MADR format | `NNNN-title.md` | -| `research/` | Dated research notes | `YYYY-MM-DD-topic.md` | -| `plans/` | Dated implementation plans | `YYYY-MM-DD-topic.md` | | `assets/` | Images and other static assets | — | ## Root-markdown allowlist The repository root carries only `README.md`, `AGENTS.md` (with its `CLAUDE.md`/`GEMINI.md` symlink bridges), `CHANGELOG.md`, `CONTRIBUTING.md`, -`SECURITY.md`, and `LICENSE`. Every other Markdown file lives under `docs/`. +`SECURITY.md`, and `LICENSE`. Every other Markdown file lives under `docs/` +(user-facing) or `.abcd/development/` (developer records). diff --git a/docs/how-to/cutting-a-release.md b/docs/how-to/cutting-a-release.md index ec772c3..0bde874 100644 --- a/docs/how-to/cutting-a-release.md +++ b/docs/how-to/cutting-a-release.md @@ -33,7 +33,7 @@ scripts/release.sh vX.Y.Z # add --dry-run to rehearse without taggin creates anything: it checks the preconditions (on `main`, clean tree, `main` up to date with `origin/main`), asserts the `## [X.Y.Z]` CHANGELOG section is promoted out of `[Unreleased]`, runs `docs-currency-lint`, requires any matching plan under -`docs/plans/` to be marked `shipped in vX.Y.Z` (via +`.abcd/development/plans/` to be marked `shipped in vX.Y.Z` (via [`scripts/check-plan-shipped.sh`](../../scripts/check-plan-shipped.sh)), and rehearses the build, version stamp, checksum manifest, and prune plan. Only then does it prompt (unless `--yes`) and run the single irreversible act: diff --git a/docs/how-to/scaffold-a-repo.md b/docs/how-to/scaffold-a-repo.md index 2b0d3d8..f92f1ca 100644 --- a/docs/how-to/scaffold-a-repo.md +++ b/docs/how-to/scaffold-a-repo.md @@ -31,8 +31,8 @@ Default (tracked) mode, for a repo you own: |---|---| | `AGENTS.md` | Router stamped from `agents/templates/AGENTS.md`, with `{{PROJECT}}` and `{{DATE}}` substituted | | `CLAUDE.md`, `GEMINI.md` | Relative symlinks to `AGENTS.md` **inside the project repo** (project-tracked content — ferry does not deploy these to `$HOME`) | -| `docs/README.md` | The documentation map, stamped from `agents/templates/docs-README.md`: the four Diátaxis directories, decisions (MADR, `NNNN-title.md`), dated research and plans, and the root-markdown allowlist | -| `docs/decisions/`, `docs/research/`, `docs/plans/` | Dated-record directories, created up front (the Diátaxis content directories are created on first use) | +| `docs/README.md` | The user-facing documentation map, stamped from `agents/templates/docs-README.md`: the four Diátaxis directories and the root-markdown allowlist | +| `.abcd/development/plans/`, `.abcd/development/research/`, `.abcd/development/decisions/` | Developer-record directories, created up front — dated plans and research (`YYYY-MM-DD-topic.md`) and ADRs (MADR, `NNNN-title.md`); `docs/` stays user-facing Diátaxis, whose content directories are created on first use | | `.work/DECISIONS.md`, `.work/CONTEXT.md` | Committed decision log and curated standing facts (the load-first summary) | | `.work.local/NEXT.md`, `.work.local/scratch/`, `.work.local/logs/` | Local-only session handoff and runtime artefacts, hidden via git `info/exclude` | | `.pre-commit-config.yaml` | Copied from the template, only when the repo has none | diff --git a/internal/agents/scaffold.go b/internal/agents/scaffold.go index 3168d75..6baec5d 100644 --- a/internal/agents/scaffold.go +++ b/internal/agents/scaffold.go @@ -206,10 +206,12 @@ var scaffoldLayout = []scaffoldFile{ {template: "docs-README.md", mode: modeTracked, trackedDest: "docs/README.md"}, } -// scaffoldDocsDirs are the docs/ subdirectories tracked mode creates up front -// (they hold dated records). The Diátaxis content dirs (tutorials/how-to/ -// reference/explanation) are created on first use, not here. -var scaffoldDocsDirs = []string{"decisions", "research", "plans"} +// scaffoldDevDocsDirs are the .abcd/development/ subdirectories tracked mode +// creates up front (they hold the durable developer record: dated plans and +// research, sequential ADRs). docs/ is reserved for user-facing Diátaxis +// content, whose dirs (tutorials/how-to/reference/explanation) are created on +// first use, not here. +var scaffoldDevDocsDirs = []string{"plans", "research", "decisions"} // putLayout stamps every scaffoldLayout entry that applies to the requested // scaffold, in table order, creating each dest's parent dir first (put itself @@ -246,19 +248,21 @@ func scaffoldPrivate(repo, name string, put func(templateName, dest string) erro // scaffoldTracked creates the committed layout on top of the shared // .work.local/ runtime dirs: a .work/ holding the committed standing-facts -// memory (DECISIONS.md, CONTEXT.md), the AGENTS.md router, the docs/ -// hierarchy with its map (docs/README.md), the in-repo CLAUDE.md/GEMINI.md -// symlinks, and the pre-commit config when absent. The volatile handoff note +// memory (DECISIONS.md, CONTEXT.md), the AGENTS.md router, the user-facing +// docs/ map (docs/README.md), the .abcd/development/ developer-record dirs, +// the in-repo CLAUDE.md/GEMINI.md symlinks, and the pre-commit config when +// absent. The volatile handoff note // NEXT.md lands in .work.local/ (uncommitted) in both modes. It never touches // .gitignore — runtime artefacts are excluded via .work.local/ + info/exclude, // and .work/ is meant to be committed whole. func scaffoldTracked(opts ScaffoldOptions, repo, name string, put func(templateName, dest string) error, out io.Writer) error { - // Docs hierarchy directories that hold dated records (the map - // docs/README.md is a scaffoldLayout entry). The Diátaxis content - // directories (tutorials/how-to/reference/explanation) are created on - // first use, not up front. - for _, d := range scaffoldDocsDirs { - if err := os.MkdirAll(filepath.Join(repo, "docs", d), 0o755); err != nil { + // Developer-record directories that hold dated plans/research and + // sequential ADRs, under the .abcd/development/ namespace (the user-facing + // map docs/README.md is a scaffoldLayout entry). docs/ stays Diátaxis-only; + // its content directories (tutorials/how-to/reference/explanation) are + // created on first use, not up front. + for _, d := range scaffoldDevDocsDirs { + if err := os.MkdirAll(filepath.Join(repo, ".abcd", "development", d), 0o755); err != nil { return err } } diff --git a/internal/agents/scaffold_golden_test.go b/internal/agents/scaffold_golden_test.go index 216260e..cd06ffd 100644 --- a/internal/agents/scaffold_golden_test.go +++ b/internal/agents/scaffold_golden_test.go @@ -119,6 +119,11 @@ func TestScaffoldGolden_Tracked(t *testing.T) { }) wantPaths := []string{ + ".abcd", + ".abcd/development", + ".abcd/development/decisions", + ".abcd/development/plans", + ".abcd/development/research", ".pre-commit-config.yaml", ".work", ".work.local", @@ -132,9 +137,6 @@ func TestScaffoldGolden_Tracked(t *testing.T) { "GEMINI.md", "docs", "docs/README.md", - "docs/decisions", - "docs/plans", - "docs/research", } wantOut := "excluded: .work.local/ via git info/exclude (local-only, not committed)\n" + "created: /.work.local/NEXT.md\n" + @@ -185,6 +187,11 @@ func TestScaffoldGolden_Attribution(t *testing.T) { }) wantPaths := []string{ + ".abcd", + ".abcd/development", + ".abcd/development/decisions", + ".abcd/development/plans", + ".abcd/development/research", ".githooks", ".githooks/prepare-commit-msg", ".pre-commit-config.yaml", @@ -200,9 +207,6 @@ func TestScaffoldGolden_Attribution(t *testing.T) { "GEMINI.md", "docs", "docs/README.md", - "docs/decisions", - "docs/plans", - "docs/research", } wantOut := "excluded: .work.local/ via git info/exclude (local-only, not committed)\n" + "created: /.work.local/NEXT.md\n" + diff --git a/internal/agents/scaffold_test.go b/internal/agents/scaffold_test.go index ccb7ded..a325df8 100644 --- a/internal/agents/scaffold_test.go +++ b/internal/agents/scaffold_test.go @@ -76,14 +76,14 @@ func TestScaffoldTrackedMode(t *testing.T) { } // Committed memory in .work/, runtime artefacts in .work.local/ — both - // modes share the .work.local layout. The docs hierarchy carries the map - // plus the dated-record directories. The tracked put files come from the - // shared scaffoldLayout table (the single source of truth), so this loop - // tracks the layout automatically. + // modes share the .work.local layout. docs/ carries the user-facing map; + // the dated developer records live under .abcd/development/. The tracked put + // files come from the shared scaffoldLayout table (the single source of + // truth), so this loop tracks the layout automatically. want := layoutDests(false) // tracked dests: .work.local/NEXT.md, .work/DECISIONS.md, .work/CONTEXT.md, AGENTS.md, docs/README.md want = append(want, ".work.local/scratch", ".work.local/logs", ".pre-commit-config.yaml") - for _, d := range scaffoldDocsDirs { - want = append(want, "docs/"+d) + for _, d := range scaffoldDevDocsDirs { + want = append(want, ".abcd/development/"+d) } for _, rel := range want { if _, err := os.Stat(filepath.Join(repo, rel)); err != nil { @@ -290,7 +290,7 @@ func TestScaffoldNeverOverwritesDocsReadme(t *testing.T) { if !strings.Contains(out, "exists:") { t.Errorf("output missing the exists skip: %q", out) } - for _, rel := range []string{"docs/decisions", "docs/research", "docs/plans"} { + for _, rel := range []string{".abcd/development/decisions", ".abcd/development/research", ".abcd/development/plans"} { if _, err := os.Stat(filepath.Join(repo, rel)); err != nil { t.Errorf("%s missing: %v", rel, err) } diff --git a/scripts/check-plan-shipped.sh b/scripts/check-plan-shipped.sh index 36c22d7..1299c2a 100755 --- a/scripts/check-plan-shipped.sh +++ b/scripts/check-plan-shipped.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ferry plan-shipped check — assert the release plan for a version is marked shipped. # -# Each release may carry a design plan under docs/plans/, named +# Each release may carry a design plan under .abcd/development/plans/, named # `YYYY-MM-DD-vX.Y.Z.md`, with a prose frontmatter line of the form # `Date: ... · Status: `. Once the release is out, that Status must read # `shipped in vX.Y.Z`. This check enforces the convention: @@ -47,7 +47,7 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Collect plan docs whose name ends in the version (e.g. 2026-07-05-v0.5.0.md # for v0.5.0). nullglob so a no-match glob expands to nothing, not itself. shopt -s nullglob -plans=( "$ROOT"/docs/plans/*"$VERSION".md ) +plans=( "$ROOT"/.abcd/development/plans/*"$VERSION".md ) shopt -u nullglob if [ "${#plans[@]}" -eq 0 ]; then diff --git a/scripts/consistency-lint.sh b/scripts/consistency-lint.sh index aa73f49..6d62044 100755 --- a/scripts/consistency-lint.sh +++ b/scripts/consistency-lint.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # consistency-lint — deterministic repo-convention checks that gate CI. # -# 1. No two ADRs in docs/decisions/ share an NNNN prefix. This is the +# 1. No two ADRs in .abcd/development/decisions/ share an NNNN prefix. This is the # ADR-race mitigation (ADR 0001): authors take the next free number # optimistically on their branch, and a collision blocks the merge so the # second branch to land renumbers. @@ -10,7 +10,8 @@ # handoff lives in the private .work.local/ layer (ADR 0002). Scope is the # human/agent-facing surface (docs, scripts, hooks, templates); Go sources # are exempt because their tests legitimately assert the OLD path is now -# absent. The historical docs/plans/ record and the changelog are exempt +# absent. The developer-record .abcd/development/plans/ and research/ (which +# describe past and investigative state) and the changelog are exempt # too (they describe past state), as is this script itself. # # Exit non-zero on the first failing invariant set; print every offending path. @@ -18,7 +19,7 @@ set -euo pipefail cd "$(dirname "$0")/.." fail=0 -decisions=docs/decisions +decisions=.abcd/development/decisions if [ -d "$decisions" ]; then # 1. duplicate NNNN prefixes @@ -44,7 +45,7 @@ if [ -d "$decisions" ]; then fi # 3. stale .work/NEXT.md handoff references (must be .work.local/NEXT.md) -hits=$(git ls-files -- ':!docs/plans/' ':!CHANGELOG.md' ':!scripts/consistency-lint.sh' ':!*.go' \ +hits=$(git ls-files -- ':!.abcd/development/plans/' ':!.abcd/development/research/' ':!CHANGELOG.md' ':!scripts/consistency-lint.sh' ':!*.go' \ | xargs -r grep -nF '.work/NEXT.md' 2>/dev/null || true) if [ -n "$hits" ]; then echo "consistency-lint: session handoff is .work.local/NEXT.md, not .work/NEXT.md:" >&2