From ddd38d2c7074e982c9c4b27b597d0eba8016760e Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:30:12 +0530 Subject: [PATCH 1/5] =?UTF-8?q?refactor(memoryindex):=20drop=20Last=20Upda?= =?UTF-8?q?ted=20column=20=E2=80=94=20idempotent=202-column=20indexes=20(u?= =?UTF-8?q?gde)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fab memory-index previously emitted a 3-column index table whose "Last Updated" cell was computed from git commit dates (HEAD/branch-relative). This caused content-identical indexes to differ across concurrent or un-rebased branches (e.g., loom PR #1846), breaking idempotency and creating spurious diffs. Fix: strip the "Last Updated" column entirely. Indexes are now 2-column (File | Description). The dated history stays in log.md, which is the correct home for per-entry timestamps. A 2.7.0 migration re-baselines existing repos. Sub-step 3a-bis in git-pr is retained but narrowed to log.md-only (index refresh is still a no-op when nothing drifted). Includes: Go implementation + tests, spec/memory updates, VERSION bump to 2.7.0, migration guide, and change artifacts for 260625-ugde. --- docs/memory/_shared/index.md | 10 +- docs/memory/distribution/index.md | 14 +- docs/memory/distribution/kit-architecture.md | 4 +- docs/memory/distribution/migrations.md | 11 +- docs/memory/memory-docs/hydrate-generate.md | 2 +- docs/memory/memory-docs/hydrate.md | 10 +- docs/memory/memory-docs/index.md | 16 +- docs/memory/memory-docs/templates.md | 16 +- docs/memory/pipeline/execution-skills.md | 6 +- docs/memory/pipeline/index.md | 18 +- docs/memory/pipeline/schemas.md | 2 +- docs/memory/runtime/index.md | 12 +- docs/specs/fkf.md | 20 +- docs/specs/skills/SPEC-docs-reorg-memory.md | 2 +- docs/specs/skills/SPEC-git-pr.md | 2 +- docs/specs/templates.md | 24 +- .../.history.jsonl | 16 ++ .../.status.yaml | 54 +++++ .../intake.md | 136 +++++++++++ .../plan.md | 214 ++++++++++++++++++ src/go/fab/cmd/fab/memory_index.go | 8 +- .../fab/internal/memoryindex/golden_test.go | 14 +- src/go/fab/internal/memoryindex/indexparse.go | 8 +- src/go/fab/internal/memoryindex/loss.go | 8 +- src/go/fab/internal/memoryindex/loss_test.go | 65 +++--- .../fab/internal/memoryindex/memoryindex.go | 139 ++++-------- .../internal/memoryindex/memoryindex_test.go | 151 +++++------- src/kit/VERSION | 2 +- src/kit/migrations/2.6.6-to-2.7.0.md | 120 ++++++++++ src/kit/reference/fkf.md | 20 +- src/kit/skills/_cli-fab.md | 22 +- src/kit/skills/docs-hydrate-memory.md | 8 +- src/kit/skills/fab-continue.md | 2 +- src/kit/skills/git-pr.md | 4 +- 34 files changed, 812 insertions(+), 348 deletions(-) create mode 100644 fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl create mode 100644 fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml create mode 100644 fab/changes/260625-ugde-memory-index-drop-date-column/intake.md create mode 100644 fab/changes/260625-ugde-memory-index-drop-date-column/plan.md create mode 100644 src/kit/migrations/2.6.6-to-2.7.0.md diff --git a/docs/memory/_shared/index.md b/docs/memory/_shared/index.md index 3248e269..9a629e09 100644 --- a/docs/memory/_shared/index.md +++ b/docs/memory/_shared/index.md @@ -3,9 +3,9 @@ description: "Cross-cutting concerns spanning all domains — config.yaml/consti --- # Shared Documentation -> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`. +> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter. -| File | Description | Last Updated | -|------|-------------|-------------| -| [configuration](configuration.md) | `config.yaml` schema (incl. `fab_version`, `review_tools`, `true_impact_exclude`, `test_paths`, `stage_hooks`, `agent.tiers` per-stage-model override — sole surface, per-field merge over fab-kit defaults, NO validation/provider-neutral, fixed non-overridable stage→tier mapping, l3ja; `stage_directives` + `model_tiers` removed in c5tr via migration 2.1.6-to-2.2.0), single fab-module parser `internal/config` + nil-safe accessors incl. the coupled-Unmarshal fallback caveat (ye8r), companion files (`context.md`, `code-quality.md`, `code-review.md` incl. `## Parsimony Pass` toggle and the wired `## Rework Budget` Max-cycles knob), `constitution.md` governance, 5 Cs of Quality, lifecycle management | 2026-06-25 | -| [context-loading](context-loading.md) | Smart context loading convention — descriptive 7-file always-load layer (skill file wins; exception set rule-derived from skill files, never enumerated in the preamble — d9rs), opt-in skill helpers (7-value allowlist incl. `_srad`/`_pipeline`/`_intake`) + stage-conditional loading, standard subagent context (orchestrators incl. `/fab-proceed`), per-stage model resolution at the dispatch seam (`fab resolve-agent `, provider-neutral, Claude-Code adapter named, review-resolves-once — l3ja; applies on every post-intake stage since the single-dispatch collapse, advisory only for a genuinely no-dispatch run — fgxx; the two halves dispatch through two seams — model via the Agent `model` param (short-alias enum opus/sonnet/haiku/fable, resolved with `fab resolve-agent --alias` so the alias is emitted directly — the deterministic adapter that superseded m3d4's prompt-side id→alias hand-map; yky7), effort via an imperative subagent-prompt instruction (no Agent effort param; omit when empty), plus a compliance-visibility expectation that each site surface the resolved `model=/effort=` — m3d4; residual = a per-sub-agent effort param on the Agent tool, a harness ask), selective domain loading, SRAD protocol pointer, scoped Next Steps Convention, generic fab-command failure rule (unconditional non-zero exit → STOP; `fab log command` exits 0 by contract) | 2026-06-19 | +| File | Description | +|------|-------------| +| [configuration](configuration.md) | `config.yaml` schema (incl. `fab_version`, `review_tools`, `true_impact_exclude`, `test_paths`, `stage_hooks`, `agent.tiers` per-stage-model override — sole surface, per-field merge over fab-kit defaults, NO validation/provider-neutral, fixed non-overridable stage→tier mapping, l3ja; `stage_directives` + `model_tiers` removed in c5tr via migration 2.1.6-to-2.2.0), single fab-module parser `internal/config` + nil-safe accessors incl. the coupled-Unmarshal fallback caveat (ye8r), companion files (`context.md`, `code-quality.md`, `code-review.md` incl. `## Parsimony Pass` toggle and the wired `## Rework Budget` Max-cycles knob), `constitution.md` governance, 5 Cs of Quality, lifecycle management | +| [context-loading](context-loading.md) | Smart context loading convention — descriptive 7-file always-load layer (skill file wins; exception set rule-derived from skill files, never enumerated in the preamble — d9rs), opt-in skill helpers (7-value allowlist incl. `_srad`/`_pipeline`/`_intake`) + stage-conditional loading, standard subagent context (orchestrators incl. `/fab-proceed`), per-stage model resolution at the dispatch seam (`fab resolve-agent `, provider-neutral, Claude-Code adapter named, review-resolves-once — l3ja; applies on every post-intake stage since the single-dispatch collapse, advisory only for a genuinely no-dispatch run — fgxx; the two halves dispatch through two seams — model via the Agent `model` param (short-alias enum opus/sonnet/haiku/fable, resolved with `fab resolve-agent --alias` so the alias is emitted directly — the deterministic adapter that superseded m3d4's prompt-side id→alias hand-map; yky7), effort via an imperative subagent-prompt instruction (no Agent effort param; omit when empty), plus a compliance-visibility expectation that each site surface the resolved `model=/effort=` — m3d4; residual = a per-sub-agent effort param on the Agent tool, a harness ask), selective domain loading, SRAD protocol pointer, scoped Next Steps Convention, generic fab-command failure rule (unconditional non-zero exit → STOP; `fab log command` exits 0 by contract) | diff --git a/docs/memory/distribution/index.md b/docs/memory/distribution/index.md index 816867a3..33cad446 100644 --- a/docs/memory/distribution/index.md +++ b/docs/memory/distribution/index.md @@ -3,11 +3,11 @@ description: "How the kit is packaged, shipped & configured — .kit/ structure, --- # Distribution Documentation -> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`. +> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter. -| File | Description | Last Updated | -|------|-------------|-------------| -| [distribution](distribution.md) | How `src/kit/` is distributed — Homebrew formula (2 binaries direct + 2 via `depends_on`), `fab` router (always-route policy), `fab-kit` lifecycle, `fab init` bootstrap, `fab upgrade-repo` (offline-first default = installed binary's `systemVersion`; `--latest` opts into the GitHub-API newest-release path; explicit arg wins; `dev`/unstamped → network fallback — 1hmj), release workflow (3 binaries, 12 cross-compiled, `SHA256SUMS` for kit-* archives, `just test` gate before tag/build + `go-version-file` single-sourcing — tb6f), hardened auto-download (bounded HTTP timeouts, version-keyed flock + atomic rename, digest verification) + fail-loud lifecycle exit contracts (`init`/`update`/`upgrade-repo`/`sync`), `wt shell-setup` wrapper; the `shll.ai/fab-kit` public docs site — README-slice pull (`ReadmeSlice.astro`) + producer-side README-conformance obligation (tail boundary at `## Development`, diagram SVGs by absolute raw URL, absolute external slice links — except README→`docs/site/` links kept repo-relative for the site rewrite) + `docs/` audience-axis layout (pull surface is exactly `README.md` + `docs/site/**`; `docs/site/**` pulled + rendered one page per file at `/tools/fab-kit/`, §9 ACTIVE) | 2026-06-16 | -| [kit-architecture](kit-architecture.md) | `src/kit/` structure (binary-free; `spec.md` template removed in j6cs; `schemas/` removed in c5tr — scaffold no longer seeds `stage_directives`), three-binary architecture (fab router + fab-kit + fab-go), router always-route policy + shared `LifecycleCommands` allowlist table with contract/collision drift tests (ye8r), `fab-kit sync` (post-state version guard, fail-loud deployment writes, version threading from init/upgrade, stamp-after-success upgrade), agent integration, versioning, monorepos, underscore file ecosystem, `fab pane` command group, `fab shell-init`, hidden `fab help-dump`, shared `internal/lines` + `internal/atomicfile` helpers (hv7t), widened `internal/config` single config.yaml parser (ye8r); tb6f: Go 1.26 + cobra v1.10 toolchain, fab-kit `sync.go` split (semver/prereqs/scaffold/skills + orchestrator), golden byte-stability suite as the standing yaml parity arbiter, yaml.v3-stays-pinned decision, `src/benchmark/` tombstoned; frlo: new `reference/` shipped content dir (read via `$(fab kit-path)/reference/...`, holds the `fkf.md` normative extract) + the whole-`src/kit/`-copied-verbatim packaging invariant (new kit content ships with no Go/packaging change); 2fm8: `templates/memory.md` added — canonical FKF memory-file template, the third artifact template, read on demand via `$(fab kit-path)/templates/memory.md` (ships verbatim, no Go/packaging/migration change) | 2026-06-25 | -| [migrations](migrations.md) | Migration system — dual-version model, migration file format, binary-owned discovery (`fab migrations-status [--json]` / `DiscoverMigrations`), `/fab-setup migrations` subcommand (delegates discovery, applies LLM-driven), brew-install migration, `1.8.0-to-1.9.0` migration (tasks-stage collapse + plan.md), `1.9.1-to-1.9.2` migration (`true_impact_exclude` config field), `1.9.7-to-1.10.0` migration (spec-stage collapse, four-state spec.md→plan.md case table), `2.1.6-to-2.2.0` migration (drops dead `stage_directives` + defensive `model_tiers`; preserves `stage_hooks`), `2.2.0-to-2.3.0` migration (fully-commented `agent.tiers` reference block, comment-sentinel idempotency), `2.5.5-to-2.6.0` migration (freeze-on-write `log.md` re-baseline — `fab memory-index --rebuild` + commit, `--rebuild` binary pre-check, no `fab/`/`.status.yaml` change), version drift detection (`upgrade-repo` mechanical detection + silent self-stamp + TTY-gated styled reminder), `fab/.kit-migration-version` creation | 2026-06-16 | -| [setup](setup.md) | `/fab-setup` skill — structural bootstrap (sync-first order since szxd: doctor → config → constitution → `fab sync`), subcommand architecture (config, constitution, migrations — version handling delegated to a single `fab migrations-status --json` run), delegation pattern with `fab-kit sync`; stage_directives editor removed, semver one-liner restored at the three-way branch, fab_version fresh-create fallback, Next Steps re-aligned to the State Table (c5tr); scaffold fragment merge fails loudly (jznd) and its `.gitignore` dedup is gitignore-aware — variant coverage + negation hard-stop, `.envrc` stays literal (mqiq) | 2026-06-25 | +| File | Description | +|------|-------------| +| [distribution](distribution.md) | How `src/kit/` is distributed — Homebrew formula (2 binaries direct + 2 via `depends_on`), `fab` router (always-route policy), `fab-kit` lifecycle, `fab init` bootstrap, `fab upgrade-repo` (offline-first default = installed binary's `systemVersion`; `--latest` opts into the GitHub-API newest-release path; explicit arg wins; `dev`/unstamped → network fallback — 1hmj), release workflow (3 binaries, 12 cross-compiled, `SHA256SUMS` for kit-* archives, `just test` gate before tag/build + `go-version-file` single-sourcing — tb6f), hardened auto-download (bounded HTTP timeouts, version-keyed flock + atomic rename, digest verification) + fail-loud lifecycle exit contracts (`init`/`update`/`upgrade-repo`/`sync`), `wt shell-setup` wrapper; the `shll.ai/fab-kit` public docs site — README-slice pull (`ReadmeSlice.astro`) + producer-side README-conformance obligation (tail boundary at `## Development`, diagram SVGs by absolute raw URL, absolute external slice links — except README→`docs/site/` links kept repo-relative for the site rewrite) + `docs/` audience-axis layout (pull surface is exactly `README.md` + `docs/site/**`; `docs/site/**` pulled + rendered one page per file at `/tools/fab-kit/`, §9 ACTIVE) | +| [kit-architecture](kit-architecture.md) | `src/kit/` structure (binary-free; `spec.md` template removed in j6cs; `schemas/` removed in c5tr — scaffold no longer seeds `stage_directives`), three-binary architecture (fab router + fab-kit + fab-go), router always-route policy + shared `LifecycleCommands` allowlist table with contract/collision drift tests (ye8r), `fab-kit sync` (post-state version guard, fail-loud deployment writes, version threading from init/upgrade, stamp-after-success upgrade), agent integration, versioning, monorepos, underscore file ecosystem, `fab pane` command group, `fab shell-init`, hidden `fab help-dump`, shared `internal/lines` + `internal/atomicfile` helpers (hv7t), widened `internal/config` single config.yaml parser (ye8r); tb6f: Go 1.26 + cobra v1.10 toolchain, fab-kit `sync.go` split (semver/prereqs/scaffold/skills + orchestrator), golden byte-stability suite as the standing yaml parity arbiter, yaml.v3-stays-pinned decision, `src/benchmark/` tombstoned; frlo: new `reference/` shipped content dir (read via `$(fab kit-path)/reference/...`, holds the `fkf.md` normative extract) + the whole-`src/kit/`-copied-verbatim packaging invariant (new kit content ships with no Go/packaging change); 2fm8: `templates/memory.md` added — canonical FKF memory-file template, the third artifact template, read on demand via `$(fab kit-path)/templates/memory.md` (ships verbatim, no Go/packaging/migration change) | +| [migrations](migrations.md) | Migration system — dual-version model, migration file format, binary-owned discovery (`fab migrations-status [--json]` / `DiscoverMigrations`), `/fab-setup migrations` subcommand (delegates discovery, applies LLM-driven), brew-install migration, `1.8.0-to-1.9.0` migration (tasks-stage collapse + plan.md), `1.9.1-to-1.9.2` migration (`true_impact_exclude` config field), `1.9.7-to-1.10.0` migration (spec-stage collapse, four-state spec.md→plan.md case table), `2.1.6-to-2.2.0` migration (drops dead `stage_directives` + defensive `model_tiers`; preserves `stage_hooks`), `2.2.0-to-2.3.0` migration (fully-commented `agent.tiers` reference block, comment-sentinel idempotency), `2.5.5-to-2.6.0` migration (freeze-on-write `log.md` re-baseline — `fab memory-index --rebuild` + commit, `--rebuild` binary pre-check, no `fab/`/`.status.yaml` change), `2.6.6-to-2.7.0` migration (drop the index `Last Updated` column → two-column index re-baseline — `fab memory-index` + commit, rendered-output binary pre-check, no `fab/`/`.status.yaml` change, VERSION bump to `2.7.0`), version drift detection (`upgrade-repo` mechanical detection + silent self-stamp + TTY-gated styled reminder), `fab/.kit-migration-version` creation | +| [setup](setup.md) | `/fab-setup` skill — structural bootstrap (sync-first order since szxd: doctor → config → constitution → `fab sync`), subcommand architecture (config, constitution, migrations — version handling delegated to a single `fab migrations-status --json` run), delegation pattern with `fab-kit sync`; stage_directives editor removed, semver one-liner restored at the three-way branch, fab_version fresh-create fallback, Next Steps re-aligned to the State Table (c5tr); scaffold fragment merge fails loudly (jznd) and its `.gitignore` dedup is gitignore-aware — variant coverage + negation hard-stop, `.envrc` stays literal (mqiq) | diff --git a/docs/memory/distribution/kit-architecture.md b/docs/memory/distribution/kit-architecture.md index 82f9bbf9..69083327 100644 --- a/docs/memory/distribution/kit-architecture.md +++ b/docs/memory/distribution/kit-architecture.md @@ -321,14 +321,14 @@ The workflow engine backend for all fab CLI operations. Source: `src/go/fab/`. - `fab resolve --pane` — output the tmux pane ID (e.g., `%5`) for the pane running the resolved change; composable with `tmux send-keys -t "$(fab resolve --pane)" "" Enter` - `fab idea add|list|show|done|reopen|edit|rm` — backlog idea management (CRUD for `fab/backlog.md`) - `fab fab-help` — dynamic skill discovery and help overview (scans `.kit/skills/` frontmatter, groups by category) -- `fab memory-index [--check [--json]]` — deterministically (re)generate the `docs/memory/` index files so agents never hand-edit them. Regenerates the root `docs/memory/index.md` (**domains-only** — `| Domain | Description |`; the legacy inlined per-file column is dropped) and every `docs/memory/{domain}/index.md` (file rows — `| File | Description | Last Updated |`) from folder contents + each file's `description:` frontmatter + `git log -1 --date=short` dates. **Recurses one level into sub-domains** (sx7a): a `{domain}/{sub-domain}/` directory holding ≥1 non-index `.md` gets its own generated `{domain}/{sub-domain}/index.md` (same file-row contract), and the parent domain index gains a `## Sub-Domains` table (`| Sub-Domain | Description |` linking to `{sub-domain}/index.md`) emitted **only when sub-domains exist** — so sub-domain-free domain indexes render byte-identically to the pre-`sx7a` output. Byte-stable / idempotent (second run = no diff), so the indexes stop drifting and stop generating per-row merge conflicts. Emits **non-fatal stderr shape warnings** across the recursive tree when a folder (domain or sub-domain) exceeds the soft width bound (~12 topic files) or depth 3 (reserved domains `_shared/`/`_unsorted/` are width-exempt; the width exemption is domain-tier only — an over-wide sub-domain still warns) — advisory only, never affecting the byte-stable output. **`--check` is tiered (glwc):** it writes nothing and classifies the rendered-vs-existing drift by **severity** encoded in the exit code — **0** clean, **1** benign drift (regen changes content but destroys nothing — an improved `description:`, a refreshed `Last Updated`; the former "out of date" condition, so existing "non-zero = stale" CI/preflight consumers keep working), **2** destructive loss (regen would wipe curated/historical content). Tier 2 has three categories (the mechanical form of `/docs-reorg-memory`'s prose signals): (1) a curated **description** that would regenerate to `—` because the file lacks `description:` frontmatter; (2) a **tombstone** row whose `docs/memory/`-relative link target is absent on disk (external/absolute links excluded — no false positives); (3) a custom structural **grouping** heading in the root `index.md` beyond the domains-only table. On tier 2 (non-`--json`) it enumerates each loss to stderr by category and ends with the pointer `→ run /docs-reorg-memory to remediate (it relocates removal-history rows to _shared/removed-domains.md and backfills description: frontmatter via /docs-hydrate-memory) before regenerating.` (`/docs-reorg-memory` is the orchestrator for all three categories — it relocates tombstone rows itself and dispatches `/docs-hydrate-memory` backfill mode for descriptions; backfill alone does not relocate tombstones). Loss is a strict subset of drift (one render pass serves both); a **born-compatible fab-kit tree is provably never tier 2** (frontmatter present, no off-disk rows, domains-only root). The optional **`--json`** flag (with `--check`) emits the loss report as a single snake_case JSON object on stdout — `{"tier": 0|1|2, "drift": bool, "losses": [{"category": "description"|"tombstone"|"grouping", "path": "", "detail": "..."}]}` (mirrors the `fab pane`/`migrations-status` `--json` convention) — suppressing the human text; the exit code is unchanged. Callers pick a threshold: CI/pre-commit fails on exit ≥1; the hydrate/reorg refuse-before-regen guards fail only on exit ==2. The classifier + existing-index-row parser are pure functions in `internal/memoryindex` (unit-tested like `RenderRoot`/`Gather`); the cmd reuses the existing rendered-vs-existing byte-compare. Source: `cmd/fab/memory_index.go` + `internal/memoryindex/`. Consumed by the hydrate skills (`/docs-hydrate-memory`, `/fab-continue` hydrate — both with a refuse-before-regen guard keyed on exit 2) and `/docs-reorg-memory` (compatibility detection via `--check --json`) +- `fab memory-index [--check [--json]]` — deterministically (re)generate the `docs/memory/` index files so agents never hand-edit them. Regenerates the root `docs/memory/index.md` (**domains-only** — `| Domain | Description |`; the legacy inlined per-file column is dropped) and every `docs/memory/{domain}/index.md` (file rows — `| File | Description |`) from folder contents + each file's `description:` frontmatter — **content-only, with no dates** (the `Last Updated` column was dropped in ugde, since a `git log` projection is HEAD/branch-relative and so not idempotent; the batched `git log` pass now serves `log.md` only). **Recurses one level into sub-domains** (sx7a): a `{domain}/{sub-domain}/` directory holding ≥1 non-index `.md` gets its own generated `{domain}/{sub-domain}/index.md` (same file-row contract), and the parent domain index gains a `## Sub-Domains` table (`| Sub-Domain | Description |` linking to `{sub-domain}/index.md`) emitted **only when sub-domains exist** — so sub-domain-free domain indexes render byte-identically to the pre-`sx7a` output. Byte-stable / idempotent (second run = no diff), so the indexes stop drifting and stop generating per-row merge conflicts. Emits **non-fatal stderr shape warnings** across the recursive tree when a folder (domain or sub-domain) exceeds the soft width bound (~12 topic files) or depth 3 (reserved domains `_shared/`/`_unsorted/` are width-exempt; the width exemption is domain-tier only — an over-wide sub-domain still warns) — advisory only, never affecting the byte-stable output. **`--check` is tiered (glwc):** it writes nothing and classifies the rendered-vs-existing drift by **severity** encoded in the exit code — **0** clean, **1** benign drift (regen changes content but destroys nothing — e.g. an improved `description:`; the former "out of date" condition, so existing "non-zero = stale" CI/preflight consumers keep working), **2** destructive loss (regen would wipe curated/historical content). Tier 2 has three categories (the mechanical form of `/docs-reorg-memory`'s prose signals): (1) a curated **description** that would regenerate to `—` because the file lacks `description:` frontmatter; (2) a **tombstone** row whose `docs/memory/`-relative link target is absent on disk (external/absolute links excluded — no false positives); (3) a custom structural **grouping** heading in the root `index.md` beyond the domains-only table. On tier 2 (non-`--json`) it enumerates each loss to stderr by category and ends with the pointer `→ run /docs-reorg-memory to remediate (it relocates removal-history rows to _shared/removed-domains.md and backfills description: frontmatter via /docs-hydrate-memory) before regenerating.` (`/docs-reorg-memory` is the orchestrator for all three categories — it relocates tombstone rows itself and dispatches `/docs-hydrate-memory` backfill mode for descriptions; backfill alone does not relocate tombstones). Loss is a strict subset of drift (one render pass serves both); a **born-compatible fab-kit tree is provably never tier 2** (frontmatter present, no off-disk rows, domains-only root). The optional **`--json`** flag (with `--check`) emits the loss report as a single snake_case JSON object on stdout — `{"tier": 0|1|2, "drift": bool, "losses": [{"category": "description"|"tombstone"|"grouping", "path": "", "detail": "..."}]}` (mirrors the `fab pane`/`migrations-status` `--json` convention) — suppressing the human text; the exit code is unchanged. Callers pick a threshold: CI/pre-commit fails on exit ≥1; the hydrate/reorg refuse-before-regen guards fail only on exit ==2. The classifier + existing-index-row parser are pure functions in `internal/memoryindex` (unit-tested like `RenderRoot`/`Gather`); the cmd reuses the existing rendered-vs-existing byte-compare. Source: `cmd/fab/memory_index.go` + `internal/memoryindex/`. Consumed by the hydrate skills (`/docs-hydrate-memory`, `/fab-continue` hydrate — both with a refuse-before-regen guard keyed on exit 2) and `/docs-reorg-memory` (compatibility detection via `--check --json`) - `fab operator` — parent command: default behavior launches singleton tmux tab for the operator skill (reads `agent.spawn_command` from config). Subcommands: `tick-start` (start-of-tick state update: increments `tick_count`, writes `last_tick_at` RFC3339 UTC to the **server-keyed XDG state file** — see "Operator State File" above — outputs `tick: N\nnow: HH:MM`) and `time` (pure clock query: outputs `now: HH:MM`; with `--interval ` also outputs `next: HH:MM`) - `fab spawn-command [--repo ]` — print a repo's configured `agent.spawn_command` to stdout via `spawn.Command(configPath)`. With `--repo `, reads `/fab/project/config.yaml` directly (no upward search from ``); without `--repo`, resolves the current repo's config via upward `resolve.FabRoot()` search (same source `fab operator` uses). Falls back to `spawn.DefaultSpawnCommand` (`claude --dangerously-skip-permissions`) when the key is missing/empty or the file is unreadable. Lets a caller (e.g. the operator) fetch a **target** repo's spawn command rather than only its own. Source: `src/go/fab/cmd/fab/spawn_command.go` (`spawnCommandCmd()`, wired into the root command in `main.go`) - `fab batch new|switch|archive` — multi-target batch operations via tmux tabs with Claude Code sessions - `fab shell-init ` — emit the shell-completion script for `bash`, `zsh`, or `fish`. Equivalent to (and delegated to) Cobra's auto-generated `fab completion `; provided as the `tu`-style verb users expect. Source: `src/go/fab/cmd/fab/shellinit.go`. Recommended install: add `eval "$(fab shell-init zsh)"` to `~/.zshrc` (or the bash/fish equivalent). Config-independent — works outside a fab repo - `fab help-dump` — **hidden, CI/build-time-only** (`Hidden: true`, `cobra.NoArgs`). Walks the live cobra command tree of the assembled root command programmatically (via `cmd.Commands()`, not regex-parsing `-h`) and writes the frozen shll.ai "command reference" contract JSON to stdout: `{tool:"fab", version (from `main.version` ldflags), captured_at (RFC3339 UTC), schema_version:1, root:Node}` where `Node={name=cmd.Name(), path=cmd.CommandPath(), short, usage=cmd.UseLine(), text=cmd.UsageString(), commands[]}`. At every level the walk drops `completion`, `help`, and any `Hidden` command (self-excluding `help-dump`), then sorts surviving children by `Name()` for byte-stable output; leaves emit `commands:[]` (never `null`). The encoder uses 2-space indent and `SetEscapeHTML(false)` to preserve raw `-h` bytes. Because it is `Hidden`, it is absent from `fab --help` and from its own dumped tree. Source: `src/go/fab/cmd/fab/helpdump.go` (`helpDumpCmd()`, `dumpDoc`, recursive `buildNode`); consumed by the `Help-dump → shll.ai` release-workflow step (see [distribution.md](/distribution/distribution.md)) -**Architecture**: `internal/spawn` provides shared spawn command resolution (reads `agent.spawn_command` from `config.yaml`, used by `operator`, `spawn-command`, and the `batch new`/`batch switch` subcommands — `batch archive` does not spawn). `spawn.Command(configPath)` accepts an explicit config path, which `fab spawn-command --repo ` exploits to read a target repo's config directly. `internal/frontmatter` provides YAML frontmatter parsing (used by `fab-help` and `memory-index` to read the `description:` field). `internal/memoryindex` powers `fab memory-index`: it follows the `internal/prmeta` Render/Gather split — pure `RenderRoot(RootData) string` (domains-only table) + `RenderDomain(DomainData) string` (file rows) renderers, plus a `Gather(repoRoot) (RootData, []DomainData, []Warning, error)` I/O orchestrator that walks `docs/memory/`, reads each topic file's H1 + `description:` frontmatter (via `internal/frontmatter.Field`), stamps "Last Updated" via `git log` (degrading to `—`), computes per-folder counts/depth, and collects shape `Warning`s (width-exempting `_shared`/`_unsorted`). **Sub-domain recursion** (sx7a): `DomainData` gains a `SubDomains []DomainData` field; `gatherSubDomains` (called from `Gather`) enumerates each domain dir's child directories that hold ≥1 non-index `.md`, builds a `DomainData` per sub-domain via the existing `gatherFiles`, and attaches them lexicographically sorted to the parent. `RenderDomain` appends a `## Sub-Domains` table only when `len(SubDomains) > 0` (sub-domain-free output unchanged), and the same `RenderDomain` renders each sub-domain `index.md` (no bespoke `RenderSubDomain` — the file-row contract is tier-agnostic). `cmd/fab/memory_index.go` flattens domains + their sub-domains into the `indexTarget` list so every sub-domain `index.md` is written/checked. This resolved the PR #377 Copilot finding that `gatherFiles` was depth-2-only (it now reaches depth-3 sub-domain topics). Recursion is one level only, matching the depth-3 bound; deeper nesting is a depth warning, not a generated tier. Repo root is resolved as `filepath.Dir(resolve.FabRoot())` (the prmeta `repoDir` idiom). The curated domain description is round-tripped through the generated domain `index.md`'s own `description:` frontmatter so the root row survives regen. The pure renderers are byte-for-byte unit-testable without git fixtures. `internal/intake` derives the mechanical archive-index description for a change: `Title(changeDir)` reads the `# Intake: {title}` heading from `intake.md` (de-prefixed, internal whitespace collapsed; `""` on any read failure), and `DescriptionFor(fabRoot, folder)` prefers that title, falling back to a humanized slug (folder name minus the `YYMMDD-XXXX-` prefix, hyphens → spaces). `internal/archive` depends on `internal/intake`, not the reverse. `internal/backlog` holds the shared backlog parser (`Item`, `ParsePending`, `ExtractContent`) extracted from `batch_new.go` (formerly `package main`, unimportable) so the batch-new and archive paths share one copy of the `[a-z0-9]{4}` regex (since hv7t `ParsePending` returns `([]Item, error)` — open/read failures surface instead of a silent nil — and `ExtractContent` distinguishes read errors from a genuinely missing ID, `not found in backlog`); it also adds `Path(fabRoot)` and `MarkDone(backlogPath, id)`, which flips a backlog line `- [ ] []` → `- [x] []` in place (never moving it to a `## Done` section) and returns `marked` / `already` (no write) / `not_found` (no match, or backlog.md missing — silent nil-error no-op). `internal/archive` keeps `Archive()` pure (folder move / index / pointer; it auto-derives an empty `--description` from the intake title via `internal/intake` before the move) and adds an `ArchiveWithBacklog()` orchestrator that runs `Archive()`, extracts the 4-char change ID via `resolve.ExtractID` (the change ID *is* the originating backlog ID), and calls `backlog.MarkDone` — recording the result on `ArchiveResult.Backlog`, which `FormatArchiveYAML` emits as a `backlog:` field. Re-archiving an already-present change returns the `ErrAlreadyArchived` sentinel, which both `fab change archive` (exit-0 soft skip) and `fab batch archive` (counted `skipped`) treat as an idempotent no-op via `errors.Is`. `internal/lines` (hv7t) is the shared read-lines helper — `ReadFileLines(path) ([]string, error)` and `Split(content) []string`, splitting on `"\n"` with a per-line trailing-`"\r"` `TrimSuffix` to preserve `bufio.ScanLines`' CRLF behavior. It replaced every unchecked production `bufio.Scanner` site (score's `countGrades` via `Split` — it takes already-read content per mz4q F02, archive's `removeFromIndex`, backlog's `ParsePending`/`ExtractContent`, hooklib's section parsers via `Split`, prmeta's checkbox counters, frontmatter's `Field`/`HasFrontmatter`, memoryindex's `readH1`): reads are all-or-nothing, so bufio's 64KB `MaxScanTokenSize` truncation class is gone, and the only production `bufio.NewScanner` left in the fab module is `internal/proc/proc_linux.go` — the one site that checks `scanner.Err()` and legitimately streams `/proc`. `internal/atomicfile` (hv7t) is the temp+rename write helper serving the archive-index writers — `WriteFile(path, data, perm)`: temp in the destination dir, write, fsync, chmod to `perm`, rename, temp removed on any failure. It mirrors the `statusfile.Save` pattern, but `statusfile.Save` and `runtime.SaveFile` deliberately keep their own inline implementations: mz4q (F03/F04) gave them distinct fsync postures — `.status.yaml` fsyncs as the pipeline's source of truth, while the ephemeral, re-derivable runtime file skips fsync because its write sits on every hook event's latency path — so a single always-fsync helper cannot serve both. `internal/statusfile` is the shared foundation — a `StatusFile` struct parsed once via `Load()`, passed by pointer across all operations, and written atomically via `Save()` (inline temp+fsync+rename, under the cross-process lock from `internal/lockfile`). All other packages (`resolve`, `log`, `status`, `preflight`, `change`, `score`, `archive`, `worktree`) import `statusfile` for YAML access — and since hv7t that single ownership holds everywhere: `batch_archive.go`'s `hydrateStatusRe` regex (the lone outlier `.status.yaml` parser, which matched `hydrate:` at any indentation anywhere) is deleted, and `isArchivable` goes through `statusfile.Load` + `GetProgress("hydrate")`. The `worktree` package provides worktree discovery via `git worktree list --porcelain` and fab state resolution, and also contains the full worktree management library used by the `wt` binary (see below). The `internal/runtime` package (extracted from `cmd/fab/runtime.go`) provides shared runtime file manipulation (`LoadFile`, `SaveFile`, `FilePath`, `ClearIdle`, `SetIdle`) — used by both `fab runtime` CLI subcommands and `fab hook` subcommands. The `internal/hooklib` package provides artifact bookkeeping logic (JSON parsing, path pattern matching, change type inference, task/checklist counting) and hook sync logic (hook-to-event mapping, JSON merging for `.claude/settings.local.json`). The `internal/pane` package provides shared pane resolution logic extracted from `panemap.go`: `ValidatePane(paneID)` (checks pane exists via `tmux list-panes`), `ResolvePaneContext(paneID)` (resolves worktree, change, stage, agent state into a `PaneContext` struct), `GetPanePID(paneID)` (shell PID via `tmux display-message`), and `FindMainWorktreeRoot(cwds)` (main worktree root discovery). Used by all four `fab pane` subcommands (`map`, `capture`, `send`, `process`). The `pane` parent command in `cmd/fab/pane.go` groups four subcommands: `map` (moved from root-level `pane-map`), `capture`, `send`, `process`. The `map` subcommand in `cmd/fab/panemap.go` combines tmux pane discovery, worktree resolution, change state, and runtime state into a single observation command — now delegates pane validation and context resolution to `internal/pane`. Supports `--json` (JSON array output), `--session ` (target specific session), and `--all-sessions` (enumerate all sessions). `discoverPanes(mode, sessionName)` accepts a session targeting mode and extends the tmux format string with `#{session_name}` and `#{window_index}`. Shared pane-matching functions (`discoverPanes`, `matchPanesByFolder`, `resolvePaneChange`) also live in `panemap.go` and are reused by `resolve --pane`. +**Architecture**: `internal/spawn` provides shared spawn command resolution (reads `agent.spawn_command` from `config.yaml`, used by `operator`, `spawn-command`, and the `batch new`/`batch switch` subcommands — `batch archive` does not spawn). `spawn.Command(configPath)` accepts an explicit config path, which `fab spawn-command --repo ` exploits to read a target repo's config directly. `internal/frontmatter` provides YAML frontmatter parsing (used by `fab-help` and `memory-index` to read the `description:` field). `internal/memoryindex` powers `fab memory-index`: it follows the `internal/prmeta` Render/Gather split — pure `RenderRoot(RootData) string` (domains-only table) + `RenderDomain(DomainData) string` (file rows) renderers, plus a `Gather(repoRoot) (RootData, []DomainData, []Warning, error)` I/O orchestrator that walks `docs/memory/`, reads each topic file's H1 + `description:` frontmatter (via `internal/frontmatter.Field`), computes per-folder counts/depth, and collects shape `Warning`s (width-exempting `_shared`/`_unsorted`). The index render is **content-only** since ugde — it no longer consumes git dates: the index-only date plumbing (`FileEntry.LastUpdated`, `gitDates.byPath`, `(*gitDates).lookup`, and the `gitLastUpdated` per-file fallback) was **removed**, while `loadGitDates` / `gitDates.commitsByPath` are **retained** because `log.md` generation (`gatherLogEntries`) still consumes the batched `git log` pass. **Sub-domain recursion** (sx7a): `DomainData` gains a `SubDomains []DomainData` field; `gatherSubDomains` (called from `Gather`) enumerates each domain dir's child directories that hold ≥1 non-index `.md`, builds a `DomainData` per sub-domain via the existing `gatherFiles`, and attaches them lexicographically sorted to the parent. `RenderDomain` appends a `## Sub-Domains` table only when `len(SubDomains) > 0` (sub-domain-free output unchanged), and the same `RenderDomain` renders each sub-domain `index.md` (no bespoke `RenderSubDomain` — the file-row contract is tier-agnostic). `cmd/fab/memory_index.go` flattens domains + their sub-domains into the `indexTarget` list so every sub-domain `index.md` is written/checked. This resolved the PR #377 Copilot finding that `gatherFiles` was depth-2-only (it now reaches depth-3 sub-domain topics). Recursion is one level only, matching the depth-3 bound; deeper nesting is a depth warning, not a generated tier. Repo root is resolved as `filepath.Dir(resolve.FabRoot())` (the prmeta `repoDir` idiom). The curated domain description is round-tripped through the generated domain `index.md`'s own `description:` frontmatter so the root row survives regen. The pure renderers are byte-for-byte unit-testable without git fixtures. `internal/intake` derives the mechanical archive-index description for a change: `Title(changeDir)` reads the `# Intake: {title}` heading from `intake.md` (de-prefixed, internal whitespace collapsed; `""` on any read failure), and `DescriptionFor(fabRoot, folder)` prefers that title, falling back to a humanized slug (folder name minus the `YYMMDD-XXXX-` prefix, hyphens → spaces). `internal/archive` depends on `internal/intake`, not the reverse. `internal/backlog` holds the shared backlog parser (`Item`, `ParsePending`, `ExtractContent`) extracted from `batch_new.go` (formerly `package main`, unimportable) so the batch-new and archive paths share one copy of the `[a-z0-9]{4}` regex (since hv7t `ParsePending` returns `([]Item, error)` — open/read failures surface instead of a silent nil — and `ExtractContent` distinguishes read errors from a genuinely missing ID, `not found in backlog`); it also adds `Path(fabRoot)` and `MarkDone(backlogPath, id)`, which flips a backlog line `- [ ] []` → `- [x] []` in place (never moving it to a `## Done` section) and returns `marked` / `already` (no write) / `not_found` (no match, or backlog.md missing — silent nil-error no-op). `internal/archive` keeps `Archive()` pure (folder move / index / pointer; it auto-derives an empty `--description` from the intake title via `internal/intake` before the move) and adds an `ArchiveWithBacklog()` orchestrator that runs `Archive()`, extracts the 4-char change ID via `resolve.ExtractID` (the change ID *is* the originating backlog ID), and calls `backlog.MarkDone` — recording the result on `ArchiveResult.Backlog`, which `FormatArchiveYAML` emits as a `backlog:` field. Re-archiving an already-present change returns the `ErrAlreadyArchived` sentinel, which both `fab change archive` (exit-0 soft skip) and `fab batch archive` (counted `skipped`) treat as an idempotent no-op via `errors.Is`. `internal/lines` (hv7t) is the shared read-lines helper — `ReadFileLines(path) ([]string, error)` and `Split(content) []string`, splitting on `"\n"` with a per-line trailing-`"\r"` `TrimSuffix` to preserve `bufio.ScanLines`' CRLF behavior. It replaced every unchecked production `bufio.Scanner` site (score's `countGrades` via `Split` — it takes already-read content per mz4q F02, archive's `removeFromIndex`, backlog's `ParsePending`/`ExtractContent`, hooklib's section parsers via `Split`, prmeta's checkbox counters, frontmatter's `Field`/`HasFrontmatter`, memoryindex's `readH1`): reads are all-or-nothing, so bufio's 64KB `MaxScanTokenSize` truncation class is gone, and the only production `bufio.NewScanner` left in the fab module is `internal/proc/proc_linux.go` — the one site that checks `scanner.Err()` and legitimately streams `/proc`. `internal/atomicfile` (hv7t) is the temp+rename write helper serving the archive-index writers — `WriteFile(path, data, perm)`: temp in the destination dir, write, fsync, chmod to `perm`, rename, temp removed on any failure. It mirrors the `statusfile.Save` pattern, but `statusfile.Save` and `runtime.SaveFile` deliberately keep their own inline implementations: mz4q (F03/F04) gave them distinct fsync postures — `.status.yaml` fsyncs as the pipeline's source of truth, while the ephemeral, re-derivable runtime file skips fsync because its write sits on every hook event's latency path — so a single always-fsync helper cannot serve both. `internal/statusfile` is the shared foundation — a `StatusFile` struct parsed once via `Load()`, passed by pointer across all operations, and written atomically via `Save()` (inline temp+fsync+rename, under the cross-process lock from `internal/lockfile`). All other packages (`resolve`, `log`, `status`, `preflight`, `change`, `score`, `archive`, `worktree`) import `statusfile` for YAML access — and since hv7t that single ownership holds everywhere: `batch_archive.go`'s `hydrateStatusRe` regex (the lone outlier `.status.yaml` parser, which matched `hydrate:` at any indentation anywhere) is deleted, and `isArchivable` goes through `statusfile.Load` + `GetProgress("hydrate")`. The `worktree` package provides worktree discovery via `git worktree list --porcelain` and fab state resolution, and also contains the full worktree management library used by the `wt` binary (see below). The `internal/runtime` package (extracted from `cmd/fab/runtime.go`) provides shared runtime file manipulation (`LoadFile`, `SaveFile`, `FilePath`, `ClearIdle`, `SetIdle`) — used by both `fab runtime` CLI subcommands and `fab hook` subcommands. The `internal/hooklib` package provides artifact bookkeeping logic (JSON parsing, path pattern matching, change type inference, task/checklist counting) and hook sync logic (hook-to-event mapping, JSON merging for `.claude/settings.local.json`). The `internal/pane` package provides shared pane resolution logic extracted from `panemap.go`: `ValidatePane(paneID)` (checks pane exists via `tmux list-panes`), `ResolvePaneContext(paneID)` (resolves worktree, change, stage, agent state into a `PaneContext` struct), `GetPanePID(paneID)` (shell PID via `tmux display-message`), and `FindMainWorktreeRoot(cwds)` (main worktree root discovery). Used by all four `fab pane` subcommands (`map`, `capture`, `send`, `process`). The `pane` parent command in `cmd/fab/pane.go` groups four subcommands: `map` (moved from root-level `pane-map`), `capture`, `send`, `process`. The `map` subcommand in `cmd/fab/panemap.go` combines tmux pane discovery, worktree resolution, change state, and runtime state into a single observation command — now delegates pane validation and context resolution to `internal/pane`. Supports `--json` (JSON array output), `--session ` (target specific session), and `--all-sessions` (enumerate all sessions). `discoverPanes(mode, sessionName)` accepts a session targeting mode and extends the tmux format string with `#{session_name}` and `#{window_index}`. Shared pane-matching functions (`discoverPanes`, `matchPanesByFolder`, `resolvePaneChange`) also live in `panemap.go` and are reused by `resolve --pane`. **Parity**: All subcommands produce stdout/stderr output matching the bash versions (modulo timestamps). diff --git a/docs/memory/distribution/migrations.md b/docs/memory/distribution/migrations.md index 0155c3bf..f7f7571d 100644 --- a/docs/memory/distribution/migrations.md +++ b/docs/memory/distribution/migrations.md @@ -1,6 +1,6 @@ --- type: memory -description: "Migration system — dual-version model, migration file format, binary-owned discovery (`fab migrations-status [--json]` / `DiscoverMigrations`), `/fab-setup migrations` subcommand (delegates discovery, applies LLM-driven), brew-install migration, `1.8.0-to-1.9.0` migration (tasks-stage collapse + plan.md), `1.9.1-to-1.9.2` migration (`true_impact_exclude` config field), `1.9.7-to-1.10.0` migration (spec-stage collapse, four-state spec.md→plan.md case table), `2.1.6-to-2.2.0` migration (drops dead `stage_directives` + defensive `model_tiers`; preserves `stage_hooks`), `2.2.0-to-2.3.0` migration (fully-commented `agent.tiers` reference block, comment-sentinel idempotency), `2.5.5-to-2.6.0` migration (freeze-on-write `log.md` re-baseline — `fab memory-index --rebuild` + commit, `--rebuild` binary pre-check, no `fab/`/`.status.yaml` change), version drift detection (`upgrade-repo` mechanical detection + silent self-stamp + TTY-gated styled reminder), `fab/.kit-migration-version` creation" +description: "Migration system — dual-version model, migration file format, binary-owned discovery (`fab migrations-status [--json]` / `DiscoverMigrations`), `/fab-setup migrations` subcommand (delegates discovery, applies LLM-driven), brew-install migration, `1.8.0-to-1.9.0` migration (tasks-stage collapse + plan.md), `1.9.1-to-1.9.2` migration (`true_impact_exclude` config field), `1.9.7-to-1.10.0` migration (spec-stage collapse, four-state spec.md→plan.md case table), `2.1.6-to-2.2.0` migration (drops dead `stage_directives` + defensive `model_tiers`; preserves `stage_hooks`), `2.2.0-to-2.3.0` migration (fully-commented `agent.tiers` reference block, comment-sentinel idempotency), `2.5.5-to-2.6.0` migration (freeze-on-write `log.md` re-baseline — `fab memory-index --rebuild` + commit, `--rebuild` binary pre-check, no `fab/`/`.status.yaml` change), `2.6.6-to-2.7.0` migration (drop the index `Last Updated` column → two-column index re-baseline — `fab memory-index` + commit, rendered-output binary pre-check, no `fab/`/`.status.yaml` change, VERSION bump to `2.7.0`), version drift detection (`upgrade-repo` mechanical detection + silent self-stamp + TTY-gated styled reminder), `fab/.kit-migration-version` creation" --- # Migrations @@ -109,6 +109,15 @@ A migration file for the transition to the system shim model. The migration: - **Idempotent (Constitution III).** Re-running `--rebuild` + commit on an already-clean tree is a no-op diff (nothing to commit), and the `--rebuild` pre-check still passes. After the baseline, `fab memory-index --check` exits **0 or 1, never 2** (a freshly re-projected tree is provably never destructive-loss). - **Version bump.** `src/kit/VERSION` is bumped to `2.6.0` (the migration's target version) — a behavior change to a shipped CLI warrants a minor bump, matching the catalog's `2.4.2-to-2.5.0` / `2.2.0-to-2.3.0` feature-migration convention. +### `2.6.6-to-2.7.0` Re-Baseline Migration (drop the index `Last Updated` column — ugde) + +`src/kit/migrations/2.6.6-to-2.7.0.md` re-baselines every `docs/memory/**/index.md` onto the **two-column** domain-index form. As of 2.7.0 (260625-ugde) `fab memory-index` no longer renders a third `Last Updated` column on domain / sub-domain indexes — the index is a pure function of content (file names + descriptions + structure), with no git dates. The old date cell was a **live `git log` projection**, which is HEAD/branch-relative, so concurrent PRs churned the cells back and forth on merge (the loom PR #1846 "lots of date-only changes" symptom); dropping the column makes the index genuinely branch-independent and idempotent (Constitution III). No capability is lost — dated, change-attributed history already lives in each folder's freeze-on-write `log.md`. Existing projects carry `index.md` files generated under the **old** three-column renderer, so the fix is a **one-time re-baseline**: run `fab memory-index` once with the new binary to rewrite every `index.md` to the two-column form, then commit. That re-baseline commit is the **last** churn the repo sees from the date column. + +- **Rendered-output binary pre-check — the *second* output-probe precedent.** Like `2.5.5-to-2.6.0`, this migration's Pre-check gates on a **binary capability** under the same upgrade ordering — the new binary lands first (`brew upgrade fab-kit`), *then* `/fab-setup migrations` applies (an older binary would re-write the indexes back to three columns). Where `2.5.5-to-2.6.0` probed a **`--help` flag** (`--rebuild` present?), this one probes the **rendered output**: it runs `fab memory-index` in a throwaway temp project and checks the generated `index.md` for a `Last Updated` header. If present (or the probe index is absent), it **aborts with no partial rewrite of the real tree** (`Aborted: this migration needs fab >= 2.7.0 (the two-column memory index). Upgrade the binary first: brew upgrade fab-kit.`). A project with no `docs/memory/` directory skips it entirely. +- **No `fab/` data change.** Like `2.5.5-to-2.6.0`, this migration ships **no `.status.yaml` schema change and no `fab/` data change** — it only regenerates `docs/memory/` `index.md` files (and the append-only `log.md` files, which do not change shape) and commits them. +- **Idempotent (Constitution III).** Re-running `fab memory-index` + commit on an already two-column tree is a no-op diff (nothing to commit), and the two-column pre-check still passes. After the baseline, `fab memory-index --check` exits **0 or 1, never 2** (a re-baselined tree is provably never destructive-loss); the `--check` exit-code contract is unchanged. +- **Version bump.** `src/kit/VERSION` is bumped to `2.7.0` (the migration's target version) — a behavior change to a shipped CLI warrants a minor bump, matching the catalog's `2.5.5-to-2.6.0` / `2.4.2-to-2.5.0` feature-migration convention. + ### Version Drift Detection - **`fab upgrade-repo`**: after sync, runs `DiscoverMigrations` against the target version's cached `migrations/` dir and the current `fab/.kit-migration-version` (mechanical relevance check, not string inequality). Three terminal cases: diff --git a/docs/memory/memory-docs/hydrate-generate.md b/docs/memory/memory-docs/hydrate-generate.md index 8a3c4027..fb29809f 100644 --- a/docs/memory/memory-docs/hydrate-generate.md +++ b/docs/memory/memory-docs/hydrate-generate.md @@ -86,7 +86,7 @@ Before d9rs, placement rules (target path, domain creation, index stubs, shape b Generate mode SHALL reuse the same mechanical index regeneration as ingest mode — `fab memory-index`, never hand-edited rows: 1. Author the `description:` frontmatter on every generated topic file (the generated index reads its row Description from this field). -2. Run `fab memory-index` once after generation. It regenerates the root `docs/memory/index.md` (domains-only — `| Domain | Description |`), every `docs/memory/{domain}/index.md` (file rows — `| File | Description | Last Updated |`), and every sub-domain `index.md` deterministically from folder contents + frontmatter + `git log` dates. +2. Run `fab memory-index` once after generation. It regenerates the root `docs/memory/index.md` (domains-only — `| Domain | Description |`), every `docs/memory/{domain}/index.md` (file rows — `| File | Description |`), and every sub-domain `index.md` deterministically from folder contents + frontmatter (content-only — the index carries no dates since ugde). 3. The command is the single writer; its output is byte-stable, all links are relative, and entries are derived from disk (so a file present on disk is always listed — there is no manual "do not remove entries" rule to forget). ### Idempotent Generation diff --git a/docs/memory/memory-docs/hydrate.md b/docs/memory/memory-docs/hydrate.md index cee2010b..bcfcd55e 100644 --- a/docs/memory/memory-docs/hydrate.md +++ b/docs/memory/memory-docs/hydrate.md @@ -84,7 +84,7 @@ Safe to run repeatedly with the same sources: The skill file's `### Index Ownership` section states the ownership model **once**, and every index-touching skill follows it: -- Index files (`index.md` at the root, domain, and sub-domain tiers) are **generated artifacts** — `fab memory-index` is their single writer. Generated content (file rows, `## Sub-Domains` tables, "Last Updated" cells) is never hand-edited. +- Index files (`index.md` at the root, domain, and sub-domain tiers) are **generated artifacts** — `fab memory-index` is their single writer. Generated content (file rows, `## Sub-Domains` tables) is never hand-edited. - The **one hand-curated field** is the `description:` frontmatter — on topic files and on domain/sub-domain index files alike. - When a new domain or sub-domain is created, its `index.md` **stub** — only the `description:` frontmatter one-liner, nothing else — is created **BEFORE** `fab memory-index` runs; the command fills in the generated body and round-trips the description. @@ -94,9 +94,9 @@ Both modes of this skill follow the model, and d9rs propagated it to the other i Every hydration operation regenerates the navigable indexes **mechanically** via `fab memory-index` — the skill never hand-edits index rows: - **Top-level** (`docs/memory/index.md`): domains-only — `| Domain | Description |`. The legacy inlined per-file "Memory Files" column was dropped (tciy); per-domain descriptions come from each domain `index.md`'s `description:` frontmatter (round-tripped by the generator). -- **Domain-level** (`docs/memory/{domain}/index.md`): file rows — `| File | Description | Last Updated |`. Each row's Description is read from the topic file's `description:` frontmatter; "Last Updated" is git-stamped from ONE batched `git -c core.quotepath=off log --date=short --format=%x00%ad --name-only -- docs/memory` pass (newest-first; the first date seen per path wins, keyed relative to the git top-level — output-equivalent to the old per-file `git log -1 --date=short` defaults, which is retained solely as the fallback when the batched call fails), degrading to `—` for uncommitted files; never hand-stamped (pw3k). +- **Domain-level** (`docs/memory/{domain}/index.md`): file rows — `| File | Description |`. Each row's Description is read from the topic file's `description:` frontmatter; the index carries no dates (content-only, branch-independent — ugde). Recency-at-a-glance lives in the per-folder `log.md`, which consumes the batched `git log` pass (see Per-folder `log.md` below). - **Sub-domain-level** (`docs/memory/{domain}/{sub-domain}/index.md`): same file-row contract as a domain index, generated for every sub-domain directory holding ≥1 non-index `.md` (sx7a; the skill's tier descriptions name all three tiers since d9rs). -- **Per-folder `log.md`** (since bmzo): the same `fab memory-index` call also (re)generates each folder's C-lite change log. The index tiers are a pure function of folder contents + frontmatter + git dates; the `log.md` files regenerate **append-only** under the **freeze-on-write** model (since tayp) — the existing `log.md` is authoritative and write-once, so regeneration reads it back and appends only new `(file-base, change-id)` entries rather than re-projecting live git from scratch (and a new unattributable commit is frozen, not re-projected). This keeps a hydration run from churning unrelated `log.md` entries on a history rewrite. See [templates](/memory-docs/templates.md) § Generated `log.md` and [pipeline/schemas.md](/pipeline/schemas.md) § Freeze-on-Write `log.md` Generation for the full contract. +- **Per-folder `log.md`** (since bmzo): the same `fab memory-index` call also (re)generates each folder's C-lite change log. The index tiers are a pure function of folder contents + frontmatter (content-only, no git dates — ugde); the `log.md` files regenerate **append-only** under the **freeze-on-write** model (since tayp), and `log.md` is now the sole consumer of the batched `git log` pass for its dates — the existing `log.md` is authoritative and write-once, so regeneration reads it back and appends only new `(file-base, change-id)` entries rather than re-projecting live git from scratch (and a new unattributable commit is frozen, not re-projected). This keeps a hydration run from churning unrelated `log.md` entries on a history rewrite. See [templates](/memory-docs/templates.md) § Generated `log.md` and [pipeline/schemas.md](/pipeline/schemas.md) § Freeze-on-Write `log.md` Generation for the full contract. - The command is the single writer of all index levels **and `log.md` files** — both are byte-stable / idempotent, so re-running produces no diff and any post-merge conflict auto-resolves by re-running `fab memory-index`. - Memory writers MUST author the `description:` frontmatter on every new/modified topic file so the regenerated index has content. - Formats follow `docs/specs/templates.md` @@ -117,6 +117,6 @@ Every hydration operation regenerates the navigable indexes **mechanically** via ### Memory Index Maintenance is a Mechanical `fab memory-index` Call **Decision**: The hydrate skill regenerates `docs/memory/` indexes by invoking the deterministic `fab memory-index` Go subcommand, not by hand-editing index rows in skill instructions. -**Why**: Hand-maintained per-row index cells (`description` + `Last Updated`) were the dominant merge-conflict and drift source — they get rewritten on nearly every memory edit. A generated, byte-stable index removes the hand-edit entirely, so two branches can never produce conflicting hand-edits to the same row, and any residual textual conflict auto-resolves by re-running the command. The render is a pure function of folder contents + `description:` frontmatter + git dates, mirroring the established `internal/prmeta` Render/Gather pattern. -**Rejected**: Markdown skill instructions for index updates (the prior approach) — they silently drift (the old root roster listed 18 files when 20+ existed; hand-stamped dates were already wrong). A bespoke bash table-parser was also rejected earlier as brittle; the deterministic Go helper is admitted by the constitution (cf. `prmeta`/`impact`/`score`) and is fully unit-testable. +**Why**: Hand-maintained per-row index cells (`description`, and formerly a `Last Updated` date) were the dominant merge-conflict and drift source — they get rewritten on nearly every memory edit. A generated, byte-stable index removes the hand-edit entirely, so two branches can never produce conflicting hand-edits to the same row, and any residual textual conflict auto-resolves by re-running the command. The render is a pure function of folder contents + `description:` frontmatter (content-only — the `Last Updated` date column was dropped in ugde, since a `git log` projection is HEAD/branch-relative and so not idempotent), mirroring the established `internal/prmeta` Render/Gather pattern. +**Rejected**: Markdown skill instructions for index updates (the prior approach) — they silently drift (the old root roster listed 18 files when 20+ existed; the former hand-stamped dates were already wrong). A bespoke bash table-parser was also rejected earlier as brittle; the deterministic Go helper is admitted by the constitution (cf. `prmeta`/`impact`/`score`) and is fully unit-testable. *Introduced by*: 260207-q7m3-separate-hydrate-smart-context (original inline-instruction design); *Superseded by*: 260607-tciy-memory-tree-shape-rebalance (mechanical `fab memory-index`) diff --git a/docs/memory/memory-docs/index.md b/docs/memory/memory-docs/index.md index 929635ee..b1dfa69a 100644 --- a/docs/memory/memory-docs/index.md +++ b/docs/memory/memory-docs/index.md @@ -3,12 +3,12 @@ description: "Authoring docs/memory & docs/specs — the hydrate skills (ingest --- # Memory Docs Documentation -> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`. +> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter. -| File | Description | Last Updated | -|------|-------------|-------------| -| [hydrate](hydrate.md) | `/docs-hydrate-memory` skill — argument routing, three modes (ingest + generate + backfill), hydration rules, mechanical index regen via `fab memory-index`, and the index-ownership model (`description:` frontmatter = single hand-curated field; stub before index — d9rs). Backfill adds frontmatter to existing files (pre-fab-kit trees), body-preserving + caller-aware regen; dispatched by `docs-reorg-memory` compatibility orchestration (5ewp). Refuse-before-regen guard at every regen site consulting `fab memory-index --check` exit 2 (destructive loss), born-compatible no-op; reorg detection now calls the `--check --json` primitive (glwc). All three modes author FKF frontmatter (`type: memory` + `description:`), stop writing per-file `## Changelog`, and use bundle-relative memory↔memory links; backfill also stamps `type: memory`, staying body-preserving (8fr5) | 2026-06-16 | -| [hydrate-generate](hydrate-generate.md) | `/docs-hydrate-memory` generate mode — codebase scanning, gap detection, interactive scoping, memory file generation + placement rules (target path, domain/sub-domain index stubs, shape bounds — d9rs) | 2026-06-16 | -| [hydrate-specs](hydrate-specs.md) | `/docs-hydrate-specs` skill — structural gap detection between memory and specs, interactive propose-then-apply (incl. the no-target new-spec-file branch and aligned prompt/handler tokens — d9rs) | 2026-06-16 | -| [specs-index](specs-index.md) | `docs/specs/` directory — pre-implementation specs, distinction from memory, bootstrap and context integration, per-skill SPEC mirror coverage + naming policy (`SPEC-{source-filename}.md`; `_cli-fab`/`_cli-external` excluded — uliv); mirrors are reserved paths for `docs-reorg-specs` (d9rs); specs are out of scope for compatibility/frontmatter backfill — no specs-index generator, hand-rewritten index, `docs-reorg-specs` carries an explicit no-symmetry note (5ewp) | 2026-06-16 | -| [templates](templates.md) | Artifact templates (intake, plan), skill frontmatter (incl. `helpers:` + the one-shared-helper-per-phase decomposition `_generation`/`_review`/`_intake`/`_pipeline`, symmetry completed by 3xaj), and memory file format (incl. `description:` frontmatter + generated domains-only index, tciy; FKF `type: memory`/`fkf_version` frontmatter + generated per-folder C-lite `log.md`, bmzo; `log.md` generation is freeze-on-write — append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` escape hatch, superset-PASS `--check`, tayp; doc skills now author `type: memory`, stop writing per-file `## Changelog` (record via `fab status set-summary`), and use bundle-relative links — 8fr5; the memory file format is now a **shipped** template `templates/memory.md` — the third artifact template, read on demand by the doc skills via `$(fab kit-path)/templates/memory.md` — 2fm8). `plan.md` (`## Requirements` + `## Tasks` + `## Acceptance` + optional review-owned `## Deletion Candidates`) absorbs the former `spec.md` (j6cs) and the prior `tasks.md` + `checklist.md` pair; acceptance R# mandate scoped to requirement-derived categories, plan Assumptions = three grades, no Status header lines (uliv). Memory Tree Shape also homes `docs-reorg-memory`'s file-moving rebalancer + pre-fab-kit compatibility orchestration (detect missing frontmatter/tombstones/groupings → relocate tombstones to `_shared/removed-domains.md` → dispatch hydrate backfill → single regen — 5ewp; detection now MECHANICAL via `fab memory-index --check --json` with older-binary prose fallback — glwc; rebalancer now FKF-aware — preserves `type: memory`/`description:` on moves + rewrites bundle-relative links, and `docs-reorg-specs` guards against stamping FKF frontmatter on spec moves — oovf) | 2026-06-16 | +| File | Description | +|------|-------------| +| [hydrate](hydrate.md) | `/docs-hydrate-memory` skill — argument routing, three modes (ingest + generate + backfill), hydration rules, mechanical index regen via `fab memory-index`, and the index-ownership model (`description:` frontmatter = single hand-curated field; stub before index — d9rs). Backfill adds frontmatter to existing files (pre-fab-kit trees), body-preserving + caller-aware regen; dispatched by `docs-reorg-memory` compatibility orchestration (5ewp). Refuse-before-regen guard at every regen site consulting `fab memory-index --check` exit 2 (destructive loss), born-compatible no-op; reorg detection now calls the `--check --json` primitive (glwc). All three modes author FKF frontmatter (`type: memory` + `description:`), stop writing per-file `## Changelog`, and use bundle-relative memory↔memory links; backfill also stamps `type: memory`, staying body-preserving (8fr5) | +| [hydrate-generate](hydrate-generate.md) | `/docs-hydrate-memory` generate mode — codebase scanning, gap detection, interactive scoping, memory file generation + placement rules (target path, domain/sub-domain index stubs, shape bounds — d9rs) | +| [hydrate-specs](hydrate-specs.md) | `/docs-hydrate-specs` skill — structural gap detection between memory and specs, interactive propose-then-apply (incl. the no-target new-spec-file branch and aligned prompt/handler tokens — d9rs) | +| [specs-index](specs-index.md) | `docs/specs/` directory — pre-implementation specs, distinction from memory, bootstrap and context integration, per-skill SPEC mirror coverage + naming policy (`SPEC-{source-filename}.md`; `_cli-fab`/`_cli-external` excluded — uliv); mirrors are reserved paths for `docs-reorg-specs` (d9rs); specs are out of scope for compatibility/frontmatter backfill — no specs-index generator, hand-rewritten index, `docs-reorg-specs` carries an explicit no-symmetry note (5ewp) | +| [templates](templates.md) | Artifact templates (intake, plan), skill frontmatter (incl. `helpers:` + the one-shared-helper-per-phase decomposition `_generation`/`_review`/`_intake`/`_pipeline`, symmetry completed by 3xaj), and memory file format (incl. `description:` frontmatter + generated domains-only index, tciy; FKF `type: memory`/`fkf_version` frontmatter + generated per-folder C-lite `log.md`, bmzo; `log.md` generation is freeze-on-write — append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` escape hatch, superset-PASS `--check`, tayp; doc skills now author `type: memory`, stop writing per-file `## Changelog` (record via `fab status set-summary`), and use bundle-relative links — 8fr5; the memory file format is now a **shipped** template `templates/memory.md` — the third artifact template, read on demand by the doc skills via `$(fab kit-path)/templates/memory.md` — 2fm8). `plan.md` (`## Requirements` + `## Tasks` + `## Acceptance` + optional review-owned `## Deletion Candidates`) absorbs the former `spec.md` (j6cs) and the prior `tasks.md` + `checklist.md` pair; acceptance R# mandate scoped to requirement-derived categories, plan Assumptions = three grades, no Status header lines (uliv). Memory Tree Shape also homes `docs-reorg-memory`'s file-moving rebalancer + pre-fab-kit compatibility orchestration (detect missing frontmatter/tombstones/groupings → relocate tombstones to `_shared/removed-domains.md` → dispatch hydrate backfill → single regen — 5ewp; detection now MECHANICAL via `fab memory-index --check --json` with older-binary prose fallback — glwc; rebalancer now FKF-aware — preserves `type: memory`/`description:` on moves + rewrites bundle-relative links, and `docs-reorg-specs` guards against stamping FKF frontmatter on spec moves — oovf) | diff --git a/docs/memory/memory-docs/templates.md b/docs/memory/memory-docs/templates.md index e5e4e3fa..4ee6293a 100644 --- a/docs/memory/memory-docs/templates.md +++ b/docs/memory/memory-docs/templates.md @@ -134,11 +134,11 @@ Memory files are the source of truth for system behavior and design decisions. A #### Index Hierarchy (generated — never hand-edited) -All index levels (and, since bmzo, the per-folder `log.md` documented below) are **generated artifacts** written by `fab memory-index` (see `_cli-fab.md` → `## fab memory-index`); agents never hand-edit them. The **index** render is a pure function of folder contents + each file's `description:` frontmatter + git dates — sourced from ONE batched `git log --date=short --name-status -- docs/memory` pass (the `--name-only`→`--name-status` widening landed in bmzo so the same pass also feeds `log.md`'s per-commit verb/history; the date projection is unchanged — newest-first, first date per path wins; the per-file `git log -1` spawn is retained solely as fallback when the batched call fails — pw3k) — mirroring the `internal/prmeta` Render/Gather pattern, so the output is byte-stable / idempotent. (The `log.md` render reuses the same batched git pass but is **not** a pure projection of it — since tayp `log.md` generation is freeze-on-write / append-only; see Generated `log.md` below.) +All index levels (and, since bmzo, the per-folder `log.md` documented below) are **generated artifacts** written by `fab memory-index` (see `_cli-fab.md` → `## fab memory-index`); agents never hand-edit them. The **index** render is a pure function of folder contents + each file's `description:` frontmatter — content-only, with no git dates (the `Last Updated` date column was dropped in ugde, since a `git log` projection is HEAD/branch-relative and so not idempotent) — mirroring the `internal/prmeta` Render/Gather pattern, so the output is byte-stable / idempotent. The batched `git log --date=short --name-status -- docs/memory` pass (the `--name-only`→`--name-status` widening landed in bmzo) now feeds **`log.md` only** — its per-commit verb/history; the index no longer consumes it. (The `log.md` render reuses that batched git pass but is **not** a pure projection of it — since tayp `log.md` generation is freeze-on-write / append-only; see Generated `log.md` below.) - **Top-level** (`docs/memory/index.md`): **domains-only** — `| Domain | Description |`. The legacy inlined per-file "Memory Files" / file-list column was dropped (tciy) because it silently drifted; each domain row's Description is read from that domain `index.md`'s `description:` frontmatter (round-tripped by the generator). As of bmzo the root index is also prepended with the FKF version frontmatter block `---\nfkf_version: "0.1"\n---` (FKF §8) — the **only** `index.md` permitted frontmatter beyond the generator's output; no domain/sub-domain index carries it. This is a byte-stable change to the root output: every regen writes the block, and a tree lacking it is benign drift (tier 1) on `--check`, never destructive loss -- **Domain-level** (`docs/memory/{domain}/index.md`): file rows — `| File | Description | Last Updated |`. Description comes from each topic file's `description:` frontmatter; "Last Updated" is git-stamped (degrades to `—` when uncommitted / in a worktree / shallow clone), never hand-stamped. When the domain folder contains sub-domains, the domain index also carries a `## Sub-Domains` table (`| Sub-Domain | Description |`, linking to `{sub-domain}/index.md`) — emitted **only** when sub-domains exist, so a sub-domain-free domain index renders byte-identically to the pre-`sx7a` output -- **Sub-domain-level** (`docs/memory/{domain}/{sub-domain}/index.md`): generated for every `{domain}/{sub-domain}/` directory holding ≥1 non-index `.md`, with the **same** `| File | Description | Last Updated |` file-row contract as a domain index (the file-row render is tier-agnostic — relative `[file](file.md)` links are correct from a sub-domain index too). This is the third addressing tier; the External `{domain}/{sub-domain}/{file}` form addresses files under it (sx7a) +- **Domain-level** (`docs/memory/{domain}/index.md`): file rows — `| File | Description |`. Description comes from each topic file's `description:` frontmatter; the index carries no dates (content-only since ugde — recency-at-a-glance lives in the per-folder `log.md`). When the domain folder contains sub-domains, the domain index also carries a `## Sub-Domains` table (`| Sub-Domain | Description |`, linking to `{sub-domain}/index.md`) — emitted **only** when sub-domains exist, so a sub-domain-free domain index renders byte-identically to the pre-`sx7a` output +- **Sub-domain-level** (`docs/memory/{domain}/{sub-domain}/index.md`): generated for every `{domain}/{sub-domain}/` directory holding ≥1 non-index `.md`, with the **same** `| File | Description |` file-row contract as a domain index (the file-row render is tier-agnostic — relative `[file](file.md)` links are correct from a sub-domain index too). This is the third addressing tier; the External `{domain}/{sub-domain}/{file}` form addresses files under it (sx7a) - All links in `index.md` SHALL be relative (the index-link convention is unchanged by FKF; the bundle-relative `/...` rule of FKF §7 governs memory↔memory cross-links inside topic-file *bodies* and `log.md` entries, not generated index rows) #### Generated `log.md` (FKF C-lite — bmzo) @@ -158,7 +158,7 @@ Generated shape (FKF §6.2): - **Update** [migrations](/distribution/migrations.md) — drops the dead `stage_directives:` block. (260612-c5tr) ``` -- **C-lite join.** Each entry joins two sources, neither hand-edited: **git history** (the *when* / *which file* / *change-id*, from the same batched `git log` pass the index dates use, now `--name-status`) with each change's **`.status.yaml` `summary:`** field (the *what* — set via `fab status set-summary`, written once into the change's own file so it has zero conflict surface). This kills the same-day-changelog-collision that per-file `## Changelog` tables suffered. +- **C-lite join.** Each entry joins two sources, neither hand-edited: **git history** (the *when* / *which file* / *change-id*, from the batched `git log` pass — now `--name-status`; since ugde dropped the index date column, `log.md` is the **sole consumer** of this pass) with each change's **`.status.yaml` `summary:`** field (the *what* — set via `fab status set-summary`, written once into the change's own file so it has zero conflict surface). This kills the same-day-changelog-collision that per-file `## Changelog` tables suffered. - **Format.** `# Log — {Title}` H1 + the `Do not hand-edit` generated comment, then entries **date-grouped, newest date first** (`## YYYY-MM-DD`). Each entry is `- {**Verb** }[base](/{domain}[/{sub}]/base.md) — {summary-or-slug} ({change-id})`: an **optional leading bold verb** (`**Creation**` / `**Deprecation**` / `**Update**`, derived from the commit's git name-status A/D/M·R·C, omitted when ambiguous), a **bundle-relative** link (FKF §7 — beginning with `/`, resolved from `docs/memory/`), the change's `summary` (or the **change slug** when no summary exists — §6.3 graceful fallback), and the `(change-id)` token. One line per change per file. - **change-id join (registry-gated, graceful degradation).** The id is recovered from the commit message and **gated against the change registry** (`fab/changes/*` + `archive/**` give the canonical `(id, folder)` set). The merge-commit branch token (`Merge pull request #N from owner/`) is the only recoverable shape and is effective only on legacy true-merge history; against this repo's squash-merged history (subjects `feat: … (#NNN)`) it recovers ≈0 ids, so most entries **degrade gracefully** — the `(change-id)` is omitted and the descriptive line falls back to the commit subject (still a conflict-free git projection), or to `—` when empty. The format physically exists and self-heals as FKF-era changes land curated summaries on true-attributable commits. - **Empty-folder skip.** A folder with no attributable commits is **skipped** — no empty `log.md` is written (the target set equals "folders with history" — or with a `log.seed.md`). @@ -184,7 +184,7 @@ When `/fab-continue` (hydrate) hydrates into memory files: 1. **New file**: Create from the shipped template read on demand via `$(fab kit-path)/templates/memory.md` (2fm8) — including the FKF frontmatter (both `type: memory` and a curated `description:`). If the domain is new, create the domain folder 2. **Existing file**: Compare spec requirements against current file. Update Requirements section semantically. Minimize edits to unchanged sections. Keep the `description:` frontmatter accurate (and stamp `type: memory` if the pre-existing file lacks it). Memory↔memory cross-links written use the bundle-relative `/...` form (FKF §7) 3. **Design decisions**: Extract durable decisions from spec. Skip tactical details. Add with change name for traceability -4. **Index + log updates**: Run `fab memory-index` — it regenerates the root (domains-only) + every domain + every sub-domain index, and (since bmzo) the per-folder `log.md` files and the root-index `fkf_version` frontmatter. The indexes regenerate deterministically from folder contents + frontmatter + git history; the `log.md` files regenerate **append-only** under the freeze-on-write model (since tayp — the existing log is authoritative, only new `(file-base, change-id)` entries are appended; see Generated `log.md` above), so hydrating a memory file no longer churns unrelated `log.md` entries. Never hand-edit index rows, "Last Updated" cells, or `log.md`; the command is the single writer of all of them +4. **Index + log updates**: Run `fab memory-index` — it regenerates the root (domains-only) + every domain + every sub-domain index, and (since bmzo) the per-folder `log.md` files and the root-index `fkf_version` frontmatter. The indexes regenerate deterministically from folder contents + frontmatter (content-only, no dates since ugde); the `log.md` files regenerate **append-only** under the freeze-on-write model (since tayp — the existing log is authoritative, only new `(file-base, change-id)` entries are appended; see Generated `log.md` above), so hydrating a memory file no longer churns unrelated `log.md` entries. Never hand-edit index rows or `log.md`; the command is the single writer of all of them 5. **What-changed summary (not a changelog row)**: As of **8fr5**, hydrate no longer appends a per-file `## Changelog` row. Instead it records the one-line what-changed **once** via `fab status set-summary {change} ""` — the C-lite `summary:` source field (FKF §6.3) that `fab memory-index` joins with git history to generate the per-folder `log.md`. The summary lives in the change's own `.status.yaml` (zero conflict surface), authored once at hydrate ## Design Decisions @@ -250,10 +250,10 @@ When `/fab-continue` (hydrate) hydrates into memory files: *Introduced by*: 260213-v4rx-simplify-templates ### Generated Memory Index + `description:` Frontmatter (Domains-Only Root) -**Decision**: Memory indexes are generated by `fab memory-index` from a new per-file `description:` frontmatter field + git dates, not hand-maintained. The root index is domains-only (the inlined per-file column is dropped); descriptions live co-located in each file's frontmatter. -**Why**: The hand-maintained per-row index cells (`description` + `Last Updated`) were the dominant merge-conflict and drift source — rewritten on nearly every memory edit, and silently wrong (stale dates, a root roster listing 18 files when 20+ existed). A generated, byte-stable index removes the hand-edit class entirely. The `description:` is a *curated* one-liner that cannot be auto-derived without loss (an H1 + first-Overview-sentence extraction breaks tables — `hydrate.md`'s Overview contains literal `|` pipes — and degrades the always-load routing signal), so it is stored as co-located frontmatter (the Starlight lesson). The root goes domains-only because the root index is near-zero churn and only the *domain* index carries the volatile file list. +**Decision**: Memory indexes are generated by `fab memory-index` from a new per-file `description:` frontmatter field, not hand-maintained — content-only (the index carries no dates; the `Last Updated` column was dropped in ugde). The root index is domains-only (the inlined per-file column is dropped); descriptions live co-located in each file's frontmatter. +**Why**: The hand-maintained per-row index cells (`description`, and formerly a `Last Updated` date) were the dominant merge-conflict and drift source — rewritten on nearly every memory edit, and silently wrong (the former stale dates, a root roster listing 18 files when 20+ existed). A generated, byte-stable index removes the hand-edit class entirely. The `Last Updated` date column was later dropped (ugde) because a `git log` projection is HEAD/branch-relative, so the date half was never idempotent; dated recency now lives in the per-folder `log.md`. The `description:` is a *curated* one-liner that cannot be auto-derived without loss (an H1 + first-Overview-sentence extraction breaks tables — `hydrate.md`'s Overview contains literal `|` pipes — and degrades the always-load routing signal), so it is stored as co-located frontmatter (the Starlight lesson). The root goes domains-only because the root index is near-zero churn and only the *domain* index carries the volatile file list. **Rejected**: Hand-edited index rows (the prior approach — the churn this change exists to kill). Auto-deriving descriptions from H1/Overview (lossy, breaks tables). Keeping the per-file column on the root (re-introduces a wide, drift-prone hand roster). Splitting wide domains *first* to relocate the hot row (Approach A — only moves the conflict and manufactures a one-time link-rewrite bomb; deferred to follow-up `sx7a`). -*Introduced by*: 260607-tciy-memory-tree-shape-rebalance; *Updated by*: 260607-sx7a-reorg-memory-shape-rebalance (the deferred file-moving rebalancer apply path + the sub-domain index tier it generates are now shipped — `fab memory-index` recurses one level and the parent index gains a conditional `## Sub-Domains` table) +*Introduced by*: 260607-tciy-memory-tree-shape-rebalance; *Updated by*: 260607-sx7a-reorg-memory-shape-rebalance (the deferred file-moving rebalancer apply path + the sub-domain index tier it generates are now shipped — `fab memory-index` recurses one level and the parent index gains a conditional `## Sub-Domains` table); 260625-ugde-memory-index-drop-date-column (dropped the `Last Updated` date column — a HEAD/branch-relative `git log` projection that was never idempotent — so the index is now content-only; dated recency lives in `log.md`) ### SRAD-Driven Open Questions (No BLOCKING/DEFERRED Labels) **Decision**: The intake's Open Questions section uses a plain list without explicit priority markers. SRAD handles prioritization at intake (scoring) and at apply-entry requirement generation. diff --git a/docs/memory/pipeline/execution-skills.md b/docs/memory/pipeline/execution-skills.md index 6e32727d..7cf97665 100644 --- a/docs/memory/pipeline/execution-skills.md +++ b/docs/memory/pipeline/execution-skills.md @@ -1,6 +1,6 @@ --- type: memory -description: "Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded, fixing the `index.md` 'Last Updated' date drift that arises because hydrate's regen is pre-commit (o203). Operator coordination moved to runtime/operator.md." +description: "Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded; it is retained for **`log.md` only** (freeze-on-write needs the post-commit projection to capture the change's own entry while its commits are still reachable, pre-squash — the index carries no dates since ugde, so its regen half is a reliable no-op) (o203; rationale narrowed by ugde). Operator coordination moved to runtime/operator.md." --- # Execution Skills @@ -60,7 +60,7 @@ Each prefix step (the `_intake` Create-Intake Procedure, `/fab-switch`, `/git-br **PR shipping**: `/git-pr` drives the `ship` pipeline stage, integrating with statusman for stage tracking (start/finish). All PRs are created as drafts (`gh pr create --draft`) — this is unconditional with no configuration toggle. Developers mark PRs ready for review manually after inspecting the agent-generated implementation. The ship stage has no `failed` state — git-pr fails fast and the user retries. Statusman calls are best-effort (silently ignored on failure). After PR creation, Step 4 executes three sub-steps in order: 4a (record PR URL via `fab status add-pr`), 4b (finish ship stage via `fab status finish` — best-effort), 4c (commit and push both `.status.yaml` and `fab/changes/{change-name}/.history.jsonl` to git). All status mutations (4a, 4b) occur before the commit boundary (4c), ensuring no uncommitted fab state files remain after PR creation completes. As of szxd, `/git-pr` resolves change context **exactly once**, in a unified **Step 0** (accepting the optional explicit `` argument since w7dp) producing four variables — `{has_fab}` (did `fab change resolve` succeed), `{name}`, `{has_intake}`, `{change_type}` — which Steps 0a/0b/1/2/3c/4a–4c consume (Step 1b no longer exists — w7dp removed the nudge); later steps MUST NOT re-run `fab change resolve` (the skill previously re-resolved at up to six step sites while warning against re-resolution). The Step 0b and Step 3c step names are kept intact — `_cli-fab.md` and `prmeta.go` cite them by name. Observable behavior was unchanged by the szxd consolidation itself (the then-extant nudge, Meta gating, add-pr path). -**Post-commit memory-index refresh — sub-step 3a-bis (o203)**: `/git-pr` Step 3 gained a `#### 3a-bis. Refresh Memory Indexes` sub-step positioned **between 3a (Commit) and 3b (Push)** — the only pipeline position where `git log` reports the change's own content commit. `fab memory-index` stamps each `index.md` row's "Last Updated" cell from `git log` (committed dates only); the hydrate-stage regen (Step 5) runs entirely **pre-commit**, so for every file a change touches the regenerated index is born "one regen behind" (it stamps the file's *previous* commit date), and a later `fab memory-index --check` (the glwc refuse-before-regen oracle) flags **benign tier-1 date drift** (exit 1) at review-pr until a post-commit regen catches up. 3a-bis fixes this for FUTURE runs by regenerating *after* 3a's content commit lands: it runs `fab memory-index` (byte-stable; writes only `docs/memory/` index + log files) and, when `docs/memory/` changed (`git diff --quiet -- docs/memory` exits non-zero), stages `git add docs/memory` and makes a **SEPARATE** `docs: refresh memory indexes` follow-up commit — **never `--amend`** (keeps 3a's authored content commit intact and reviewable; squash collapses the pair on merge anyway). It is gated on **BOTH** `{has_fab}` (the Step 0 variable) AND 3a-having-just-committed-this-invocation (the `has_uncommitted` path ran) — skipped entirely otherwise, so it is a **silent no-op** on the "already shipped" / no-change re-run paths and when `/git-pr` runs standalone outside a fab project (`{has_fab}` false → general-purpose standalone use unaffected). The `git diff --quiet -- docs/memory` guard suppresses an empty follow-up commit when nothing drifted (Constitution III idempotency); a regen-or-commit failure → **report + STOP**, leaving the 3a content commit intact (a failed refresh degrades to a benign stale-date index recoverable by re-running `fab memory-index` — never a torn state). There is **no push inside 3a-bis** — 3b ("if has_unpushed or just committed") pushes both the content commit and the index-refresh commit together. The step lives in **ship, not hydrate**, precisely because hydrate is entirely pre-commit (no in-hydrate regen can see the change's own commit); this is the in-skill `{has_fab}`-gated route, chosen over `stage_hooks.ship.post`. The change is **skill-prose + spec-mirror only** — no Go change, no new CLI command, no migration; it adds a new *caller* of the unchanged `fab memory-index`, mirrored into `docs/specs/skills/SPEC-git-pr.md`'s Flow tree. The Key Properties Idempotent? row records that 3a-bis is gated on 3a-having-just-committed (a re-run on the no-commit path skips it) and is byte-stable + diff-guarded even if reached. This belongs in the same `/git-pr`-hardening lineage as g8st / w7dp / glwc below. +**Post-commit memory-index refresh — sub-step 3a-bis (o203; rationale narrowed by ugde)**: `/git-pr` Step 3 gained a `#### 3a-bis. Refresh Memory Indexes` sub-step positioned **between 3a (Commit) and 3b (Push)** — the only pipeline position where `git log` reports the change's own content commit. Its job is **`log.md`**, a freeze-on-write projection of *committed* git history: it must capture this change's own entry while the change's commit is still reachable (pre-squash). The hydrate-stage regen (Step 5) runs entirely **pre-commit**, so it cannot see the change's own commit; 3a-bis closes that gap at the only pipeline position where `git log` already knows the change's commit — immediately after 3a commits, before 3b pushes. The **index** no longer participates: since ugde it carries no dates (a pure function of content), so its regen half is a reliable no-op and `log.md` is the sole reason 3a-bis remains. (Originally, o203 added 3a-bis to fix the index `Last Updated` date drift — the index was born "one regen behind" because hydrate's regen was pre-commit, and `fab memory-index --check` then flagged benign tier-1 date drift at review-pr; dropping the index date column in ugde removed that index-side justification entirely, leaving only `log.md`'s freeze-on-write need.) It runs `fab memory-index` (byte-stable; writes only `docs/memory/` index + log files) and, when `docs/memory/` changed (`git diff --quiet -- docs/memory` exits non-zero), stages `git add docs/memory` and makes a **SEPARATE** `docs: refresh memory indexes` follow-up commit — **never `--amend`** (keeps 3a's authored content commit intact and reviewable; squash collapses the pair on merge anyway). It is gated on **BOTH** `{has_fab}` (the Step 0 variable) AND 3a-having-just-committed-this-invocation (the `has_uncommitted` path ran) — skipped entirely otherwise, so it is a **silent no-op** on the "already shipped" / no-change re-run paths and when `/git-pr` runs standalone outside a fab project (`{has_fab}` false → general-purpose standalone use unaffected). The `git diff --quiet -- docs/memory` guard suppresses an empty follow-up commit when nothing drifted (Constitution III idempotency); a regen-or-commit failure → **report + STOP**, leaving the 3a content commit intact (a failed refresh degrades to a benign stale `log.md` recoverable by re-running `fab memory-index` — never a torn state). There is **no push inside 3a-bis** — 3b ("if has_unpushed or just committed") pushes both the content commit and the index-refresh commit together. The step lives in **ship, not hydrate**, precisely because hydrate is entirely pre-commit (no in-hydrate regen can see the change's own commit); this is the in-skill `{has_fab}`-gated route, chosen over `stage_hooks.ship.post`. The original o203 change was **skill-prose + spec-mirror only** — no Go change, no new CLI command, no migration; it adds a new *caller* of the unchanged `fab memory-index`, mirrored into `docs/specs/skills/SPEC-git-pr.md`'s Flow tree. The Key Properties Idempotent? row records that 3a-bis is gated on 3a-having-just-committed (a re-run on the no-commit path skips it) and is byte-stable + diff-guarded even if reached. This belongs in the same `/git-pr`-hardening lineage as g8st / w7dp / glwc below. **Ship-pipeline git-state hardening (g8st)**: `/git-pr` verifies git state before mutating, instead of assuming an attached, main-defaulted, clean world: @@ -219,7 +219,7 @@ Loads: config, constitution, `specs/index.md`, `plan.md` (incl. its `## Requirem - **Author FKF frontmatter** on every new or modified memory file — both `type: memory` (the constant FKF type, [fkf.md](../../specs/fkf.md) §3.1; stamp it if a pre-existing file lacks it) and a curated, accurate `description:` (§3.2 — the generated index reads each row's Description from it). New domains get a domain folder; the domain `index.md` is generated (not hand-written) - **No `## Changelog` write** (8fr5): memory files no longer carry a per-file `## Changelog` section (FKF §3.3). Instead, record the one-line what-changed **once** via `fab status set-summary {change} ""` — the C-lite `summary:` source field (FKF §6.3) that `fab memory-index` joins with git history to generate the per-folder `log.md`. (Stopping new changelog writes was 8fr5; the bulk strip of fab-kit's own existing per-file tables landed in the **oovf** cutover, FKF change 4/4, which seeded their history into per-folder `log.seed.md`. Hydrate never strips an existing `## Changelog` body — that body-preserving discipline is unchanged.) - **Bundle-relative cross-links** (FKF §7): any memory↔memory link hydrate writes uses the bundle-relative `/...` form (resolved from `docs/memory/`); links out of the bundle (sources, specs, URLs) stay repo-relative/absolute-URL -5. **Regenerate indexes** — run `fab memory-index` (tciy). It deterministically rewrites the root `docs/memory/index.md` (domains-only), every domain `index.md` (file rows with git-stamped "Last Updated"), and — as of sx7a — every `{domain}/{sub-domain}/index.md`, adding a `## Sub-Domains` table to any domain that has them (emitted only when sub-domains exist, so flat domains stay byte-identical), all from folder contents + frontmatter, byte-stable and idempotent. The hydrate skill never hand-edits index rows. **Refuse-before-regen guard (defense-in-depth, glwc)**: before that regen, consult `fab memory-index --check`; on **exit 2** (destructive loss — a curated description would regenerate to `—`, a tombstone row would drop, or a custom grouping would flatten), refuse to regenerate and surface the `→ run /docs-reorg-memory to remediate ...` pointer (the orchestrator that relocates tombstone rows and dispatches `/docs-hydrate-memory` backfill mode for descriptions; backfill alone does not relocate tombstones). This guard is a **no-op for born-compatible fab-kit trees** — a tree hydrated by the pipeline is provably always exit 0/1, never 2, so it never fires here (do NOT mistake it for dead code or remove it). It is defense-in-depth for the pathological case of a pre-fab-kit tree reaching the pipeline's hydrate stage; the loss logic lives entirely in Go (tiered `--check` exit codes 0/1/2 — see [kit-architecture.md](/distribution/kit-architecture.md) § fab memory-index), so this site adds only a one-line exit-code consult, the same primitive `/docs-hydrate-memory` and `/docs-reorg-memory` consult (see [memory-docs/hydrate.md](/memory-docs/hydrate.md) § Refuse-Before-Regen Guard). Heed (but do not block on) any non-fatal shape-bound warnings it prints across the recursive tree — they signal an over-wide / over-deep folder that `docs-reorg-memory` can rebalance (its apply path actually moves files, rewrites the broken relative links in both directions, and re-runs `fab memory-index` under a no-dangling-link guard — sx7a; the rebalance itself is a deliberate, separately-reviewed `/docs-reorg-memory` run, not part of hydrate) +5. **Regenerate indexes** — run `fab memory-index` (tciy). It deterministically rewrites the root `docs/memory/index.md` (domains-only), every domain `index.md` (file rows — `| File | Description |`; content-only, no dates since ugde), and — as of sx7a — every `{domain}/{sub-domain}/index.md`, adding a `## Sub-Domains` table to any domain that has them (emitted only when sub-domains exist, so flat domains stay byte-identical), all from folder contents + frontmatter, byte-stable and idempotent. The hydrate skill never hand-edits index rows. **Refuse-before-regen guard (defense-in-depth, glwc)**: before that regen, consult `fab memory-index --check`; on **exit 2** (destructive loss — a curated description would regenerate to `—`, a tombstone row would drop, or a custom grouping would flatten), refuse to regenerate and surface the `→ run /docs-reorg-memory to remediate ...` pointer (the orchestrator that relocates tombstone rows and dispatches `/docs-hydrate-memory` backfill mode for descriptions; backfill alone does not relocate tombstones). This guard is a **no-op for born-compatible fab-kit trees** — a tree hydrated by the pipeline is provably always exit 0/1, never 2, so it never fires here (do NOT mistake it for dead code or remove it). It is defense-in-depth for the pathological case of a pre-fab-kit tree reaching the pipeline's hydrate stage; the loss logic lives entirely in Go (tiered `--check` exit codes 0/1/2 — see [kit-architecture.md](/distribution/kit-architecture.md) § fab memory-index), so this site adds only a one-line exit-code consult, the same primitive `/docs-hydrate-memory` and `/docs-reorg-memory` consult (see [memory-docs/hydrate.md](/memory-docs/hydrate.md) § Refuse-Before-Regen Guard). Heed (but do not block on) any non-fatal shape-bound warnings it prints across the recursive tree — they signal an over-wide / over-deep folder that `docs-reorg-memory` can rebalance (its apply path actually moves files, rewrites the broken relative links in both directions, and re-runs `fab memory-index` under a no-dangling-link guard — sx7a; the rebalance itself is a deliberate, separately-reviewed `/docs-reorg-memory` run, not part of hydrate) 6. **Update status** to `hydrate: done` in `.status.yaml` 7. **Pattern capture** *(optional)* — if the change introduced non-obvious implementation patterns that future changes should follow (e.g., a new error handling approach, a reusable abstraction), note them in the relevant memory file's Design Decisions section with the change name for traceability. Skip for implementations that follow existing patterns diff --git a/docs/memory/pipeline/index.md b/docs/memory/pipeline/index.md index 6f41999e..727c02c2 100644 --- a/docs/memory/pipeline/index.md +++ b/docs/memory/pipeline/index.md @@ -3,13 +3,13 @@ description: "The change pipeline — stage lifecycle & state machine, planning/ --- # Pipeline Documentation -> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`. +> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter. -| File | Description | Last Updated | -|------|-------------|-------------| -| [change-lifecycle](change-lifecycle.md) | Change naming, folder structure, `.status.yaml` (6-stage pipeline + `plan:` block, flock-serialized writes + fsync durability + sparse-key insertion), `.fab-status.yaml` symlink (atomic temp+rename swap, dangling-target validation), `stage_hooks` pre/post commands around start/finish (documented in c5tr), git integration (bookkeeping decoupled; ship-time branch↔change hard guard since w7dp), `/fab-status` (six-glyph progress legend incl. `⏭` skipped), `/fab-switch` (six-state `{state}` qualifier), backlog scanning, optional `summary` log field (FKF C-lite source, 5943) + the generated FKF memory-artifact surface (`fab memory-index` emits per-folder `log.md` + `fkf_version`/`type: memory` frontmatter, bmzo) | 2026-06-25 | -| [clarify](clarify.md) | `/fab-clarify` skill — intake-only (j6cs), dual modes (suggest/auto), intake taxonomy scan, structured questions, coverage reports, audit trail, grade reclassification, always-recompute intake score; hosts the [AUTO-MODE] Skill Invocation Protocol and is sole bulk-confirm authority (zc9m); bulk confirm evaluated before the zero-gaps exit, confirmations graded by recomputed composite (no fiat Certain), unified audit-trail placement rules (c5tr) | 2026-06-19 | -| [execution-skills](execution-skills.md) | Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded, fixing the `index.md` 'Last Updated' date drift that arises because hydrate's regen is pre-commit (o203). Operator coordination moved to runtime/operator.md. | 2026-06-25 | -| [planning-skills](planning-skills.md) | `/fab-new`, `/fab-continue`, `/fab-ff`, `/fab-clarify` — the planning stage (intake only) and the shared `_generation.md` partial (Intake + unified Plan procedures); requirement capture + plan generation live at apply entry (spec stage removed in j6cs); fab-new/fab-draft re-run/resume semantics (ID collisions route to resume, 9u91); change_type is hook-owned — skills verify via .status.yaml and override only if wrong (uliv); SRAD framework lives in the `_srad` helper, declared by the 6 planning skills (zc9m); pre-boundary intake creation (fab-new Steps 0–9) lives in the shared `_intake` helper parameterized by `{questioning-mode}` (interactive | promptless-defer) — fab-new/fab-draft/fab-proceed are thin call-sites, completing the one-shared-helper-per-phase symmetry with `_pipeline`; fab-new keeps the activate/branch tail, fab-draft stops at ready (momentum warning retired), fab-proceed dispatches promptless-defer + chains /fab-switch→/git-branch (3xaj); fab-ff/fab-fff are thin wrappers over the shared `_pipeline` bracket, fab-new Step 11 is a single first-match-wins branch table (szxd; 6 rows since g8st — remote-only `--track` checkout, dirty-tree carried-over note excluding the change's own artifacts, same-change rename); SRAD grades by half-open bands, the plan walk emits `## Assumptions` explicitly, artifacts always carry the section (omit-when-zero is display-only), fab-new output puts the Assumptions block last (c5tr); SRAD v2 demerit confidence scoring — composite weights `0.20/0.30/0.30/0.20`, grade bands 80/50/20 (indicative only), both hard-fail short-circuits (Unresolved→0.0 and `R<25∧A<25`) removed, blocking emergent from the penalty curve (4yi8); fab-new's backlog-ID collision pre-check is exact-ID anchored (`fab resolve --id` equality) and fab-proceed's promptless `_intake` dispatch carries the defer-and-surface contract with the matching `_srad` Critical-Rule carve-out (w7dp); fab-new/fab-draft `Next:` lines derive at runtime per the _preamble Lookup Procedure and `_generation` names fab-continue in both consumer groups (d9rs); change_type explicit set is sticky — set-change-type marks `change_type_source: explicit` and the hook re-infers only when source is absent/inferred, retiring the re-verify-after-later-writes caveat (jznd) | 2026-06-19 | -| [preflight](preflight.md) | `lib/preflight.sh` script — validation, accessor-based architecture, structured YAML output, skill integration | 2026-06-16 | -| [schemas](schemas.md) | Workflow schema authority — the Go state machine (`internal/status` transitions + `internal/statusfile` stage order/progress; declarative `workflow.yaml` retired in c5tr): 6-stage pipeline, states, transitions, validation rules; `.status.yaml` `plan:` (`## Requirements`-aware), `confidence:` (indicative retired), and lazy `true_impact:` block schemas (incl. the `tests` sub-block + render-time `impl` residual, 7t5a); `fab impact` and `fab pr-meta` helper subcommands (rj31); allowed-states-enforced transition targets, `fab score --check-gate` non-zero gate-fail exit, iterations-preserving reset cascade (k4ge; cycle-count accuracy is a choreography property not a state-machine one, the fix lives in skill prose — qg64); `fab score` normal-mode hard-fail on load/persist/read errors (hv7t); per-stage allowed-states + transition-event tables enumerated, pinned by the exhaustive 216-cell matrix test (tb6f); `change_type_source: inferred|explicit` guard (set-change-type marks explicit, hook re-infers only when not explicit), read-time `acceptance_completed` via `status.LiveAcceptance` (counter demoted to cache), and `internal/resolve` `ErrNotFound`/`ErrAmbiguous` sentinels (jznd); optional `summary` field + `set-summary`/`get-summary` verbs (FKF C-lite log source, 5943); the `log.md` C-lite schema + registry-gated change-id join + extended (benign-only, no-new-category) `--check` loss tiers (bmzo); the `log.seed.md` seed-merge — `parseSeedLog`/`mergeSeedEntries` curated-sidecar input merged beneath git-projected entries, idempotent, seed-only folders still emit a log.md, loss tier stays benign (oovf); freeze-on-write `log.md` generation — existing log is authoritative/write-once, `parseLog` reads it back, append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` destructive escape hatch, `--check` superset-PASS / missing-or-hand-edit-FAIL via merge-as-rendered (tayp) | 2026-06-25 | +| File | Description | +|------|-------------| +| [change-lifecycle](change-lifecycle.md) | Change naming, folder structure, `.status.yaml` (6-stage pipeline + `plan:` block, flock-serialized writes + fsync durability + sparse-key insertion), `.fab-status.yaml` symlink (atomic temp+rename swap, dangling-target validation), `stage_hooks` pre/post commands around start/finish (documented in c5tr), git integration (bookkeeping decoupled; ship-time branch↔change hard guard since w7dp), `/fab-status` (six-glyph progress legend incl. `⏭` skipped), `/fab-switch` (six-state `{state}` qualifier), backlog scanning, optional `summary` log field (FKF C-lite source, 5943) + the generated FKF memory-artifact surface (`fab memory-index` emits per-folder `log.md` + `fkf_version`/`type: memory` frontmatter, bmzo) | +| [clarify](clarify.md) | `/fab-clarify` skill — intake-only (j6cs), dual modes (suggest/auto), intake taxonomy scan, structured questions, coverage reports, audit trail, grade reclassification, always-recompute intake score; hosts the [AUTO-MODE] Skill Invocation Protocol and is sole bulk-confirm authority (zc9m); bulk confirm evaluated before the zero-gaps exit, confirmations graded by recomputed composite (no fiat Certain), unified audit-trail placement rules (c5tr) | +| [execution-skills](execution-skills.md) | Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded; it is retained for **`log.md` only** (freeze-on-write needs the post-commit projection to capture the change's own entry while its commits are still reachable, pre-squash — the index carries no dates since ugde, so its regen half is a reliable no-op) (o203; rationale narrowed by ugde). Operator coordination moved to runtime/operator.md. | +| [planning-skills](planning-skills.md) | `/fab-new`, `/fab-continue`, `/fab-ff`, `/fab-clarify` — the planning stage (intake only) and the shared `_generation.md` partial (Intake + unified Plan procedures); requirement capture + plan generation live at apply entry (spec stage removed in j6cs); fab-new/fab-draft re-run/resume semantics (ID collisions route to resume, 9u91); change_type is hook-owned — skills verify via .status.yaml and override only if wrong (uliv); SRAD framework lives in the `_srad` helper, declared by the 6 planning skills (zc9m); pre-boundary intake creation (fab-new Steps 0–9) lives in the shared `_intake` helper parameterized by `{questioning-mode}` (interactive | promptless-defer) — fab-new/fab-draft/fab-proceed are thin call-sites, completing the one-shared-helper-per-phase symmetry with `_pipeline`; fab-new keeps the activate/branch tail, fab-draft stops at ready (momentum warning retired), fab-proceed dispatches promptless-defer + chains /fab-switch→/git-branch (3xaj); fab-ff/fab-fff are thin wrappers over the shared `_pipeline` bracket, fab-new Step 11 is a single first-match-wins branch table (szxd; 6 rows since g8st — remote-only `--track` checkout, dirty-tree carried-over note excluding the change's own artifacts, same-change rename); SRAD grades by half-open bands, the plan walk emits `## Assumptions` explicitly, artifacts always carry the section (omit-when-zero is display-only), fab-new output puts the Assumptions block last (c5tr); SRAD v2 demerit confidence scoring — composite weights `0.20/0.30/0.30/0.20`, grade bands 80/50/20 (indicative only), both hard-fail short-circuits (Unresolved→0.0 and `R<25∧A<25`) removed, blocking emergent from the penalty curve (4yi8); fab-new's backlog-ID collision pre-check is exact-ID anchored (`fab resolve --id` equality) and fab-proceed's promptless `_intake` dispatch carries the defer-and-surface contract with the matching `_srad` Critical-Rule carve-out (w7dp); fab-new/fab-draft `Next:` lines derive at runtime per the _preamble Lookup Procedure and `_generation` names fab-continue in both consumer groups (d9rs); change_type explicit set is sticky — set-change-type marks `change_type_source: explicit` and the hook re-infers only when source is absent/inferred, retiring the re-verify-after-later-writes caveat (jznd) | +| [preflight](preflight.md) | `lib/preflight.sh` script — validation, accessor-based architecture, structured YAML output, skill integration | +| [schemas](schemas.md) | Workflow schema authority — the Go state machine (`internal/status` transitions + `internal/statusfile` stage order/progress; declarative `workflow.yaml` retired in c5tr): 6-stage pipeline, states, transitions, validation rules; `.status.yaml` `plan:` (`## Requirements`-aware), `confidence:` (indicative retired), and lazy `true_impact:` block schemas (incl. the `tests` sub-block + render-time `impl` residual, 7t5a); `fab impact` and `fab pr-meta` helper subcommands (rj31); allowed-states-enforced transition targets, `fab score --check-gate` non-zero gate-fail exit, iterations-preserving reset cascade (k4ge; cycle-count accuracy is a choreography property not a state-machine one, the fix lives in skill prose — qg64); `fab score` normal-mode hard-fail on load/persist/read errors (hv7t); per-stage allowed-states + transition-event tables enumerated, pinned by the exhaustive 216-cell matrix test (tb6f); `change_type_source: inferred|explicit` guard (set-change-type marks explicit, hook re-infers only when not explicit), read-time `acceptance_completed` via `status.LiveAcceptance` (counter demoted to cache), and `internal/resolve` `ErrNotFound`/`ErrAmbiguous` sentinels (jznd); optional `summary` field + `set-summary`/`get-summary` verbs (FKF C-lite log source, 5943); the `log.md` C-lite schema + registry-gated change-id join + extended (benign-only, no-new-category) `--check` loss tiers (bmzo); the `log.seed.md` seed-merge — `parseSeedLog`/`mergeSeedEntries` curated-sidecar input merged beneath git-projected entries, idempotent, seed-only folders still emit a log.md, loss tier stays benign (oovf); freeze-on-write `log.md` generation — existing log is authoritative/write-once, `parseLog` reads it back, append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` destructive escape hatch, `--check` superset-PASS / missing-or-hand-edit-FAIL via merge-as-rendered (tayp) | diff --git a/docs/memory/pipeline/schemas.md b/docs/memory/pipeline/schemas.md index 8c554dcc..cc0a2637 100644 --- a/docs/memory/pipeline/schemas.md +++ b/docs/memory/pipeline/schemas.md @@ -153,7 +153,7 @@ As of 260615-bmzo (FKF KEYSTONE, change 2/4) `fab memory-index` emits a generate **The two-source C-lite join (the schema-relevant part):** -1. **Git history** — the *when* / *which-file* / *per-commit name-status*, taken from the **same single batched pass** the index dates use: `git log -c core.quotepath=off --date=short --format= --name-status -- docs/memory`. bmzo widened the former `--name-only` projection to `--name-status` so the one pass yields BOTH the existing newest-date-per-path map (`byPath`, index "Last Updated", behavior unchanged) AND a new ordered per-path commit list (`commitsByPath` — `(date, subject, status)` tuples). No per-file `git log` spawn is reintroduced (pw3k F34 invariant preserved). +1. **Git history** — the *when* / *which-file* / *per-commit name-status*, taken from a **single batched pass**: `git log -c core.quotepath=off --date=short --format= --name-status -- docs/memory`. bmzo widened the former `--name-only` projection to `--name-status` so the one pass yields an ordered per-path commit list (`commitsByPath` — `(date, subject, status)` tuples). The pass formerly also produced a newest-date-per-path map (`byPath`) that fed the index "Last Updated" cell, but **ugde removed both the index date column and `byPath`** (the index is dateless / content-only since then), so the batched pass now yields **only `commitsByPath` for `log.md`** — `log.md` is its sole consumer. No per-file `git log` spawn is reintroduced (pw3k F34 invariant preserved). 2. **`.status.yaml` `summary:`** — the *what*, the **source-field linkage to Change 1 (5943)**. `GatherLogs` builds a change registry by enumerating `fab/changes/*` + `fab/changes/archive/**` (the canonical `(change-id, folder, slug, summary)` set), reading each `.status.yaml` `summary:` via `internal/statusfile.Load`. The entry's descriptive line is that change's `summary`, or the **change slug** when the summary is empty/absent (§6.3 graceful fallback). **`LogData`/`LogEntry` render contract** (pure `RenderLog(LogData) string`, mirroring `RenderRoot`/`RenderDomain`): `LogData{Title, Entries []LogEntry}`; `LogEntry{Date, Verb, FileBase, BundleRelPath, Summary, ChangeID}`. Output is `# Log — {Title}` + the `Do not hand-edit` generated comment, then entries **date-grouped, newest date first** (`## YYYY-MM-DD`), each `- {**Verb** }[base](/{domain}[/{sub}]/base.md) — {summary-or-slug} ({change-id})`. Intra-date order is a stable sort (date desc, then file base, then change-id) so output is byte-stable / idempotent. Verb derivation maps git name-status `A`→`**Creation**`, `D`→`**Deprecation**`, `M`/`R`/`C`/`T`→`**Update**`, else omit (optional per §6.2). Links are **bundle-relative** (FKF §7 — `/`-rooted, resolved from `docs/memory/`). A folder with zero attributable commits is **skipped** (no empty file — Design Decision: target set = "folders with history"). diff --git a/docs/memory/runtime/index.md b/docs/memory/runtime/index.md index 1cc8296b..8e5cf6c3 100644 --- a/docs/memory/runtime/index.md +++ b/docs/memory/runtime/index.md @@ -3,10 +3,10 @@ description: "tmux & multi-agent runtime — fab pane primitives, .fab-runtime.y --- # Runtime Documentation -> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`. +> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter. -| File | Description | Last Updated | -|------|-------------|-------------| -| [operator](operator.md) | Operator coordination skill (`/fab-operator`, superseding the historical operator4) — multi-repo / multi-session coordination on one tmux server via the `(session, repo, pane)` addressing tuple, the server-keyed operator state file (term defined once in skill §4), 3-file context loading (§1 exception, zc9m), multi-agent monitoring, auto-answer model with non-blocking strategic handling (auto-pick-and-notify / leave-open-and-notify, keep ticking, pick up async answer on a later tick — mmmt), an adaptive heartbeat (3m relaxed / 90s when any monitored agent is menu-waiting, one-loop invariant preserved — mmmt), the `rk notify`-default fail-silent notification-send abstraction (mmmt), repo-targeted spawning (spawning rules homed in `_cli-external.md` wt section; spawn sequence stated once in skill §6 with a 3-row entry-form table, szxd; now 7 steps with an existence-guarded pointer-activation step 3 — `fab change switch` in the new worktree, guarded on `fab resolve --folder`, fail-soft, so an operator-spawned existing-change worktree is self-describing, dispatch step renumbered 5→6, raw-text/backlog defer to `/fab-new`, 5xnx), two-tier dependency resolution (fetch-first, resolved-default-branch cherry-pick/rebase base — g8st), repo-spanning autopilot with a single spawn-embedded dispatch point (gate before the tab opens), nearest-same-repo-predecessor queue chaining, and the `/fab-fff ` entry-form command (w7dp), the markdown status frame (skill §4 Status Frame Format subsection, szxd), and tmux tab-naming, the git-optional / `fab/`-optional launcher (window cwd = git root else `os.Getwd()`; spawn command = project config else `spawn.DefaultSpawnCommand`) running its coordinating agent on the doing tier (`fab resolve-agent apply` + `spawn.WithProfile`, built-in `{claude-opus-4-8, high}` fallback — first non-pipeline consumer of the l3ja agent-tier system, 2sdj). Historical operator4 context plus the operator design-decision lineage. | 2026-06-17 | -| [pane-commands](pane-commands.md) | `fab pane {map,capture,send,process,window-name}` subcommand reference, persistent `--server`/`-L` flag, unified pane-family exit-code scheme (2 = pane missing / 3 = other tmux failure across capture/send/window-name), shared `internal/pane` helpers (`WithServer`, `RunCmd`/`StderrError`/`IsPaneMissing`/`PaneNotFoundError`, the targeted `display-message` pane-validation probe), pane map `display_state` JSON field, pane-ID-per-server semantics, motivating multi-socket use case, three-axis model (Change / Agent / Process), window-name primitives for idempotent / guarded tmux window rewrites | 2026-06-16 | -| [runtime-agents](runtime-agents.md) | `.fab-runtime.yaml` schema — `_agents[session_id]` keying, hook write/clear pipeline (stop/session-start/user-prompt) with one flock-serialized `UpdateAgent` call per event (GC folded in, skip-save-when-unchanged, no fsync), throttled GC via `last_run_gc`, grandparent PID walker, pane-map matching rule | 2026-06-16 | +| File | Description | +|------|-------------| +| [operator](operator.md) | Operator coordination skill (`/fab-operator`, superseding the historical operator4) — multi-repo / multi-session coordination on one tmux server via the `(session, repo, pane)` addressing tuple, the server-keyed operator state file (term defined once in skill §4), 3-file context loading (§1 exception, zc9m), multi-agent monitoring, auto-answer model with non-blocking strategic handling (auto-pick-and-notify / leave-open-and-notify, keep ticking, pick up async answer on a later tick — mmmt), an adaptive heartbeat (3m relaxed / 90s when any monitored agent is menu-waiting, one-loop invariant preserved — mmmt), the `rk notify`-default fail-silent notification-send abstraction (mmmt), repo-targeted spawning (spawning rules homed in `_cli-external.md` wt section; spawn sequence stated once in skill §6 with a 3-row entry-form table, szxd; now 7 steps with an existence-guarded pointer-activation step 3 — `fab change switch` in the new worktree, guarded on `fab resolve --folder`, fail-soft, so an operator-spawned existing-change worktree is self-describing, dispatch step renumbered 5→6, raw-text/backlog defer to `/fab-new`, 5xnx), two-tier dependency resolution (fetch-first, resolved-default-branch cherry-pick/rebase base — g8st), repo-spanning autopilot with a single spawn-embedded dispatch point (gate before the tab opens), nearest-same-repo-predecessor queue chaining, and the `/fab-fff ` entry-form command (w7dp), the markdown status frame (skill §4 Status Frame Format subsection, szxd), and tmux tab-naming, the git-optional / `fab/`-optional launcher (window cwd = git root else `os.Getwd()`; spawn command = project config else `spawn.DefaultSpawnCommand`) running its coordinating agent on the doing tier (`fab resolve-agent apply` + `spawn.WithProfile`, built-in `{claude-opus-4-8, high}` fallback — first non-pipeline consumer of the l3ja agent-tier system, 2sdj). Historical operator4 context plus the operator design-decision lineage. | +| [pane-commands](pane-commands.md) | `fab pane {map,capture,send,process,window-name}` subcommand reference, persistent `--server`/`-L` flag, unified pane-family exit-code scheme (2 = pane missing / 3 = other tmux failure across capture/send/window-name), shared `internal/pane` helpers (`WithServer`, `RunCmd`/`StderrError`/`IsPaneMissing`/`PaneNotFoundError`, the targeted `display-message` pane-validation probe), pane map `display_state` JSON field, pane-ID-per-server semantics, motivating multi-socket use case, three-axis model (Change / Agent / Process), window-name primitives for idempotent / guarded tmux window rewrites | +| [runtime-agents](runtime-agents.md) | `.fab-runtime.yaml` schema — `_agents[session_id]` keying, hook write/clear pipeline (stop/session-start/user-prompt) with one flock-serialized `UpdateAgent` call per event (GC folded in, skip-save-when-unchanged, no fsync), throttled GC via `last_run_gc`, grandparent PID walker, pane-map matching rule | diff --git a/docs/specs/fkf.md b/docs/specs/fkf.md index b5074e22..396a8ebe 100644 --- a/docs/specs/fkf.md +++ b/docs/specs/fkf.md @@ -55,8 +55,8 @@ A `docs/memory/` tree conforms to **FKF v0.1** if all of the following hold: Items 1–2 are the OKF conformance floor (specialized: `type` is fixed, `description` is promoted to required). Items 3–4 are FKF's added strictness. As in OKF, consumers SHOULD degrade -gracefully — a missing optional body section, an unknown extra frontmatter key, or a stale -"Last Updated" cell does not make a file non-conforming. +gracefully — a missing optional body section or an unknown extra frontmatter key does not make a +file non-conforming. --- @@ -192,8 +192,8 @@ job; the index command only detects and warns. Every directory holding ≥1 non-index `.md` carries a generated `index.md`. **All index tiers are generated artifacts written solely by `fab memory-index`** — agents never hand-edit index rows. -The render is a pure function of (folder contents + each file's `description:` frontmatter + git -dates), so the output is **byte-stable / idempotent**: two branches cannot produce conflicting +The render is a pure function of folder contents + each file's `description:` frontmatter — so +the output is **byte-stable / idempotent**: two branches cannot produce conflicting hand-edits to the same row, and any residual textual conflict auto-resolves by re-running `fab memory-index` post-merge. @@ -205,10 +205,10 @@ Three tiers: - **Root** (`docs/memory/index.md`) — **domains-only**: `| Domain | Description |`. Each domain row's Description is read from that domain `index.md`'s `description:` frontmatter (round-tripped by the generator). No inlined per-file column (it silently drifts). -- **Domain** (`docs/memory/{domain}/index.md`) — file rows: `| File | Description | Last Updated |`. - Description from each topic file's frontmatter; "Last Updated" git-stamped (degrades to `—` when - uncommitted / in a worktree / shallow clone), never hand-stamped. Carries its own - `description:` frontmatter (the source for the root row). When sub-domains exist, appends a +- **Domain** (`docs/memory/{domain}/index.md`) — file rows: `| File | Description |`. + Description from each topic file's frontmatter. The index carries no dates — it is a pure + function of content, so it is branch-independent and idempotent (recency lives in `log.md`). + Carries its own `description:` frontmatter (the source for the root row). When sub-domains exist, appends a `## Sub-Domains` table (`| Sub-Domain | Description |`) — emitted **only when sub-domains exist**, so a flat domain index is byte-identical to a sub-domain-free one. - **Sub-domain** (`docs/memory/{domain}/{sub-domain}/index.md`) — same file-row contract as a @@ -240,8 +240,8 @@ tables that FKF removes from memory files (§3.3). `log.md` is assembled from **two sources**, neither of which any agent hand-edits: 1. **Git history**, keyed to the folder — the *when*, the *which file*, and the change ID. This is - a projection of `git log` (the same date source the index uses), so it is always accurate and - never conflicts. + a projection of `git log` (the sole consumer of the batched git pass, now that the index is + dateless), so it is always accurate and never conflicts. 2. **A per-change one-line summary** — the *what*, written **once** into the change's own `.status.yaml` `summary:` field (§6.3). Because each change touches only *its own* `.status.yaml`, the summary has **zero conflict surface**. diff --git a/docs/specs/skills/SPEC-docs-reorg-memory.md b/docs/specs/skills/SPEC-docs-reorg-memory.md index 30f89b65..459799e2 100644 --- a/docs/specs/skills/SPEC-docs-reorg-memory.md +++ b/docs/specs/skills/SPEC-docs-reorg-memory.md @@ -38,7 +38,7 @@ For any move-bearing migration the skill, on approval: 1. **Moves files / sections** to their new paths (`git mv` semantics where possible; plain move otherwise), **preserving the moved file's FKF frontmatter** (`type: memory` + `description:`) byte-for-byte. 2. **Rewrites bundle-relative links** broken by the move — the dominant case being links *to* a moved file (their path-after-`/` changes), `#anchor` preserved — per the proposal's **Link Impact** list. A link to a sibling whose bundle path did not change needs no edit (§7). 3. **Authors `description:` frontmatter** (+ `type: memory`) — the curated index fields — on any new topic file a split creates, and creates each new sub-domain's stub `index.md` (only the `description:` frontmatter one-liner) **before** step 4's `fab memory-index` runs; the generator fills the body and round-trips the description. -4. **Regenerates indexes (and logs)** via `fab memory-index` (root + every domain + every sub-domain index, plus each folder's `log.md` — seed-merging any `log.seed.md` beneath the git projection, FKF §6). Generated rows and "Last Updated" cells are never hand-edited — the frontmatter stub is the only hand-curated part. +4. **Regenerates indexes (and logs)** via `fab memory-index` (root + every domain + every sub-domain index, plus each folder's `log.md` — seed-merging any `log.seed.md` beneath the git projection, FKF §6). Generated rows are never hand-edited — the frontmatter stub is the only hand-curated part. The index carries no dates (a pure function of content); recency lives in `log.md`. 5. **Enforces a no-dangling-link guard**: a residual broken `](/…)` bundle-relative link is a hard block — that migration is not finalized until every broken link is rewritten. **Abort escape**: if a dangling link cannot be rewritten (target genuinely gone, or ambiguous), that migration is rolled back instead — moves and link rewrites restored, `fab memory-index` re-run — and the remaining approved migrations continue. ### Link Impact (mandatory before approval) diff --git a/docs/specs/skills/SPEC-git-pr.md b/docs/specs/skills/SPEC-git-pr.md index 16f67dc8..ac3383db 100644 --- a/docs/specs/skills/SPEC-git-pr.md +++ b/docs/specs/skills/SPEC-git-pr.md @@ -15,7 +15,7 @@ Autonomously commits, pushes, and creates a draft GitHub PR. No prompts, no ques **Dispatch-target hardening** (260612-w7dp): accepts an optional explicit `` argument (any argument that isn't one of the 7 PR types), resolved transiently in Step 0 — `.fab-status.yaml` untouched; an explicit argument that fails to resolve STOPs (caller error), while argless failure keeps the silent `{has_fab}=false` degradation. Callers SHOULD pass the change folder name, not a bare 4-char id (an id spelling a type word — `feat`/`docs`/`test` — would classify as a type); `/fab-fff` Step 4 passes the folder name through (`/git-pr {name}`). Step 0 ends with a **branch-matches-change guard** (exact folder-name equality, or the folder name as a branch substring) that STOPs before any status mutation, commit, or push on mismatch — no autonomous checkout; an empty branch (detached HEAD) likewise STOPs there, before Step 0a's `fab status start` (verify-before-mutate parity with git-pr-review — Step 2's own guard still covers the no-fab path). It supersedes the former Step 1b non-blocking nudge, which is removed. -**Memory-index date-drift fix** (260620-o203): a new sub-step **3a-bis. Refresh Memory Indexes** sits between 3a (Commit) and 3b (Push). `fab memory-index` stamps each `index.md` row's "Last Updated" cell from `git log` (committed dates only); the hydrate-stage regen runs entirely pre-commit, so every file the change touched is stamped one regen behind until the content commit lands — a benign tier-1 `fab memory-index --check` drift at review-pr. 3a-bis closes the gap at the only pipeline position where `git log` already knows the change's real commit date: immediately after 3a commits, before 3b pushes. It regenerates byte-stably and, when `docs/memory/` actually drifted (`git diff --quiet -- docs/memory` non-zero), makes a **separate** `docs: refresh memory indexes` follow-up commit (never `--amend` — squash collapses the pair on merge); a no-drift regen produces no diff and no commit (Constitution III). It performs no push of its own — 3b's "if has_unpushed or just committed" trigger pushes both commits together. It is **gated on `{has_fab}` AND 3a-having-just-committed**, so it is a silent no-op for standalone `/git-pr` (`{has_fab}` false) and for the no-change re-run paths. On regen/commit failure it reports + STOPs with the 3a content commit intact (a benign stale-date index recoverable by re-running `fab memory-index` — never a torn state). +**Post-commit `log.md` refresh** (260620-o203; rationale narrowed by 260625-ugde): a sub-step **3a-bis. Refresh Memory Indexes** sits between 3a (Commit) and 3b (Push). Its job is `log.md`, a freeze-on-write projection of *committed* git history — it must capture this change's own entry while the change's commit is still reachable (pre-squash). The hydrate-stage regen runs entirely pre-commit, so it cannot see the change's own commit; 3a-bis closes that gap at the only pipeline position where `git log` already knows the change's commit: immediately after 3a commits, before 3b pushes. The **index** no longer participates — it carries no dates (a pure function of content, 260625-ugde), so its regen half is a reliable no-op; `log.md` is the sole reason 3a-bis remains. It regenerates byte-stably and, when `docs/memory/` actually drifted (`git diff --quiet -- docs/memory` non-zero), makes a **separate** `docs: refresh memory indexes` follow-up commit (never `--amend` — squash collapses the pair on merge); a no-drift regen produces no diff and no commit (Constitution III). It performs no push of its own — 3b's "if has_unpushed or just committed" trigger pushes both commits together. It is **gated on `{has_fab}` AND 3a-having-just-committed**, so it is a silent no-op for standalone `/git-pr` (`{has_fab}` false) and for the no-change re-run paths. On regen/commit failure it reports + STOPs with the 3a content commit intact (a benign stale `log.md` recoverable by re-running `fab memory-index` — never a torn state). **Prose optimization** (260620-skop): skill content trimmed to remove re-explanation of partial-owned concepts (the `fab change resolve` ID/substring/name forms, type-vs-change arg classification, the default-branch probe safety-net, and the 3a-bis "why here" rationale already in the Summary), and a `## Contents` TOC added; no behavioral change (Flow / Tools / Sub-agents unchanged). diff --git a/docs/specs/templates.md b/docs/specs/templates.md index 2f5f267a..e0e2ca4e 100644 --- a/docs/specs/templates.md +++ b/docs/specs/templates.md @@ -415,8 +415,8 @@ silently drifts as files are added). Each domain row's Description is read from The domain index carries its own curated one-liner as `description:` frontmatter (the source for the root row), then file rows. Descriptions come from each file's `description:` -frontmatter; "Last Updated" is git-stamped (`—` when uncommitted / in a worktree / shallow -clone). +frontmatter. The index carries no dates — it is a pure function of content (recency lives in +`log.md`). ```markdown --- @@ -426,10 +426,10 @@ description: "Authentication and authorization" > **Generated by `fab memory-index`** — do not hand-edit. -| File | Description | Last Updated | -|------|-------------|-------------| -| [authentication](authentication.md) | User login, session management, OAuth | {DATE} | -| [authorization](authorization.md) | Roles, permissions, access control | {DATE} | +| File | Description | +|------|-------------| +| [authentication](authentication.md) | User login, session management, OAuth | +| [authorization](authorization.md) | Roles, permissions, access control | ``` When a domain contains sub-domains, the generated domain index appends a `## Sub-Domains` @@ -460,10 +460,10 @@ description: "Chargebacks, arbitration, dispute lifecycle" > **Generated by `fab memory-index`** — do not hand-edit. -| File | Description | Last Updated | -|------|-------------|-------------| -| [arbitration](arbitration.md) | Dispute arbitration flow | {DATE} | -| [chargebacks](chargebacks.md) | Chargeback handling | {DATE} | +| File | Description | +|------|-------------| +| [arbitration](arbitration.md) | Dispute arbitration flow | +| [chargebacks](chargebacks.md) | Chargeback handling | ``` ### Memory Tree Shape (SHOULD guidance) @@ -538,7 +538,7 @@ description: "One-line summary used by the generated domain index row." | {change-name} | {DATE} | {one-line summary of what changed} | ``` -**Design rationale**: The index-based hierarchy solves discoverability — agents and humans can navigate from top-level down to any requirement without scanning folders. The Design Decisions section captures durable "why" context, so developers don't need to dig through archived changes to understand architectural choices. The Changelog table provides traceability back to the change that introduced each modification. Domain indexes include a git-stamped "Last Updated" so stale files are visible at a glance — generated by `fab memory-index`, never hand-stamped (hand-stamped dates silently drift). +**Design rationale**: The index-based hierarchy solves discoverability — agents and humans can navigate from top-level down to any requirement without scanning folders. The Design Decisions section captures durable "why" context, so developers don't need to dig through archived changes to understand architectural choices. The Changelog table provides traceability back to the change that introduced each modification. Domain indexes are a pure function of content (file names + descriptions + structure) — they carry no dates, so they are branch-independent and idempotent; recency-at-a-glance lives in each folder's `log.md`, generated by `fab memory-index`. ### Initial Memory (created by `/fab-setup`) @@ -568,6 +568,6 @@ When `/fab-continue` (hydrate) hydrates `plan.md` `## Requirements` into memory: 1. **New memory file**: If the requirements reference a file that doesn't exist yet, create it from the individual memory file template (including the `description:` frontmatter) and place it under the domain folder. If the domain doesn't exist, create the domain folder and a domain `index.md` carrying the domain's `description:` frontmatter. 2. **Existing memory file**: Compare the plan's `## Requirements` against the current file to determine what's new, changed, or removed. Update the Requirements section semantically. Minimize edits to unchanged sections. Keep the `description:` frontmatter accurate. -3. **Index updates**: Run `fab memory-index` — it regenerates the root (domains-only) and every domain index deterministically from folder contents + frontmatter + git dates. Never hand-edit index rows or "Last Updated" cells; the command is the single writer. +3. **Index updates**: Run `fab memory-index` — it regenerates the root (domains-only) and every domain index deterministically from folder contents + frontmatter. Never hand-edit index rows; the command is the single writer. 4. **Changelog row**: Append a row to the memory file's Changelog table with the change name, date, and one-line summary. 5. **Archive index**: Maintain `fab/changes/archive/index.md` listing all completed changes (most-recent-first). Each entry includes the change folder name and a one-line description. `/fab-archive` (via `fab change archive`) prepends the entry when moving a change into the date-bucketed archive. diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl new file mode 100644 index 00000000..3da38b61 --- /dev/null +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl @@ -0,0 +1,16 @@ +{"action":"enter","driver":"fab-new","event":"stage-transition","stage":"intake","ts":"2026-06-25T07:43:05Z"} +{"args":"Make fab memory-index idempotent by dropping the Last Updated column from generated domain/sub-domain indexes (live git-date projection is branch-relative; observed date-only churn in loom PR #1846)","cmd":"fab-new","event":"command","ts":"2026-06-25T07:43:05Z"} +{"delta":"+4.9","event":"confidence","score":4.9,"trigger":"calc-score","ts":"2026-06-25T07:44:29Z"} +{"delta":"+0.0","event":"confidence","score":4.9,"trigger":"calc-score","ts":"2026-06-25T07:44:37Z"} +{"cmd":"fab-fff","event":"command","ts":"2026-06-25T07:46:17Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-25T07:46:17Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-25T08:07:07Z"} +{"event":"review","result":"failed","ts":"2026-06-25T08:19:10Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-25T08:19:10Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-25T08:25:33Z"} +{"event":"review","result":"failed","ts":"2026-06-25T08:36:53Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-25T08:36:53Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-25T08:44:18Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T08:54:48Z"} +{"event":"review","result":"passed","ts":"2026-06-25T08:54:48Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:58:06Z"} diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml new file mode 100644 index 00000000..e11406cf --- /dev/null +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml @@ -0,0 +1,54 @@ +id: ugde +name: 260625-ugde-memory-index-drop-date-column +created: 2026-06-25T07:43:05Z +created_by: sahil-noon +change_type: refactor +issues: [] +progress: + intake: done + apply: done + review: done + hydrate: done + ship: active + review-pr: pending +plan: + generated: true + task_count: 19 + acceptance_count: 22 + acceptance_completed: 22 +confidence: + certain: 5 + confident: 2 + tentative: 0 + unresolved: 0 + score: 4.9 + fuzzy: true + dimensions: + signal: 85.0 + reversibility: 70.7 + competence: 90.7 + disambiguation: 86.4 +stage_metrics: + intake: {started_at: "2026-06-25T07:43:05Z", driver: fab-new, iterations: 1, completed_at: "2026-06-25T07:46:17Z"} + apply: {started_at: "2026-06-25T08:36:53Z", driver: fab-fff, iterations: 3, completed_at: "2026-06-25T08:44:18Z"} + review: {started_at: "2026-06-25T08:44:18Z", driver: fab-fff, iterations: 3, completed_at: "2026-06-25T08:54:48Z"} + hydrate: {started_at: "2026-06-25T08:54:48Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:58:06Z"} + ship: {started_at: "2026-06-25T08:58:06Z", driver: fab-fff, iterations: 1} +prs: [] +true_impact: + added: 0 + deleted: 0 + net: 0 + excluding: + added: 0 + deleted: 0 + net: 0 + tests: + added: 0 + deleted: 0 + net: 0 + computed_at: "2026-06-25T08:58:06Z" + computed_at_stage: hydrate +summary: drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md +# true_impact: lazily created on first apply-finish (no placeholder here). +last_updated: 2026-06-25T08:58:06Z diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/intake.md b/fab/changes/260625-ugde-memory-index-drop-date-column/intake.md new file mode 100644 index 00000000..e402c6ff --- /dev/null +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/intake.md @@ -0,0 +1,136 @@ +# Intake: Drop the "Last Updated" column from generated memory indexes + +**Change**: 260625-ugde-memory-index-drop-date-column +**Created**: 2026-06-25 + +## Origin + +Initiated from a `/fab-discuss` session about `fab memory-index` generating spurious changes when it shouldn't. The user observed (loom PR #1846) "a lot of changes that are ONLY date changes" — e.g. `docs/memory/lib-murphy/index.md` regenerating with a different `Last Updated` cell while the underlying memory content was unchanged. The user's requirement, verbatim: + +> memory-index needs to get to a stable state such that for no doc changes, it really shouldn't update dates (idempotency) — it should be safe to re-run again and again. + +The discussion diagnosed the root cause and weighed four options (drop the column / freeze-on-write the dates / pin to a stable ref / relax `--check`). The user explicitly chose **Option A — drop the "Last Updated" column entirely** (interactive selection in the discuss session). Mode: conversational; the design decision and full sweep class were resolved in-conversation before this intake was created. + +## Why + +**The problem.** The domain/sub-domain `index.md` renders `| File | Description | Last Updated |`, and the `Last Updated` cell is a **live `git log` projection** (author date `%ad`, via `loadGitDates` → `gitDates.lookup` in `src/go/fab/internal/memoryindex/memoryindex.go:316`, rendered at `:183-187`). `git log -1 -- ` is stable only for a *fixed HEAD*; it is HEAD/branch-relative. So: + +- A branch cut from (or not rebased onto) a newer `main` doesn't contain commits that touched a file after its branch point → regenerating there projects an **older** date than `main`'s committed index → the regen *reverts* the cell. +- Concurrent PRs each branch at a different point and project a different date snapshot for the same unchanged files → the cells churn back and forth on merge. This is exactly the loom PR #1846 symptom: many files × many concurrent branches = "lots of date-only changes." + +So the index is idempotent only under "same HEAD" — the one assumption that never holds across the branch/rebase/merge lifecycle. This violates Constitution III (Idempotent Operations). + +**The consequence if unfixed.** Persistent date-only churn in every domain index; `fab memory-index --check` flags it as benign tier-1 drift (exit 1) at review-pr, forcing the recurring regen-and-recommit dance. An entire downstream workaround already exists for it — `/git-pr` sub-step 3a-bis (change `o203`), a post-commit/pre-push regen — and the cost keeps recurring on every change touching `docs/memory/`. + +**Why this approach over alternatives.** This is the *exact* non-determinism that `fkf.md` §6.4 already recognized and fixed for `log.md` via **freeze-on-write** ("A pure projection of *live* git history is not deterministic… re-projecting from scratch on every run produces a different result per contributor and across time"). The index date column was never given the same treatment — and `fkf.md` §5 even *claims* the index render is "byte-stable / idempotent" while depending on git dates, which is false for the date half. + +Of the four options: +- **A. Drop the column (chosen).** The date is the *only* non-content-derived, non-idempotent input. Remove it and the index becomes a pure function of content (file names + descriptions + structure) → genuinely branch-independent and idempotent, making §5's claim true. No capability is lost: dated, change-attributed history already lives in the per-folder freeze-on-write `log.md`. The index's job becomes pure navigation (what exists + what it's about); recency-at-a-glance is `log.md`'s job now. +- B. Freeze-on-write the date cell — stops backward drift only; still moves on any touch; re-introduces index parse/merge machinery the team kept out of this path; weaker guarantee than A. +- C. Pin dates to `origin/main`/merge-base — main still moves; new files show `—` until merged; breaks in shallow clones / no-origin / offline. Environment-dependent. Reject. +- D. Make `--check` ignore the date column — silences the gate only; the file still churns. Half-measure. + +## What Changes + +### 1. Go renderer (`src/go/fab/internal/memoryindex/memoryindex.go`) — the behavior change + +`RenderDomain` drops the third column. The header/separator/row format change from: + +```go +b.WriteString("| File | Description | Last Updated |\n") +b.WriteString("|------|-------------|-------------|\n") +// ... +fmt.Fprintf(&b, "| [%s](%s.md) | %s | %s |\n", f.Base, f.Base, desc, date) +``` + +to: + +```go +b.WriteString("| File | Description |\n") +b.WriteString("|------|-------------|\n") +// ... +fmt.Fprintf(&b, "| [%s](%s.md) | %s |\n", f.Base, f.Base, desc) +``` + +(`memoryindex.go:176-188`). Also update the package doc comment (lines 1-20) and the `RenderDomain`/§5 doc references that mention `git log` dates / "stamping Last Updated". + +Remove the now-dead date plumbing **on the index path only**: +- `FileEntry.LastUpdated` field (`:66-67`) and its population in `gatherFiles` (`:316`, the `dates.lookup(...)` call). +- `gitDates.byPath` (the newest-date-per-path map), `(*gitDates).lookup` (`:537-552`), and `gitLastUpdated` (`:559-569`) — the per-file date fallback. These exist **only** to serve the index date cell. +- In `parseGitLog` (`:488-529`), stop building/returning `byPath` (it has no remaining consumer). + +**KEEP**: `loadGitDates`, the batched `git log` pass, and `commitsByPath` — `log.md` generation (`gatherLogEntries`) still depends on the per-path commit list. The `--name-status` projection and the `top`/`gitRelPath` machinery stay. Only the *date-map* projection is removed; the *commit-list* projection is untouched. + +### 2. `--check` parser + classifier (`indexparse.go`, `loss.go`) + +`fab memory-index --check` parses the existing committed `index.md` to detect destructive-loss categories (description / tombstone / grouping). The existing-index row parser must expect **2 columns** for the domain/sub-domain tier so tombstone/description/grouping detection still works after the format change. Verify `Classify` and any golden/structural assumptions about the 3-column domain table are updated. (Root index is unaffected — it was already `| Domain | Description |`.) + +### 3. CLI help text (`src/go/fab/cmd/fab/memory_index.go`) + +The `Long` description mentions "stamping \"Last Updated\" from git" (`memory_index.go:26-27`) and the tier-1 example "a refreshed `Last Updated`" — update both. + +### 4. Tests + +`internal/memoryindex/*_test.go` — golden fixtures (`golden_test.go`), render tests (`memoryindex_test.go`), and any `freeze_test.go` / `log_test.go` / `seed_test.go` / `loss_test.go` cases that assert the 3-column domain index must be updated to the 2-column form. Per `code-quality.md` Test Strategy (test-alongside) and Constitution VII (tests conform to spec), update the goldens to the new rendered output. Run the `internal/memoryindex` + `cmd/fab` package tests before considering the change done. + +### 5. Spec + kit-reference mirror (constitution-pinned sync) + +- `docs/specs/fkf.md` — §2 (line 59, the "stale Last Updated cell" conformance note), §5 (lines 208-209, the Domain tier `| File | Description | Last Updated |` description + the "git-stamped" sentence), §6.1 (line 244, "the same date source the index uses" — the index no longer uses dates; reword so it refers to `log.md`'s use of the batched pass only). +- `src/kit/reference/fkf.md` — the **normative mirror** (lines 38, 146-147). `fkf.md`'s own header rule: *"Any change to FKF normative rules MUST update both files."* Both move together. +- `docs/specs/templates.md` — lines 418, 429, 463, 541 (the index-hierarchy design rationale), 571 (the "never hand-edit Last Updated cells" instruction). +- `src/kit/skills/_cli-fab.md` — § fab memory-index (lines 495, 506, 573, 578, 636): the command reference's column description, the batched-pass note, and the tier-1 drift example. + +### 6. Skills + their SPEC mirrors (the skill ↔ SPEC-*.md class) + +Each `src/kit/skills/*.md` edit requires its `docs/specs/skills/SPEC-*.md` mirror (Constitution Additional Constraints; `code-quality.md` § Sibling & Mirror Sweeps): +- `src/kit/skills/docs-hydrate-memory.md` (lines 37, 113) + `SPEC-docs-hydrate-memory.md` +- `src/kit/skills/fab-continue.md` (line 209) + `SPEC-fab-continue.md` +- `src/kit/skills/docs-reorg-memory.md` + `SPEC-docs-reorg-memory.md` (line 41) +- `src/kit/skills/git-pr.md` (line 213, the 3a-bis rationale) + `SPEC-git-pr.md` (line 18) — **see the 3a-bis nuance below**. + +Each occurrence drops/rewords the "Last Updated" column reference. Grep `Last Updated` repo-wide as the sweep anchor (the discussion enumerated the full hit list). + +### 7. Migration (`src/kit/migrations/`) + +The generated `index.md` files are user data; changing their column shape is a data restructuring → it MUST ship as a migration (context.md § Migrations; `code-review.md` project rule), not an ad-hoc script. The migration re-baselines every `index.md` to the 2-column form by running the new `fab memory-index`. Standard ordering: new binary first, then `/fab-setup migrations`. Pre-check that the installed binary produces the 2-column output before rewriting. That re-baseline commit is the **last** churn the repo sees from the date column; every run afterward is byte-stable. + +### 8. The 3a-bis nuance (do NOT delete it) + +`/git-pr` sub-step 3a-bis (change `o203`) is a post-commit/pre-push `fab memory-index` regen that was added *specifically* to close the index `Last Updated` date drift (hydrate's regen is pre-commit, so the index was born "one regen behind"). After dropping the column, **3a-bis is still required** — `log.md` (freeze-on-write) still needs that post-commit projection to capture the change's *own* entry while its commits are still reachable (pre-squash). What changes is its **rationale**: it narrows from "index dates + log.md" to "**log.md only**," and its index-regen half becomes a reliable no-op (the index no longer depends on commit timing). **Rewrite 3a-bis's prose (in `git-pr.md`, `SPEC-git-pr.md`, and `pipeline/execution-skills.md`) to reflect the narrowed rationale — do not rip the sub-step out.** + +## Affected Memory + +- `memory-docs/hydrate`: (modify) Index Maintenance / Generated Index sections — remove the `| File | Description | Last Updated |` column and the "git-stamped Last Updated" date-sourcing prose (lines 87, 97, 120); the index is now content-only. +- `memory-docs/hydrate-generate`: (modify) the domain-row format reference (line 89) → `| File | Description |`. +- `memory-docs/templates`: (modify) Index Hierarchy (lines 140-141, 187) and the design-rationale "Why" (line 254) — domain/sub-domain rows are now `| File | Description |`; recency lives in `log.md`. +- `distribution/kit-architecture`: (modify) the `fab memory-index` subcommand description (line 324) and the `internal/memoryindex` architecture paragraph (line 331) — drop the date-stamping description and the dead `byPath`/`lookup`/`gitLastUpdated` references; note `loadGitDates`/`commitsByPath` retained for `log.md`. +- `pipeline/execution-skills`: (modify) the hydrate "Regenerate indexes" step (line 222) and the 3a-bis design decision (lines 63, plus the index `description:` line 3) — narrow 3a-bis rationale to log.md-only. +- `pipeline/schemas`: (modify) the batched-git-pass description (line 156) — the one pass now yields only `commitsByPath` for `log.md`; the `byPath` index-date projection is removed. + +> Frozen, NOT modified: every `log.md` / `log.seed.md` historical entry that mentions the old 3-column format. They accurately describe what was true when written and are append-only/frozen artifacts — leave them. + +## Impact + +- **Code**: `src/go/fab/internal/memoryindex/{memoryindex.go,indexparse.go,loss.go}` + tests; `src/go/fab/cmd/fab/memory_index.go`. Net effect is partly a *deletion* (dead date plumbing). +- **Generated data**: every `docs/memory/**/index.md` loses a column (one-time, via migration). Root `index.md` unaffected. +- **Skills/specs/docs**: the `Last Updated` sweep class enumerated above (specs + kit reference mirror + 4 skills + their SPEC mirrors + 6 memory files). +- **Downstream**: `/git-pr` 3a-bis retained with narrowed rationale; the review-pr benign-drift dance disappears for the date column. +- **No API/flag changes**: `fab memory-index` keeps the same flags (`--check`, `--json`, `--rebuild`); only its rendered output and help text change. `--check` exit-code contract is unchanged. + +## Open Questions + +- None blocking. The design (Option A), the full sweep class, the migration approach, and the 3a-bis disposition were all resolved in the discuss session. Minor execution-time judgment (exact wording of the migration pre-check; whether `parseGitLog` keeps a 2-tuple return signature or collapses to one) is apply-stage decide-and-record, not a human gate. + +## Assumptions + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Certain | Drop the `Last Updated` column entirely (Option A), rather than freeze-on-write the dates, pin to a ref, or relax `--check` | User explicitly selected Option A in the discuss session after a four-option comparison; it is the only option that makes the index a pure function of content (true idempotency, Constitution III) | S:95 R:70 A:95 D:95 | +| 2 | Certain | Keep `loadGitDates` + `commitsByPath`; remove only the `byPath`/`lookup`/`gitLastUpdated` date-map plumbing | `log.md` generation (`gatherLogEntries`) consumes `commitsByPath` from the same batched pass; only the index date cell consumed `byPath` | S:90 R:80 A:95 D:90 | +| 3 | Certain | Ship a `src/kit/migrations/` file to re-baseline existing `index.md` files to 2-column | Generated index files are user data; column-shape restructuring MUST ship as a migration per context.md § Migrations + the code-review project rule, never an ad-hoc script | S:85 R:75 A:95 D:90 | +| 4 | Certain | Retain `/git-pr` sub-step 3a-bis; rewrite its rationale to log.md-only instead of deleting it | `log.md` freeze-on-write still needs a post-commit projection to capture the change's own entry pre-squash; only the index-date justification disappears | S:90 R:65 A:90 D:85 | +| 5 | Certain | Sweep the full `Last Updated` mirror class (specs + `src/kit/reference/fkf.md` + 4 skills + SPEC mirrors + 6 memory files) in one change | Missed sibling/mirror sweeps are this repo's #1 rework cause (`code-quality.md` § Sibling & Mirror Sweeps); reviewers treat SPEC-mirror + fkf dual-file sync as must-fix | S:90 R:60 A:90 D:85 | +| 6 | Confident | Update `indexparse.go`/`loss.go` so `--check` parses the 2-column domain table; `--check` exit-code contract unchanged | The destructive-loss detectors parse existing index rows; the column count is a structural input they must track. The classifier categories (description/tombstone/grouping) are column-shape-independent in intent | S:70 R:75 A:85 D:80 | +| 7 | Confident | Treat this as a single change (Go + specs + skills + memory + migration together), not split per surface | The Go render and its doc/spec/skill mirrors are constitution-pinned to move together; splitting would ship a skill change without its SPEC mirror (a must-fix violation) | S:75 R:70 A:85 D:80 | + +7 assumptions (5 certain, 2 confident, 0 tentative, 0 unresolved). diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/plan.md b/fab/changes/260625-ugde-memory-index-drop-date-column/plan.md new file mode 100644 index 00000000..2f968e7e --- /dev/null +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/plan.md @@ -0,0 +1,214 @@ +# Plan: Drop the "Last Updated" column from generated memory indexes + +**Change**: 260625-ugde-memory-index-drop-date-column +**Intake**: `intake.md` + +## Requirements + +### Renderer: Domain Index Column Shape + +#### R1: Domain/sub-domain index renders two columns only +The `RenderDomain` function SHALL render the domain (and sub-domain) file-row table with exactly two columns — `| File | Description |` — dropping the third `Last Updated` column. The root index and the `## Sub-Domains` table (already two-column) SHALL be unchanged. The render SHALL remain a pure function of content (file names + descriptions + structure), making the output branch-independent and idempotent (Constitution III). + +- **GIVEN** a `DomainData` with topic files +- **WHEN** `RenderDomain(d)` is called +- **THEN** the table header is `| File | Description |`, the separator is `|------|-------------|`, and each row is `| [base](base.md) | desc |` +- **AND** re-running `fab memory-index` on an unchanged tree produces a byte-identical index regardless of branch/HEAD + +#### R2: Dead index-only date plumbing is removed +The renderer package SHALL remove the date-map plumbing that existed solely to feed the index date cell: `FileEntry.LastUpdated`, its population in `gatherFiles` (the `dates.lookup(...)` call), `(*gitDates).lookup`, `gitLastUpdated`, and `gitDates.byPath` (with `parseGitLog` collapsed to return only what `log.md` needs). The batched `git log` pass (`loadGitDates`), `gitDates.commitsByPath`, `gitDates.top`, `gitRelPath`, and everything `gatherLogEntries`/`GatherLogs` consume SHALL be retained — `log.md` still depends on `commitsByPath`. + +- **GIVEN** the renderer package after the column drop +- **WHEN** the package is compiled and `log.md` generation runs +- **THEN** no `byPath`/`lookup`/`gitLastUpdated` symbols remain, and `log.md` still renders from `commitsByPath` +- **AND** the package doc comment no longer claims it stamps "Last Updated" / "git log dates" for the index + +### `--check` Classifier: Two-Column Parsing + +#### R3: `--check` parses the 2-column domain table; exit-code contract unchanged +The `indexparse.go`/`loss.go` destructive-loss detectors SHALL parse the new 2-column domain/sub-domain rows so description/tombstone/grouping detection still works after the format change. The `--check` exit-code contract (0 clean / 1 benign drift / 2 destructive loss) SHALL be unchanged. + +- **GIVEN** a committed 2-column `index.md` +- **WHEN** `fab memory-index --check` runs +- **THEN** description-loss, tombstone, and grouping detection behave as before and the exit code maps 0/1/2 unchanged + +### CLI Help Text + +#### R4: CLI help no longer mentions stamping "Last Updated" +The `memory-index` command's `Long` description and any tier-1 example SHALL no longer reference stamping/refreshing "Last Updated" from git for the index. + +- **GIVEN** `fab memory-index --help` +- **WHEN** the help text is read +- **THEN** it describes the index as content-only (no "Last Updated" / git-stamping for the index), while still describing the retained batched pass for `log.md` + +### Tests Conform to Spec + +#### R5: Tests assert the 2-column domain index +All renderer/classifier tests (`golden_test.go`, `memoryindex_test.go`, `loss_test.go`, and any `freeze_test.go`/`log_test.go`/`seed_test.go` case asserting a 3-column domain table) SHALL be updated to the 2-column form, and `parseGitLog`/`lookup` tests SHALL be updated for the collapsed signature (Constitution VII — tests conform to spec). + +- **GIVEN** the updated renderer and classifier +- **WHEN** `go test ./internal/memoryindex/... ./cmd/fab/...` runs +- **THEN** all tests pass against the 2-column output + +### Documentation Mirror Sweep + +#### R6: Spec + kit-reference mirror move together +`docs/specs/fkf.md` (§2/§5/§6.1), `src/kit/reference/fkf.md` (the normative mirror), `docs/specs/templates.md`, and `src/kit/skills/_cli-fab.md` SHALL drop/reword every live "Last Updated" / index-date reference. The fkf.md dual-file rule requires both fkf files move together. + +- **GIVEN** the doc sweep +- **WHEN** the specs/reference are read +- **THEN** no live spec/reference describes the index as carrying a git-stamped "Last Updated" column; the batched-pass description now attributes dates to `log.md` only + +#### R7: Skills + their SPEC mirrors move together +Each touched `src/kit/skills/*.md` (`docs-hydrate-memory.md`, `fab-continue.md`, `docs-reorg-memory.md`, `git-pr.md`) SHALL be edited together with its `docs/specs/skills/SPEC-*.md` mirror (Constitution Additional Constraints). Edits SHALL be on canonical `src/kit/skills/` sources only — never `.claude/skills/`. + +- **GIVEN** a skill edit dropping the "Last Updated" reference +- **WHEN** the change is reviewed +- **THEN** every edited skill has a matching SPEC-*.md edit in the same change + +#### R8: 3a-bis is retained with a log.md-only rationale +`/git-pr` sub-step 3a-bis SHALL NOT be deleted. Its rationale in `git-pr.md`, `SPEC-git-pr.md`, and `pipeline/execution-skills.md` SHALL be narrowed to "log.md only" — log.md still needs the post-commit projection to capture the change's own entry pre-squash; the index-regen half becomes a reliable no-op. + +- **GIVEN** the narrowed rationale +- **WHEN** 3a-bis prose is read +- **THEN** it justifies the post-commit regen by `log.md`'s freeze-on-write needs, not by index date drift, and the sub-step still exists + +### Memory Prose + +#### R9: Post-impl memory prose describing the column is reworded +`docs/memory/memory-docs/{hydrate,hydrate-generate,templates}.md`, `docs/memory/distribution/kit-architecture.md`, `docs/memory/pipeline/{execution-skills,schemas}.md` SHALL drop/reword the `| File | Description | Last Updated |` references and date-stamping prose, narrowing the 3a-bis design-decision prose to log.md-only. Frozen `log.md`/`log.seed.md` historical entries and generated `docs/memory/**/index.md` files SHALL be left untouched (the migration regenerates the indexes). The root `docs/memory/index.md` SHALL NOT be touched. + +- **GIVEN** the memory prose sweep +- **WHEN** the memory files are read +- **THEN** no active memory prose describes the index date column; frozen log entries are preserved verbatim + +### Migration + +#### R10: Migration re-baselines every index.md to 2-column form +A `src/kit/migrations/` file SHALL re-baseline every `docs/memory/**/index.md` to the 2-column form by running the new `fab memory-index`, matching the existing migration format/versioning convention, with a pre-check that the installed binary produces 2-column output before rewriting. This is the last churn the repo sees from the date column. + +- **GIVEN** an existing project with 3-column domain indexes +- **WHEN** `/fab-setup migrations` applies this migration +- **THEN** the indexes are re-baselined to 2-column and `fab memory-index --check` is byte-stable afterward (never tier 2) +- **AND** the migration aborts cleanly if the running binary still emits a 3-column index + +### Design Decisions + +1. **Drop the column (Option A)** rather than freeze-on-write / pin-to-ref / relax `--check`: only Option A makes the index a pure function of content — true idempotency. *Rejected*: B (still moves on any touch), C (env-dependent), D (file still churns). (Intake assumption #1.) +2. **`parseGitLog` collapses to a single return** (`commitsByPath` only): `byPath` has no remaining consumer once the index date cell is gone. Keeping a dead 2-tuple would be misleading. (Intake OQ — apply-stage judgment.) + +### Non-Goals + +- No flag/API changes to `fab memory-index` (`--check`, `--json`, `--rebuild` keep their signatures). +- No edits to frozen `log.md`/`log.seed.md` history or to generated `index.md` files by hand (the migration regenerates indexes). + +## Tasks + +### Phase 1: Core Renderer + +- [x] T001 In `src/go/fab/internal/memoryindex/memoryindex.go`, change `RenderDomain` to a 2-column table (header `| File | Description |`, separator `|------|-------------|`, row `| [%s](%s.md) | %s |`); reword the "do not hand-edit" note to drop "dates from `git log`"; update the package doc comment to drop "git log dates" / "stamping Last Updated" for the index +- [x] T002 In `memoryindex.go`, remove `FileEntry.LastUpdated`, the `dates.lookup(...)` call in `gatherFiles`, `(*gitDates).lookup`, `gitLastUpdated`, and `gitDates.byPath`; collapse `parseGitLog` to return only `commitsByPath`; update `loadGitDates` and all callers + doc comments; keep `loadGitDates`/`commitsByPath`/`top`/`gitRelPath`/`gatherLogEntries`/`GatherLogs` intact + +### Phase 2: `--check` Parser + CLI Help + +- [x] T003 In `src/go/fab/internal/memoryindex/indexparse.go` and `loss.go`, update the row-parsing doc comments/examples to expect 2-column domain rows; verify `parseIndexRows` (cell[0]=link, cell[1]=desc) is column-count-tolerant so detection still works; keep the 0/1/2 contract +- [x] T004 In `src/go/fab/cmd/fab/memory_index.go`, reword the `Long` description to drop "stamping \"Last Updated\" from git" (index) and reword the tier-1 example "a refreshed `Last Updated`"; keep the log.md batched-pass description + +### Phase 3: Tests + +- [x] T005 Update `golden_test.go`, `memoryindex_test.go`, and `loss_test.go` to the 2-column domain index; update `parseGitLog`/`lookup`/`LastUpdated` test expectations to the collapsed signature; run `cd src/go/fab && go test ./internal/memoryindex/... ./cmd/fab/...` and fix until green + +### Phase 4: Spec + Reference + CLI-ref Sweep + +- [x] T006 Edit `docs/specs/fkf.md` (§2 line ~59, §5 lines ~208-209, §6.1 line ~244) and `src/kit/reference/fkf.md` (lines ~38, ~146-147) together — drop the "Last Updated" column from the Domain tier, the conformance note, and reword §6.1's "same date source the index uses" to log.md-only +- [x] T007 Edit `docs/specs/templates.md` (lines ~418, ~429, ~463, ~541, ~571) — drop the `| File | Description | Last Updated |` tables, the design-rationale date sentence, and the "never hand-edit Last Updated cells" instruction +- [x] T008 Edit `src/kit/skills/_cli-fab.md` (§ fab memory-index, lines ~495, ~506, ~573, ~578, ~636) — column description, batched-pass note (log.md only), tier-1 drift example + +### Phase 5: Skills + SPEC Mirrors + +- [x] T009 [P] Edit `src/kit/skills/docs-hydrate-memory.md` (lines ~37, ~113) + `docs/specs/skills/SPEC-docs-hydrate-memory.md` (drop "Last Updated" cell references) +- [x] T010 [P] Edit `src/kit/skills/fab-continue.md` (line ~209) + `docs/specs/skills/SPEC-fab-continue.md` (drop "Last Updated" cell reference) +- [x] T011 [P] Edit `src/kit/skills/docs-reorg-memory.md` + `docs/specs/skills/SPEC-docs-reorg-memory.md` (line ~41, drop "Last Updated" cell reference) +- [x] T012 Edit `src/kit/skills/git-pr.md` (line ~213) + `docs/specs/skills/SPEC-git-pr.md` (line ~18) — rewrite 3a-bis rationale to log.md-only; do NOT delete the sub-step + +### Phase 6: Memory Prose + +- [x] T013 [P] Edit `docs/memory/memory-docs/hydrate.md` (lines ~87, ~97, ~120) and `docs/memory/memory-docs/hydrate-generate.md` (line ~89) — index is content-only, drop date-stamping prose +- [x] T014 [P] Edit `docs/memory/memory-docs/templates.md` (lines ~140-141, ~187, ~254) — domain/sub-domain rows are `| File | Description |`; recency lives in log.md +- [x] T015 [P] Edit `docs/memory/distribution/kit-architecture.md` (lines ~324, ~331) — drop date-stamping description + dead `byPath`/`lookup`/`gitLastUpdated` references; note `loadGitDates`/`commitsByPath` retained for log.md +- [x] T016 [P] Edit `docs/memory/pipeline/execution-skills.md` (lines ~3, ~63, ~222) and `docs/memory/pipeline/schemas.md` (line ~156) — narrow 3a-bis rationale to log.md-only; batched pass now yields only `commitsByPath` + +### Phase 7: Migration + +- [x] T017 Create `src/kit/migrations/2.6.6-to-{next}.md` matching the existing migration format — re-baseline every `docs/memory/**/index.md` to 2-column via `fab memory-index`, with a pre-check that the installed binary produces 2-column output before rewriting +- [x] T018 Bump `src/kit/VERSION` to `2.7.0` (the migration's target version). +- [x] T019 Catalog the new migration in `docs/memory/distribution/migrations.md` — add a `### 2.6.6-to-2.7.0` section mirroring the `### 2.5.5-to-2.6.0` entry's shape (re-baseline mechanism, the rendered-output binary pre-check as a second output-probe precedent, no `fab/`/`.status.yaml` change, VERSION bump to 2.7.0) and extend the frontmatter `description:` clause to mention the `2.6.6-to-2.7.0` migration. + +## Execution Order + +- T001, T002 (renderer) before T003 (parser comments reference renderer shape) and T005 (tests assert renderer output) +- T005 runs after T001-T004 (compiles + asserts final behavior) +- T006-T016 (doc sweep) are independent of the Go work and of each other (different files), but T012/T016 share the 3a-bis narrative — keep them consistent +- T017 (migration) last — it documents the shipped behavior + +## Acceptance + +### Functional Completeness + +- [x] A-001 R1: `RenderDomain` emits `| File | Description |` (2-column) for domain and sub-domain indexes; root and `## Sub-Domains` tables unchanged +- [x] A-002 R2: `FileEntry.LastUpdated`, `(*gitDates).lookup`, `gitLastUpdated`, `gitDates.byPath` are gone; `parseGitLog` returns only `commitsByPath`; `loadGitDates`/`commitsByPath`/`gatherLogEntries`/`GatherLogs` retained and `log.md` still generates +- [x] A-003 R3: `--check` parses 2-column domain rows; description/tombstone/grouping detection works; exit codes 0/1/2 unchanged +- [x] A-004 R4: `fab memory-index --help` no longer references stamping/refreshing "Last Updated" for the index +- [x] A-005 R5: renderer/classifier tests assert 2-column output and the collapsed `parseGitLog` signature +- [x] A-006 R6: `docs/specs/fkf.md`, `src/kit/reference/fkf.md`, `docs/specs/templates.md`, `src/kit/skills/_cli-fab.md` carry no live index "Last Updated" reference +- [x] A-007 R7: each edited `src/kit/skills/*.md` has its matching `docs/specs/skills/SPEC-*.md` edit; no `.claude/skills/` edits +- [x] A-008 R8: 3a-bis still exists in `git-pr.md`/`SPEC-git-pr.md`/`execution-skills.md` with a log.md-only rationale +- [x] A-009 R9: memory prose in the 6 listed files reworded; frozen log.md/log.seed.md and generated index.md untouched; root index.md untouched +- [x] A-010 R10: a `src/kit/migrations/` file re-baselines every index.md to 2-column with a binary pre-check, matching the existing migration convention + +### Behavioral Correctness + +- [x] A-011 R1: re-running `fab memory-index` on an unchanged tree is byte-stable (second run reports "already up to date") and branch-independent +- [x] A-012 R3: the `--check` exit-code contract is unchanged (verified by passing classifier tests) + +### Removal Verification + +- [x] A-013 R2: no dead date-map symbols remain (`go build` clean; grep finds no `byPath`/`lookup`/`gitLastUpdated`/`LastUpdated` in the index path) + +### Edge Cases & Error Handling + +- [x] A-014 R10: migration pre-check aborts cleanly when the running binary still emits a 3-column index (no partial rewrite) + +### Code Quality + +- [x] A-015 Pattern consistency: new code/doc follows surrounding naming, comment density, and idiom +- [x] A-016 No unnecessary duplication: reuses existing renderer/parser utilities; no reimplementation +- [x] A-017 Migrations for user-data restructuring: index column-shape change ships as a `src/kit/migrations/` file, not an ad-hoc script (code-review.md project rule) +- [x] A-018 Go changes ship tests: the `.go` change carries updated tests (Constitution VII) + +### documentation_accuracy + +- [x] A-019: every live spec/skill/template/active-memory reference to the index "Last Updated" column is dropped or reworded; only frozen historical `log.md`/`log.seed.md` entries and generated `index.md` files retain it +- [x] A-020: CLI help (`_cli-fab.md` + `memory_index.go` Long) and the memory `kit-architecture` description accurately describe the retained batched pass as serving `log.md` only + +### cross_references + +- [x] A-021: the skill ↔ SPEC-*.md mirror class and the `fkf.md` dual-file (spec + kit reference) are swept together — no half-updated mirror +- [x] A-022: 3a-bis prose is internally consistent across `git-pr.md`, `SPEC-git-pr.md`, `pipeline/execution-skills.md` (and `pipeline/index.md` if it restates the line) + +## Notes + +- Check items as you review: `- [x]` +- All acceptance items must pass before `/fab-continue` (hydrate) + +## Assumptions + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Certain | Drop the `Last Updated` column entirely (Option A) | User explicitly selected Option A in the discuss session; only option making the index a pure function of content (Constitution III) | S:95 R:70 A:95 D:95 | +| 2 | Certain | Collapse `parseGitLog` to return only `commitsByPath` (single return value) | `byPath` has no remaining consumer once the index date cell is gone; a dead 2-tuple would mislead readers | S:85 R:80 A:90 D:85 | +| 3 | Confident | Update `loss_test.go` existing/rendered fixtures to 2-column rather than leaving them 3-column | Tests should reflect the real shipped format (Constitution VII); the classifier is column-tolerant but fixtures must model reality | S:75 R:80 A:85 D:80 | +| 4 | Certain | Name the migration `2.6.6-to-2.7.0.md` (next minor after the installed 2.6.6) and bump kit VERSION to 2.7.0 | Matches the existing migration naming convention (`{from}-to-{to}.md`); a column-shape change is a minor bump, consistent with 2.5.5→2.6.0 (the prior memory-index migration) | S:80 R:75 A:85 D:80 | +| 5 | Confident | Migration uses a plain `fab memory-index` re-baseline (not `--rebuild`) with a 2-column binary pre-check | The index drop is byte-stable on a plain run; `--rebuild` is the destructive log.md escape hatch and is not needed for an index column change. The pre-check probes the rendered output, mirroring 2.5.5-to-2.6.0's capability probe | S:75 R:75 A:80 D:75 | + +5 assumptions (3 certain, 2 confident, 0 tentative). diff --git a/src/go/fab/cmd/fab/memory_index.go b/src/go/fab/cmd/fab/memory_index.go index 9d93a435..624ca239 100644 --- a/src/go/fab/cmd/fab/memory_index.go +++ b/src/go/fab/cmd/fab/memory_index.go @@ -23,8 +23,9 @@ func memoryIndexCmd() *cobra.Command { "FKF fkf_version: \"0.1\" frontmatter), every docs/memory/{domain}/index.md " + "(file rows + a Sub-Domains reference table when sub-domains exist), and " + "every docs/memory/{domain}/{sub-domain}/index.md (file rows) from folder " + - "contents, reading each file's H1 + `description:` frontmatter and " + - "stamping \"Last Updated\" from git. It also emits a per-folder FKF " + + "contents, reading each file's H1 + `description:` frontmatter. The index " + + "is a pure function of content (no git dates), so its output is " + + "branch-independent and idempotent. It also emits a per-folder FKF " + "log.md (C-lite change history: one batched git-log pass joined with each " + "change's .status.yaml summary, change-id recovered from the git history " + "and gated against the fab/changes registry; unattributable commits " + @@ -43,7 +44,8 @@ func memoryIndexCmd() *cobra.Command { "every log.md from current git (the pre-freeze behavior, opt-in) — for a " + "corrupted log or a deliberate re-baseline. With --check, writes nothing " + "and classifies drift by severity in the exit code: 0 = clean, 1 = benign " + - "drift (regen changes content but destroys nothing — all log.md / FKF " + + "drift (regen changes content but destroys nothing — an improved " + + "description:, all log.md / FKF " + "frontmatter drift is benign; for log.md a benign FAIL means the committed " + "log is missing a projected attributable (file-base, change-id) entry, or a " + "frozen line was hand-edited render-unstably — a committed log that is a " + diff --git a/src/go/fab/internal/memoryindex/golden_test.go b/src/go/fab/internal/memoryindex/golden_test.go index 9d7c2581..5ed9d102 100644 --- a/src/go/fab/internal/memoryindex/golden_test.go +++ b/src/go/fab/internal/memoryindex/golden_test.go @@ -46,8 +46,8 @@ func TestGolden_RenderDomain_FullDocument(t *testing.T) { Title: "Auth Documentation", Description: "Authentication & session handling", Files: []FileEntry{ - {Base: "login-flow", Description: "Login + MFA flow", LastUpdated: "2026-06-01"}, - {Base: "sessions", Description: "", LastUpdated: ""}, // both degrade + {Base: "login-flow", Description: "Login + MFA flow"}, + {Base: "sessions", Description: ""}, // description degrades }, SubDomains: []DomainData{ {Name: "tokens", Description: "Token issuance & rotation"}, @@ -58,12 +58,12 @@ func TestGolden_RenderDomain_FullDocument(t *testing.T) { "---\n" + "# Auth Documentation\n" + "\n" + - "> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`.\n" + + "> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter.\n" + "\n" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login-flow](login-flow.md) | Login + MFA flow | 2026-06-01 |\n" + - "| [sessions](sessions.md) | — | — |\n" + + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login-flow](login-flow.md) | Login + MFA flow |\n" + + "| [sessions](sessions.md) | — |\n" + "\n" + "## Sub-Domains\n" + "\n" + diff --git a/src/go/fab/internal/memoryindex/indexparse.go b/src/go/fab/internal/memoryindex/indexparse.go index 71fd2ea6..30438af1 100644 --- a/src/go/fab/internal/memoryindex/indexparse.go +++ b/src/go/fab/internal/memoryindex/indexparse.go @@ -26,9 +26,11 @@ type indexRow struct { // parseIndexRows extracts the table rows of an index file. It recognizes the // generated shapes — root `| [name](name/index.md) | desc |` and domain -// `| [base](base.md) | desc | date |` — plus any hand-curated row of the same -// `| [text](target) | desc | ... |` form. Header rows (`| Domain |`), -// separator rows (`|---|`), and non-row lines are ignored. +// `| [base](base.md) | desc |` — plus any hand-curated row of the same +// `| [text](target) | desc | ... |` form (extra trailing cells are ignored, so +// a legacy 3-column domain row still parses to the same link + description). +// Header rows (`| Domain |`), separator rows (`|---|`), and non-row lines are +// ignored. func parseIndexRows(content string) []indexRow { var rows []indexRow for _, line := range strings.Split(content, "\n") { diff --git a/src/go/fab/internal/memoryindex/loss.go b/src/go/fab/internal/memoryindex/loss.go index 0877ad0a..b573e895 100644 --- a/src/go/fab/internal/memoryindex/loss.go +++ b/src/go/fab/internal/memoryindex/loss.go @@ -4,10 +4,10 @@ package memoryindex // // The existing --check branch already computes the rendered-vs-existing drift // per index file (a string compare). What it cannot do is *classify* that -// drift: distinguish a benign improvement (a better description, a refreshed -// date) from a destructive loss (a curated description wiped to "—", a -// tombstone row silently dropped, a custom grouping flattened). This file adds -// that classifier as pure functions — the mechanical form of the three prose +// drift: distinguish a benign improvement (a better description) from a +// destructive loss (a curated description wiped to "—", a tombstone row +// silently dropped, a custom grouping flattened). This file adds that +// classifier as pure functions — the mechanical form of the three prose // signals 5ewp's /docs-reorg-memory detected by eye. // // All functions here are pure except the on-disk tombstone existence check, diff --git a/src/go/fab/internal/memoryindex/loss_test.go b/src/go/fab/internal/memoryindex/loss_test.go index b2fcba9e..107726dd 100644 --- a/src/go/fab/internal/memoryindex/loss_test.go +++ b/src/go/fab/internal/memoryindex/loss_test.go @@ -36,13 +36,13 @@ func TestClassify_Clean_NoDrift(t *testing.T) { func TestClassify_BenignDrift_ImprovedDescription(t *testing.T) { existing := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | old desc | 2026-01-01 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | old desc |\n" rendered := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | improved desc | 2026-06-15 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | improved desc |\n" report := Classify([]CheckTarget{ {Path: "docs/memory/auth/index.md", Existing: existing, Rendered: rendered, LinkBase: "auth"}, }, setExists(map[string]bool{"auth/login.md": true})) @@ -62,14 +62,14 @@ func TestClassify_BenignDrift_ImprovedDescription(t *testing.T) { func TestClassify_DescriptionLoss(t *testing.T) { existing := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | Curated login flow | 2026-01-01 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | Curated login flow |\n" // Regen: same row, but description gone to "—" (frontmatter missing). rendered := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | — | 2026-01-01 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | — |\n" report := Classify([]CheckTarget{ {Path: "docs/memory/auth/index.md", Existing: existing, Rendered: rendered, LinkBase: "auth"}, }, setExists(map[string]bool{"auth/login.md": true})) @@ -86,21 +86,24 @@ func TestClassify_DescriptionLoss(t *testing.T) { } func TestClassify_DescriptionAlreadyMissing_NotALoss(t *testing.T) { - // Existing already "—" → nothing curated to lose even though the row drifts. + // The login row's description is already "—" → nothing curated to lose, even + // though the file drifts (a sibling row's description improved). existing := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | — | 2026-01-01 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | — |\n" + + "| [signup](signup.md) | old desc |\n" rendered := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [login](login.md) | — | 2026-06-15 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [login](login.md) | — |\n" + + "| [signup](signup.md) | improved desc |\n" report := Classify([]CheckTarget{ {Path: "docs/memory/auth/index.md", Existing: existing, Rendered: rendered, LinkBase: "auth"}, - }, setExists(map[string]bool{"auth/login.md": true})) + }, setExists(map[string]bool{"auth/login.md": true, "auth/signup.md": true})) if report.Tier != TierBenignDrift { - t.Errorf("existing already — (date drift only) → TierBenignDrift, got %d", report.Tier) + t.Errorf("login already — (sibling improvement drift only) → TierBenignDrift, got %d", report.Tier) } } @@ -233,10 +236,10 @@ func TestClassify_SubDomainsHeading_NotGrouping(t *testing.T) { func TestClassify_HighestTierWins(t *testing.T) { // One file benign-drifts, another has a destructive loss → tier 2 overall. - benignExisting := "| [x](x.md) | old | d |\n" - benignRendered := "| [x](x.md) | new | d |\n" - lossExisting := "| [y](y.md) | Curated | d |\n" - lossRendered := "| [y](y.md) | — | d |\n" + benignExisting := "| [x](x.md) | old |\n" + benignRendered := "| [x](x.md) | new |\n" + lossExisting := "| [y](y.md) | Curated |\n" + lossRendered := "| [y](y.md) | — |\n" report := Classify([]CheckTarget{ {Path: "a/index.md", Existing: benignExisting, Rendered: benignRendered, LinkBase: "a"}, {Path: "b/index.md", Existing: lossExisting, Rendered: lossRendered, LinkBase: "b"}, @@ -250,14 +253,14 @@ func TestClassify_HighestTierWins(t *testing.T) { func TestClassify_Invariants_Tier2HasLoss_Tier1HasDrift(t *testing.T) { // Tier 2 ⇒ at least one loss; tier 1 ⇒ drift true, zero losses. loss := Classify([]CheckTarget{ - {Path: "b/index.md", Existing: "| [y](y.md) | Curated | d |\n", Rendered: "| [y](y.md) | — | d |\n", LinkBase: "b"}, + {Path: "b/index.md", Existing: "| [y](y.md) | Curated |\n", Rendered: "| [y](y.md) | — |\n", LinkBase: "b"}, }, setExists(map[string]bool{"b/y.md": true})) if loss.Tier == TierDestructiveLoss && len(loss.Losses) == 0 { t.Error("tier 2 must carry at least one loss") } benign := Classify([]CheckTarget{ - {Path: "a/index.md", Existing: "| [x](x.md) | old | d |\n", Rendered: "| [x](x.md) | new | d |\n", LinkBase: "a"}, + {Path: "a/index.md", Existing: "| [x](x.md) | old |\n", Rendered: "| [x](x.md) | new |\n", LinkBase: "a"}, }, setExists(map[string]bool{"a/x.md": true})) if benign.Tier == TierBenignDrift && (!benign.Drift || len(benign.Losses) != 0) { t.Errorf("tier 1 must have Drift=true and zero losses, got Drift=%v losses=%v", benign.Drift, benign.Losses) @@ -343,10 +346,16 @@ func TestParseIndexRows_GeneratedShapes(t *testing.T) { if len(rootRow) != 1 || rootRow[0].Target != "auth/index.md" || rootRow[0].Description != "Auth domain" { t.Errorf("root row parse mismatch: %+v", rootRow) } - domRow := parseIndexRows("| [login](login.md) | Login flow | 2026-06-01 |\n") + domRow := parseIndexRows("| [login](login.md) | Login flow |\n") if len(domRow) != 1 || domRow[0].Text != "login" || domRow[0].Target != "login.md" || domRow[0].Description != "Login flow" { t.Errorf("domain row parse mismatch: %+v", domRow) } + // A legacy 3-column domain row (a not-yet-migrated committed index) still + // parses to the same link + description — the extra trailing cell is ignored. + legacyRow := parseIndexRows("| [login](login.md) | Login flow | 2026-06-01 |\n") + if len(legacyRow) != 1 || legacyRow[0].Target != "login.md" || legacyRow[0].Description != "Login flow" { + t.Errorf("legacy 3-column row parse mismatch: %+v", legacyRow) + } } func TestParseIndexRows_SkipsHeaderAndSeparator(t *testing.T) { diff --git a/src/go/fab/internal/memoryindex/memoryindex.go b/src/go/fab/internal/memoryindex/memoryindex.go index d2b61157..b5b2f934 100644 --- a/src/go/fab/internal/memoryindex/memoryindex.go +++ b/src/go/fab/internal/memoryindex/memoryindex.go @@ -3,9 +3,12 @@ // docs/memory/{domain}/index.md (file rows). It is the deterministic // counterpart to the hand-maintained index rows that previously lived in the // hydrate / docs-reorg-memory skill prose — reading the same inputs (each -// memory file's H1 + `description:` frontmatter, plus `git log` dates) and -// emitting the exact same markdown on every run so the indexes stop drifting -// and stop generating merge conflicts on the hot per-row cells. +// memory file's H1 + `description:` frontmatter) and emitting the exact same +// markdown on every run so the indexes stop drifting and stop generating merge +// conflicts on the hot per-row cells. The index is a pure function of content +// (file names + descriptions + structure) — no git dates — so its output is +// branch-independent and idempotent; per-folder change history (the "when") +// lives in the freeze-on-write log.md instead. // // Rendering is split into pure functions that take structured inputs and // return markdown (RenderRoot / RenderDomain), plus a Gather orchestrator that @@ -62,9 +65,6 @@ type FileEntry struct { // Description is the `description:` frontmatter value; "" → rendered as the // missing-cell fallback. Description string - // LastUpdated is the `git log -1 --date=short` date (YYYY-MM-DD); "" → - // rendered as the missing-cell fallback. - LastUpdated string } // DomainData is everything RenderDomain needs to render one domain index. It is @@ -124,7 +124,7 @@ func (w Warning) String() string { } } -// missingCell is the fallback rendered for an absent description or date, +// missingCell is the fallback rendered for an absent description, // matching internal/prmeta's "—" convention for missing data. const missingCell = "—" @@ -172,19 +172,15 @@ func RenderDomain(d DomainData) string { fmt.Fprintf(&b, "---\ndescription: %q\n---\n", d.Description) } fmt.Fprintf(&b, "# %s\n\n", d.Title) - b.WriteString("> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`.\n\n") - b.WriteString("| File | Description | Last Updated |\n") - b.WriteString("|------|-------------|-------------|\n") + b.WriteString("> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter.\n\n") + b.WriteString("| File | Description |\n") + b.WriteString("|------|-------------|\n") for _, f := range d.Files { desc := f.Description if desc == "" { desc = missingCell } - date := f.LastUpdated - if date == "" { - date = missingCell - } - fmt.Fprintf(&b, "| [%s](%s.md) | %s | %s |\n", f.Base, f.Base, desc, date) + fmt.Fprintf(&b, "| [%s](%s.md) | %s |\n", f.Base, f.Base, desc) } // Sub-domain references — emitted only when sub-domains exist, so a flat // domain index renders byte-identically to the pre-recursion output. Mirrors @@ -205,11 +201,11 @@ func RenderDomain(d DomainData) string { } // Gather walks docs/memory/ under repoRoot and reads every input the index -// renderers need: each domain's topic files (H1 + `description:` frontmatter + -// git date) and a domain description for the root row. It also computes the -// non-fatal shape warnings. Returns (root, domains, warnings, err). A missing +// renderers need: each domain's topic files (H1 + `description:` frontmatter) +// and a domain description for the root row. It also computes the non-fatal +// shape warnings. Returns (root, domains, warnings, err). A missing // docs/memory/ directory is a hard error; everything else degrades gracefully -// (missing frontmatter / dates render as the missing-cell fallback). +// (missing frontmatter renders as the missing-cell fallback). // // domains is sorted lexicographically by Name; each domain's Files are sorted // lexicographically by Base — so the output is deterministic and byte-stable. @@ -224,11 +220,6 @@ func Gather(repoRoot string) (RootData, []DomainData, []Warning, error) { var root RootData var warnings []Warning - // One batched git-log pass over docs/memory replaces the per-file - // `git log -1` spawns (N files → N subprocesses, each a history walk). - // nil on failure → the per-file fallback inside dates.lookup. - dates := loadGitDates(repoRoot) - for _, e := range entries { if !e.IsDir() { continue @@ -236,8 +227,8 @@ func Gather(repoRoot string) (RootData, []DomainData, []Warning, error) { domainName := e.Name() domainDir := filepath.Join(memRoot, domainName) - files := gatherFiles(repoRoot, domainDir, dates) - subDomains := gatherSubDomains(repoRoot, domainDir, dates) + files := gatherFiles(domainDir) + subDomains := gatherSubDomains(domainDir) desc := domainDescription(domainDir) domains = append(domains, DomainData{ Name: domainName, @@ -285,15 +276,14 @@ func Gather(repoRoot string) (RootData, []DomainData, []Warning, error) { } // gatherFiles reads the topic files (non-index .md) directly under domainDir, -// sorted lexicographically by base name. dates is the batched git-date map -// (nil when the batched pass failed — lookup then falls back per file). +// sorted lexicographically by base name. // // index.md and log.md are both generated, single-writer artifacts — not topic // files — so they are skipped. (gatherLogEntries applies the identical skip; // excluding log.md here is what keeps a freshly-generated tree idempotent: a // second `fab memory-index` run must not read the just-written log.md back as a // topic row and add a spurious `[log](log.md)` line to the domain index.) -func gatherFiles(repoRoot, domainDir string, dates *gitDates) []FileEntry { +func gatherFiles(domainDir string) []FileEntry { dirEntries, err := os.ReadDir(domainDir) if err != nil { return nil @@ -313,7 +303,6 @@ func gatherFiles(repoRoot, domainDir string, dates *gitDates) []FileEntry { Base: base, Title: readH1(path), Description: frontmatter.Field(path, "description"), - LastUpdated: dates.lookup(repoRoot, path), }) } sort.Slice(files, func(i, j int) bool { return files[i].Base < files[j].Base }) @@ -327,7 +316,7 @@ func gatherFiles(repoRoot, domainDir string, dates *gitDates) []FileEntry { // warning, not an additional generated index tier (the depth-3 bound is // {domain}/{sub-domain}/{topic}.md). An empty sub-folder (no .md) yields no // entry, so it never produces a spurious index. -func gatherSubDomains(repoRoot, domainDir string, dates *gitDates) []DomainData { +func gatherSubDomains(domainDir string) []DomainData { dirEntries, err := os.ReadDir(domainDir) if err != nil { return nil @@ -339,7 +328,7 @@ func gatherSubDomains(repoRoot, domainDir string, dates *gitDates) []DomainData } subName := de.Name() subDir := filepath.Join(domainDir, subName) - files := gatherFiles(repoRoot, subDir, dates) + files := gatherFiles(subDir) if len(files) == 0 { continue // no topic files → not a sub-domain, no index to generate } @@ -400,14 +389,13 @@ func titleCase(name string) string { } // gitDates is the result of the single batched git-log pass over -// docs/memory: the most recent commit date per file, keyed by the path -// relative to the git top-level (slash-separated, as git prints it). The same -// pass also captures the full per-path commit list (commitsByPath) that the -// log.md generator joins with the change registry — both projections come from -// ONE `git log` invocation (no per-file spawns; pw3k F34). +// docs/memory: the full per-path commit list (commitsByPath) that the log.md +// generator joins with the change registry, keyed by the path relative to the +// git top-level (slash-separated, as git prints it). This is the sole git +// projection the package needs — the index is a pure function of content (no +// dates), so only log.md consumes this pass. type gitDates struct { top string // `git rev-parse --show-toplevel` for repoRoot - byPath map[string]string // repo-relative path → newest YYYY-MM-DD commitsByPath map[string][]gitTouch // repo-relative path → commits touching it, newest-first } @@ -440,15 +428,11 @@ const ( const gitLogFormat = "%x00%ad%x1f%s" // loadGitDates runs ONE `git log --date=short --name-status` pass over -// docs/memory and records (a) the first (= most recent, git log is -// newest-first) date seen per path and (b) the ordered per-path commit list -// the log generator consumes. Returns nil when git fails (not a repo, git -// missing) — callers then fall back to the per-file gitLastUpdated and emit no -// log.md. Equivalence with the per-file `git log -1 -- ` date defaults: -// merge commits contribute no file list (no -m), renames are not followed, -// matching both defaults; core.quotepath=off keeps non-ASCII paths unquoted so -// map keys match filesystem paths. --name-status (vs the former --name-only) -// is a superset — the date projection is unchanged; the status column is new. +// docs/memory and records the ordered per-path commit list the log generator +// consumes. Returns nil when git fails (not a repo, git missing) — callers then +// emit no log.md. core.quotepath=off keeps non-ASCII paths unquoted so map keys +// match filesystem paths. --name-status carries the per-commit status column +// the log's verb derivation needs. func loadGitDates(repoRoot string) *gitDates { topCmd := exec.Command("git", "rev-parse", "--show-toplevel") if repoRoot != "" { @@ -470,23 +454,19 @@ func loadGitDates(repoRoot string) *gitDates { if err != nil { return nil } - byPath, commitsByPath := parseGitLog(string(out)) - return &gitDates{top: top, byPath: byPath, commitsByPath: commitsByPath} + return &gitDates{top: top, commitsByPath: parseGitLog(string(out))} } // parseGitLog parses the batched `--format=%ad%s --name-status` stream -// into both projections the package needs from ONE pass: -// - byPath: the newest date per path (FIRST date seen wins, git being -// newest-first) — the index's "Last Updated" source, unchanged in behavior. -// - commitsByPath: the ordered (newest-first) list of (date, subject, status) -// tuples per path — the C-lite log's raw git input. +// into the per-path commit list the C-lite log generator consumes: +// commitsByPath maps each path to the ordered (newest-first) list of +// (date, subject, status) tuples touching it. // // A record begins with a NUL line carrying ""; the following // name-status lines are "\t" (or "\t\t" // for renames/copies — the LAST tab-field is the current path). Pure function, // extracted for unit tests. -func parseGitLog(out string) (byPath map[string]string, commitsByPath map[string][]gitTouch) { - byPath = make(map[string]string) +func parseGitLog(out string) (commitsByPath map[string][]gitTouch) { commitsByPath = make(map[string][]gitTouch) curDate, curSubject := "", "" for _, line := range strings.Split(out, "\n") { @@ -516,56 +496,13 @@ func parseGitLog(out string) (byPath map[string]string, commitsByPath map[string if path == "" { continue } - if _, seen := byPath[path]; !seen { - byPath[path] = curDate - } commitsByPath[path] = append(commitsByPath[path], gitTouch{ Date: curDate, Subject: curSubject, Status: status, }) } - return byPath, commitsByPath -} - -// lookup returns the last-updated date for path. With a populated batch map -// (non-nil receiver) it is a pure map lookup — a missing key means the file -// is uncommitted and yields "", exactly like the per-file call. With a nil -// receiver (batched pass failed), or when path cannot be expressed relative -// to the git top-level (e.g. symlinked temp dirs — git prints the resolved -// top), it falls back to the per-file gitLastUpdated spawn. -func (d *gitDates) lookup(repoRoot, path string) string { - if d == nil { - return gitLastUpdated(repoRoot, path) - } - rel, err := filepath.Rel(d.top, path) - if err != nil || strings.HasPrefix(rel, "..") { - // Retry with symlinks resolved (git's top-level is always resolved). - if resolved, rerr := filepath.EvalSymlinks(path); rerr == nil { - if r2, e2 := filepath.Rel(d.top, resolved); e2 == nil && !strings.HasPrefix(r2, "..") { - return d.byPath[filepath.ToSlash(r2)] - } - } - return gitLastUpdated(repoRoot, path) - } - return d.byPath[filepath.ToSlash(rel)] -} - -// gitLastUpdated returns `git log -1 --date=short --format=%ad ` run in -// repoRoot, or "" when git produces no output (uncommitted file, worktree / -// shallow-clone / squash / rebase context, or git unavailable). Mirrors how -// internal/prmeta degrades on missing git context — never an error. Kept as -// the per-file FALLBACK for when the batched loadGitDates pass fails. -func gitLastUpdated(repoRoot, path string) string { - cmd := exec.Command("git", "log", "-1", "--date=short", "--format=%ad", "--", path) - if repoRoot != "" { - cmd.Dir = repoRoot - } - out, err := cmd.Output() - if err != nil { - return "" - } - return strings.TrimSpace(string(out)) + return commitsByPath } // --- C-lite log.md: change registry + commit attribution + gathering -------- @@ -735,7 +672,7 @@ func GatherLogs(repoRoot, fabRoot string, rebuild bool) ([]LogTarget, error) { targets = append(targets, t) } // Sub-domain logs (one level down, mirroring the index tiers). - for _, sd := range gatherSubDomains(repoRoot, domainDir, dates) { + for _, sd := range gatherSubDomains(domainDir) { subDir := filepath.Join(domainDir, sd.Name) if t, ok := buildLogTarget(repoRoot, dates, reg, subDir, domainName+"/"+sd.Name, sd.Title, rebuild); ok { targets = append(targets, t) diff --git a/src/go/fab/internal/memoryindex/memoryindex_test.go b/src/go/fab/internal/memoryindex/memoryindex_test.go index f5331f51..35613c62 100644 --- a/src/go/fab/internal/memoryindex/memoryindex_test.go +++ b/src/go/fab/internal/memoryindex/memoryindex_test.go @@ -48,21 +48,24 @@ func TestRenderDomain_FileRows(t *testing.T) { Name: "auth", Title: "Auth Documentation", Files: []FileEntry{ - {Base: "authentication", Description: "Login & sessions", LastUpdated: "2026-05-08"}, - {Base: "authorization", Description: "Roles & permissions", LastUpdated: "2026-04-02"}, + {Base: "authentication", Description: "Login & sessions"}, + {Base: "authorization", Description: "Roles & permissions"}, }, }) wantRows := "" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [authentication](authentication.md) | Login & sessions | 2026-05-08 |\n" + - "| [authorization](authorization.md) | Roles & permissions | 2026-04-02 |\n" + "| File | Description |\n" + + "|------|-------------|\n" + + "| [authentication](authentication.md) | Login & sessions |\n" + + "| [authorization](authorization.md) | Roles & permissions |\n" if !strings.HasSuffix(got, wantRows) { t.Fatalf("RenderDomain table mismatch.\n--- got ---\n%s\n--- want suffix ---\n%s", got, wantRows) } if !strings.HasPrefix(got, "# Auth Documentation\n") { t.Errorf("RenderDomain should start with the domain Title H1, got:\n%s", got) } + if strings.Contains(got, "Last Updated") { + t.Error("RenderDomain must NOT contain the dropped 'Last Updated' column") + } } func TestRenderDomain_NoSubDomainsByteIdentical(t *testing.T) { @@ -71,16 +74,16 @@ func TestRenderDomain_NoSubDomainsByteIdentical(t *testing.T) { got := RenderDomain(DomainData{ Name: "auth", Title: "Auth Documentation", - Files: []FileEntry{{Base: "authentication", Description: "Login & sessions", LastUpdated: "2026-05-08"}}, + Files: []FileEntry{{Base: "authentication", Description: "Login & sessions"}}, }) if strings.Contains(got, "Sub-Domains") { t.Errorf("a sub-domain-free domain must not emit a Sub-Domains section, got:\n%s", got) } want := "# Auth Documentation\n\n" + - "> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter; dates from `git log`.\n\n" + - "| File | Description | Last Updated |\n" + - "|------|-------------|-------------|\n" + - "| [authentication](authentication.md) | Login & sessions | 2026-05-08 |\n" + "> **Generated by `fab memory-index`** — do not hand-edit. Descriptions come from each file's `description:` frontmatter.\n\n" + + "| File | Description |\n" + + "|------|-------------|\n" + + "| [authentication](authentication.md) | Login & sessions |\n" if got != want { t.Fatalf("flat domain render drifted.\n--- got ---\n%q\n--- want ---\n%q", got, want) } @@ -90,14 +93,14 @@ func TestRenderDomain_WithSubDomains(t *testing.T) { got := RenderDomain(DomainData{ Name: "fab-workflow", Title: "Fab Workflow Documentation", - Files: []FileEntry{{Base: "context-loading", Description: "Loading convention", LastUpdated: "2026-06-07"}}, + Files: []FileEntry{{Base: "context-loading", Description: "Loading convention"}}, SubDomains: []DomainData{ {Name: "runtime", Description: "Runtime agents & skills"}, {Name: "schemas", Description: ""}, // missing description → degrades }, }) wantSuffix := "" + - "| [context-loading](context-loading.md) | Loading convention | 2026-06-07 |\n" + + "| [context-loading](context-loading.md) | Loading convention |\n" + "\n## Sub-Domains\n\n" + "| Sub-Domain | Description |\n" + "|------------|-------------|\n" + @@ -108,15 +111,15 @@ func TestRenderDomain_WithSubDomains(t *testing.T) { } } -func TestRenderDomain_MissingDescriptionAndDateDegrade(t *testing.T) { +func TestRenderDomain_MissingDescriptionDegrades(t *testing.T) { got := RenderDomain(DomainData{ Name: "auth", Title: "Auth Documentation", - Files: []FileEntry{{Base: "orphan", Description: "", LastUpdated: ""}}, + Files: []FileEntry{{Base: "orphan", Description: ""}}, }) - want := "| [orphan](orphan.md) | " + missingCell + " | " + missingCell + " |\n" + want := "| [orphan](orphan.md) | " + missingCell + " |\n" if !strings.HasSuffix(got, want) { - t.Fatalf("missing description+date should both degrade.\ngot:\n%s", got) + t.Fatalf("missing description should degrade.\ngot:\n%s", got) } } @@ -125,7 +128,7 @@ func TestRender_Idempotent(t *testing.T) { if RenderRoot(root) != RenderRoot(root) { t.Error("RenderRoot is not byte-stable for identical input") } - dom := DomainData{Name: "auth", Title: "Auth", Files: []FileEntry{{Base: "a", Description: "d", LastUpdated: "2026-01-01"}}} + dom := DomainData{Name: "auth", Title: "Auth", Files: []FileEntry{{Base: "a", Description: "d"}}} if RenderDomain(dom) != RenderDomain(dom) { t.Error("RenderDomain is not byte-stable for identical input") } @@ -189,21 +192,21 @@ func TestGather_ReadsFrontmatterAndH1(t *testing.T) { } } -func TestGather_UncommittedDateDegrades(t *testing.T) { - // A temp dir is not a git repo → git log produces no output → "" date, - // which the renderer degrades to the missing-cell fallback. +func TestGather_MissingDescriptionDegrades(t *testing.T) { + // A file with no `description:` frontmatter renders the missing-cell + // fallback in the (now two-column) domain index. repo := t.TempDir() writeFile(t, repo, "docs/memory/auth/x.md", "# X\n") _, domains, _, err := Gather(repo) if err != nil { t.Fatal(err) } - if domains[0].Files[0].LastUpdated != "" { - t.Errorf("uncommitted file should have empty LastUpdated, got %q", domains[0].Files[0].LastUpdated) + if domains[0].Files[0].Description != "" { + t.Errorf("file without frontmatter should have empty Description, got %q", domains[0].Files[0].Description) } out := RenderDomain(domains[0]) if !strings.Contains(out, "| "+missingCell+" |") { - t.Errorf("uncommitted date should render as %q in domain index, got:\n%s", missingCell, out) + t.Errorf("missing description should render as %q in domain index, got:\n%s", missingCell, out) } } @@ -450,34 +453,22 @@ func TestGather_SubDomainRenderIdempotent(t *testing.T) { } } -// --- Batched git-date pass (F34) ------------------------------------------- +// --- Batched git-commit pass (F34; serves log.md only) --------------------- func TestParseGitLog(t *testing.T) { - // New batched format: a record line "", then - // "\t" name-status rows. + // Batched format: a record line "", then + // "\t" name-status rows. The pass now yields only the + // per-path commit list (the index carries no dates). rec := func(date, subject string) string { return gitLogRecordSep + date + gitLogFieldSep + subject + "\n" } - t.Run("first (most recent) date per path wins; commits captured per path", func(t *testing.T) { + t.Run("commits captured per path, newest-first", func(t *testing.T) { out := rec("2026-06-10", "feat: thing (#420)") + "M\tdocs/memory/auth/x.md\nA\tdocs/memory/auth/y.md\n\n" + rec("2026-05-01", "Merge pull request #1 from o/260501-aaaa-slug") + "M\tdocs/memory/auth/x.md\nM\tdocs/memory/pay/z.md\n" - byPath, commits := parseGitLog(out) + commits := parseGitLog(out) - wantDates := map[string]string{ - "docs/memory/auth/x.md": "2026-06-10", // newest-first: first seen wins - "docs/memory/auth/y.md": "2026-06-10", - "docs/memory/pay/z.md": "2026-05-01", - } - for path, date := range wantDates { - if byPath[path] != date { - t.Errorf("parseGitLog byPath[%q] = %q, want %q", path, byPath[path], date) - } - } - if len(byPath) != len(wantDates) { - t.Errorf("parseGitLog byPath returned %d entries, want %d: %v", len(byPath), len(wantDates), byPath) - } // x.md was touched by both commits, newest-first; status + subject captured. xc := commits["docs/memory/auth/x.md"] if len(xc) != 2 { @@ -486,31 +477,32 @@ func TestParseGitLog(t *testing.T) { if xc[0].Date != "2026-06-10" || xc[0].Status != "M" || xc[0].Subject != "feat: thing (#420)" { t.Errorf("x.md newest commit mismatch: %+v", xc[0]) } - if xc[1].Subject != "Merge pull request #1 from o/260501-aaaa-slug" { - t.Errorf("x.md older commit subject mismatch: %+v", xc[1]) + if xc[1].Date != "2026-05-01" || xc[1].Subject != "Merge pull request #1 from o/260501-aaaa-slug" { + t.Errorf("x.md older commit mismatch: %+v", xc[1]) } // y.md was an addition. if yc := commits["docs/memory/auth/y.md"]; len(yc) != 1 || yc[0].Status != "A" { t.Errorf("y.md should have 1 added commit, got %+v", yc) } + // z.md touched once. + if zc := commits["docs/memory/pay/z.md"]; len(zc) != 1 || zc[0].Date != "2026-05-01" { + t.Errorf("z.md should have 1 commit dated 2026-05-01, got %+v", zc) + } }) t.Run("rename row uses the new (last) path", func(t *testing.T) { out := rec("2026-06-08", "docs: reorg (#381)") + "R099\tdocs/memory/old/foo.md\tdocs/memory/new/foo.md\n" - byPath, commits := parseGitLog(out) - if _, ok := byPath["docs/memory/new/foo.md"]; !ok { - t.Errorf("rename should key the NEW path, got %v", byPath) - } + commits := parseGitLog(out) if c := commits["docs/memory/new/foo.md"]; len(c) != 1 || c[0].Status != "R099" { t.Errorf("rename status should be preserved on the new path, got %+v", c) } }) - t.Run("empty input yields empty maps", func(t *testing.T) { - byPath, commits := parseGitLog("") - if len(byPath) != 0 || len(commits) != 0 { - t.Errorf("parseGitLog(\"\") = (%v,%v), want empty", byPath, commits) + t.Run("empty input yields empty map", func(t *testing.T) { + commits := parseGitLog("") + if len(commits) != 0 { + t.Errorf("parseGitLog(\"\") = %v, want empty", commits) } }) } @@ -528,10 +520,10 @@ func gitDateRun(t *testing.T, dir string, args ...string) { } } -// TestLoadGitDates_BatchEqualsPerFile pins commit author dates in a real git -// repo and asserts the batched pass yields exactly the dates the per-file -// `git log -1` calls produce — the F34 equivalence contract. -func TestLoadGitDates_BatchEqualsPerFile(t *testing.T) { +// TestLoadGitDates_CapturesCommitsPerPath pins commits in a real git repo and +// asserts the batched pass records the per-path commit list (newest-first) that +// log.md consumes. The index itself no longer carries dates. +func TestLoadGitDates_CapturesCommitsPerPath(t *testing.T) { repo := t.TempDir() gitDateRun(t, repo, "init") writeFile(t, repo, "docs/memory/auth/x.md", "# X\n") @@ -540,57 +532,32 @@ func TestLoadGitDates_BatchEqualsPerFile(t *testing.T) { writeFile(t, repo, "docs/memory/auth/y.md", "# Y\n") gitDateRun(t, repo, "add", ".") gitDateRun(t, repo, "commit", "-m", "second", "--date", "2026-03-02T12:00:00 +0000") - // x.md touched again later — most recent date must win. + // x.md touched again later — newest commit must come first in the list. writeFile(t, repo, "docs/memory/auth/x.md", "# X updated\n") gitDateRun(t, repo, "add", ".") gitDateRun(t, repo, "commit", "-m", "third", "--date", "2026-04-20T12:00:00 +0000") - // Uncommitted file: present on disk, absent from the batch map → "". - writeFile(t, repo, "docs/memory/auth/uncommitted.md", "# U\n") dates := loadGitDates(repo) if dates == nil { t.Fatal("loadGitDates returned nil in a real git repo") } - wantDates := map[string]string{ - filepath.Join(repo, "docs/memory/auth/x.md"): "2026-04-20", - filepath.Join(repo, "docs/memory/auth/y.md"): "2026-03-02", - filepath.Join(repo, "docs/memory/auth/uncommitted.md"): "", - } - for path, want := range wantDates { - if got := dates.lookup(repo, path); got != want { - t.Errorf("batched lookup(%q) = %q, want %q", path, got, want) - } - // Equivalence: the per-file fallback must agree. - if perFile := gitLastUpdated(repo, path); perFile != want { - t.Errorf("per-file gitLastUpdated(%q) = %q, want %q (fixture broken?)", path, perFile, want) - } - } - - // Gather threads the same dates into FileEntry.LastUpdated. - _, domains, _, err := Gather(repo) - if err != nil { - t.Fatal(err) + rel := func(p string) string { return filepath.ToSlash(filepath.Join("docs/memory/auth", p)) } + xc := dates.commitsByPath[rel("x.md")] + if len(xc) != 2 || xc[0].Date != "2026-04-20" || xc[1].Date != "2026-01-15" { + t.Errorf("x.md commit list mismatch (want 2026-04-20 then 2026-01-15): %+v", xc) } - byBase := map[string]string{} - for _, f := range domains[0].Files { - byBase[f.Base] = f.LastUpdated - } - if byBase["x"] != "2026-04-20" || byBase["y"] != "2026-03-02" || byBase["uncommitted"] != "" { - t.Errorf("Gather dates mismatch: %v", byBase) + yc := dates.commitsByPath[rel("y.md")] + if len(yc) != 1 || yc[0].Date != "2026-03-02" { + t.Errorf("y.md commit list mismatch (want one dated 2026-03-02): %+v", yc) } } -// TestGitDatesLookup_NilReceiverFallsBack verifies the nil-receiver fallback -// path used when the batched git pass fails (non-git dir, git missing). -func TestGitDatesLookup_NilReceiverFallsBack(t *testing.T) { - var d *gitDates +// TestLoadGitDates_NilOutsideGitRepo verifies the batched pass returns nil when +// git is unavailable (non-git dir) — callers then emit no log.md. +func TestLoadGitDates_NilOutsideGitRepo(t *testing.T) { nonGit := t.TempDir() writeFile(t, nonGit, "docs/memory/auth/x.md", "# X\n") - // Non-git dir → per-file fallback also degrades to "". - if got := d.lookup(nonGit, filepath.Join(nonGit, "docs/memory/auth/x.md")); got != "" { - t.Errorf("nil-receiver lookup = %q, want \"\" via per-file fallback", got) - } if loadGitDates(nonGit) != nil { t.Error("loadGitDates should return nil outside a git repo") } diff --git a/src/kit/VERSION b/src/kit/VERSION index 338a5b5d..24ba9a38 100644 --- a/src/kit/VERSION +++ b/src/kit/VERSION @@ -1 +1 @@ -2.6.6 +2.7.0 diff --git a/src/kit/migrations/2.6.6-to-2.7.0.md b/src/kit/migrations/2.6.6-to-2.7.0.md new file mode 100644 index 00000000..3a16e880 --- /dev/null +++ b/src/kit/migrations/2.6.6-to-2.7.0.md @@ -0,0 +1,120 @@ +# Migration: 2.6.6 to 2.7.0 + +## Summary + +Re-baseline every `docs/memory/**/index.md` onto the **two-column** domain-index +form. As of 2.7.0 (260625-ugde), `fab memory-index` no longer renders a third +`Last Updated` column on domain / sub-domain indexes — the index is now a pure +function of content (file names + descriptions + structure), with no git dates. +The old date cell was a **live `git log` projection**, which is HEAD/branch-relative: +a branch cut from (or not rebased onto) a newer `main` projected an *older* date than +`main`'s committed index, so concurrent PRs churned the cells back and forth on merge +(the loom PR #1846 "lots of date-only changes" symptom). Dropping the column makes the +index genuinely branch-independent and idempotent (Constitution III). No capability is +lost: dated, change-attributed history already lives in each folder's freeze-on-write +`log.md`; the index's job is pure navigation. See `docs/specs/fkf.md` §5. + +Existing projects carry domain / sub-domain `index.md` files generated under the +**old** (three-column) renderer. The fix is a **one-time re-baseline**: run +`fab memory-index` once with the new binary to rewrite every `index.md` to the +two-column form, then commit it. That re-baseline commit is the **last** churn the +repo sees from the date column; every run afterward is byte-stable. + +**This migration IS the fix for existing repos** — there is no separate manual step. + +This migration ships **no `.status.yaml` schema change** and **no `fab/` data +change**; it only regenerates `docs/memory/` `index.md` files (and, as `fab memory-index` +always does, append-only `log.md` files — which do not change shape) and commits the +result. A project with no `docs/memory/` directory skips it entirely. It is +**idempotent**: re-running `fab memory-index` + commit on an already two-column tree is +a no-op diff. + +## Pre-check + +> **Upgrade ordering (MUST be respected).** The new **binary** lands first +> (`brew upgrade fab-kit`), *then* `/fab-setup migrations` applies this migration. +> Applying it with an older binary would re-write the indexes back to three columns, +> so the pre-check below verifies the running binary emits two-column output and +> **aborts before rewriting anything** if it does not. + +1. **Binary capability probe — confirm two-column output.** Verify the running `fab` + binary renders the new two-column domain index (added in 2.7.0) **before** touching + the real tree. Probe in a throwaway temp project so nothing in `docs/memory/` is + mutated by the probe: + + ```sh + probe="$(mktemp -d)" + mkdir -p "$probe/fab/project" "$probe/docs/memory/probe" + : > "$probe/fab/project/config.yaml" + : > "$probe/fab/project/constitution.md" + printf '%s\n' '---' 'description: "probe"' '---' '# Probe' > "$probe/docs/memory/probe/x.md" + ( cd "$probe" && fab memory-index >/dev/null 2>&1 ) + if grep -q 'Last Updated' "$probe/docs/memory/probe/index.md" 2>/dev/null; then + echo 'Aborted: this migration needs fab >= 2.7.0 (the two-column memory index). Upgrade the binary first: brew upgrade fab-kit.' + rm -rf "$probe" + exit 1 + fi + rm -rf "$probe" + ``` + + If the generated probe index still contains a `Last Updated` header, this is an old + binary — **abort** with no rewrite of the real tree. Print: + `Aborted: this migration needs fab >= 2.7.0 (the two-column memory index). Upgrade the binary first: brew upgrade fab-kit.` + (If the probe `index.md` is absent — e.g. the binary errored — treat it the same way: + abort rather than rewrite blindly.) + +2. **Memory presence.** Confirm `docs/memory/` exists at the project root — if not, + this project has no memory tree and the migration is a complete no-op. Print: + `Skipped: docs/memory/ not present.` + +## Changes + +### 1. Re-baseline the indexes to two columns + +Run the new renderer once over the real tree: + +```sh +fab memory-index +``` + +This rewrites every domain / sub-domain `index.md` to the two-column +`| File | Description |` form (the root `index.md` was already two-column and is +unchanged in shape), reading folder contents + each file's `description:` frontmatter. +`fab memory-index` is byte-stable, so only the files whose shape actually changes are +rewritten. The result is the **last** churn the repo sees from the date column; every +run afterward is byte-stable. + +### 2. Commit the baseline + +Stage and commit the regenerated `docs/memory/` tree: + +```sh +git add docs/memory/ +git commit -m "chore: re-baseline memory indexes onto two-column form (2.7.0)" +``` + +The re-baseline commit is itself a **one-time, intentional churn** (it rewrites the +currently three-column `index.md` files into the two-column form). That commit is the +*last* churn; from there every `fab memory-index` run is byte-stable. + +### 3. Bump the kit VERSION + +`src/kit/VERSION` (the kit copy) reads `2.7.0`. + +## Verification + +1. The running binary emits two columns: the Pre-check probe produced a + `docs/memory/probe/index.md` whose domain table is `| File | Description |` with no + `Last Updated` header. (On an old binary the pre-check aborts here with the + upgrade-first message and rewrites nothing.) +2. After `fab memory-index` + commit, no domain / sub-domain `index.md` under + `docs/memory/` contains a `Last Updated` column header. (Frozen `log.md` / + `log.seed.md` historical entries that mention the old format are append-only + artifacts and are NOT touched.) +3. A subsequent plain `fab memory-index` run is **byte-stable**: it reports + `Memory indexes already up to date.` and produces a no-op diff. +4. `fab memory-index --check` exits **0 or 1** (never 2 — a re-baselined tree is + provably never destructive-loss); the `--check` exit-code contract is unchanged. +5. Re-running this migration on the same project is a complete no-op: re-running + `fab memory-index` + commit on an already two-column tree produces a no-op diff + (nothing to commit), and the two-column pre-check still passes. diff --git a/src/kit/reference/fkf.md b/src/kit/reference/fkf.md index 64fb3ae9..fa14c374 100644 --- a/src/kit/reference/fkf.md +++ b/src/kit/reference/fkf.md @@ -34,8 +34,8 @@ A `docs/memory/` tree conforms to **FKF v0.1** if all of the following hold: Items 1–2 are the OKF conformance floor (specialized: `type` is fixed, `description` is promoted to required). Items 3–4 are FKF's added strictness. As in OKF, consumers SHOULD degrade -gracefully — a missing optional body section, an unknown extra frontmatter key, or a stale -"Last Updated" cell does not make a file non-conforming. +gracefully — a missing optional body section or an unknown extra frontmatter key does not make a +file non-conforming. --- @@ -130,8 +130,8 @@ unknown frontmatter keys on round-trip and MUST NOT reject a file for carrying t Every directory holding ≥1 non-index `.md` carries a generated `index.md`. **All index tiers are generated artifacts written solely by `fab memory-index`** — agents never hand-edit index rows. -The render is a pure function of (folder contents + each file's `description:` frontmatter + git -dates), so the output is **byte-stable / idempotent**: two branches cannot produce conflicting +The render is a pure function of folder contents + each file's `description:` frontmatter — so +the output is **byte-stable / idempotent**: two branches cannot produce conflicting hand-edits to the same row, and any residual textual conflict auto-resolves by re-running `fab memory-index` post-merge. @@ -143,10 +143,10 @@ Three tiers: - **Root** (`docs/memory/index.md`) — **domains-only**: `| Domain | Description |`. Each domain row's Description is read from that domain `index.md`'s `description:` frontmatter (round-tripped by the generator). No inlined per-file column (it silently drifts). -- **Domain** (`docs/memory/{domain}/index.md`) — file rows: `| File | Description | Last Updated |`. - Description from each topic file's frontmatter; "Last Updated" git-stamped (degrades to `—` when - uncommitted / in a worktree / shallow clone), never hand-stamped. Carries its own - `description:` frontmatter (the source for the root row). When sub-domains exist, appends a +- **Domain** (`docs/memory/{domain}/index.md`) — file rows: `| File | Description |`. + Description from each topic file's frontmatter. The index carries no dates — it is a pure + function of content, so it is branch-independent and idempotent (recency lives in `log.md`). + Carries its own `description:` frontmatter (the source for the root row). When sub-domains exist, appends a `## Sub-Domains` table (`| Sub-Domain | Description |`) — emitted **only when sub-domains exist**, so a flat domain index is byte-identical to a sub-domain-free one. - **Sub-domain** (`docs/memory/{domain}/{sub-domain}/index.md`) — same file-row contract as a @@ -178,8 +178,8 @@ tables that FKF removes from memory files (§3.3). `log.md` is assembled from **two sources**, neither of which any agent hand-edits: 1. **Git history**, keyed to the folder — the *when*, the *which file*, and the change ID. This is - a projection of `git log` (the same date source the index uses), so it is always accurate and - never conflicts. + a projection of `git log` (the sole consumer of the batched git pass, now that the index is + dateless), so it is always accurate and never conflicts. 2. **A per-change one-line summary** — the *what*, written **once** into the change's own `.status.yaml` `summary:` field (§6.3). Because each change touches only *its own* `.status.yaml`, the summary has **zero conflict surface**. diff --git a/src/kit/skills/_cli-fab.md b/src/kit/skills/_cli-fab.md index baae59bd..a78bce14 100644 --- a/src/kit/skills/_cli-fab.md +++ b/src/kit/skills/_cli-fab.md @@ -492,8 +492,9 @@ hand-edit them — the deterministic replacement for the hand-maintained index r `## Changelog` tables) that previously lived in the hydrate / `docs-reorg-memory` skill prose. Modeled on `fab pr-meta` (pure `RenderRoot`/`RenderDomain`/`RenderLog` + a `Gather` I/O orchestrator in `internal/memoryindex`), so the output is byte-for-byte stable across runs and -stops the per-row / per-changelog-row merge conflicts on the hot `description` / `Last Updated` -cells. It produces the generated half of the **FKF** format (Fab Knowledge Format — see +stops the per-row / per-changelog-row merge conflicts on the hot `description` cells. The index +is a pure function of content (no git dates), so it is branch-independent and idempotent. It +produces the generated half of the **FKF** format (Fab Knowledge Format — see `$(fab kit-path)/reference/fkf.md`): per-folder `log.md`, the `type: memory` round-trip mechanism, and the root-index `fkf_version` frontmatter. @@ -503,7 +504,7 @@ What it writes: beyond the generator's own output — FKF §8; no domain/sub-domain index carries it). The legacy inlined per-file "Memory Files" column is dropped (it silently drifts). Each domain row's Description is read from that domain `index.md`'s `description:` frontmatter. -- **Every `docs/memory/{domain}/index.md`** — file rows (`| File | Description | Last Updated |`) +- **Every `docs/memory/{domain}/index.md`** — file rows (`| File | Description |`) for each non-`index` `.md` file, plus a `description:` frontmatter line carrying the domain's curated one-liner (round-tripped so the root row survives regen). When the domain contains sub-domains, a `## Sub-Domains` table is appended referencing each (`[sub](sub/index.md)`) — @@ -570,14 +571,11 @@ What it writes: Data sourcing (all read by the command itself): - Each topic file's **H1** (first `# ` line) and **`description:` frontmatter** (via `internal/frontmatter`). A file with no `description:` renders `—` in that cell (never errors). -- **"Last Updated"** and the **`log.md` history** both come from ONE batched - `git log --date=short --name-status -- docs/memory` pass (newest-first). The index takes the - first date seen per path (equivalent to the old per-file `git log -1 --date=short --format=%ad - -- ` defaults, kept only as the per-file fallback when the batched call fails); the log - takes the full per-path commit list (date + subject + name-status) from the **same** pass — no - per-file `git log` spawns. "Last Updated" degrades to `—` when git records nothing for a file — - uncommitted file, worktree, shallow clone, squash/rebase, or git unavailable — mirroring how - `fab pr-meta` degrades on missing git/gh context; when the whole batched pass fails, **no +- The **`log.md` history** comes from ONE batched + `git log --date=short --name-status -- docs/memory` pass (newest-first): the log takes the + full per-path commit list (date + subject + name-status) — no per-file `git log` spawns. The + **index** consumes none of this — it carries no dates (a pure function of content), so the + batched pass now serves `log.md` only. When the whole batched pass fails, **no `log.md` is written** (the log surface degrades to absent, never an error). - The **`log.md` summary + change-id** are joined from two sources, neither hand-edited (FKF §6): each change's `.status.yaml` **`summary:`** field (the *what* — set via `fab status @@ -633,7 +631,7 @@ benign drift — see below): - **`0`** — clean: every index **and `log.md`** file is byte-identical to its regenerated form (no regen needed). - **`1`** — **benign drift**: regen would change content but destroy nothing (e.g. an *improved* - `description:`, a refreshed `Last Updated` date, a stale `log.md`, a `log.md` gaining merged + `description:`, a stale `log.md`, a `log.md` gaining merged `log.seed.md` entries, or absent/changed FKF frontmatter). This is the former "out of date" condition — existing consumers treating "non-zero = stale" still work unchanged. **All `log.md` and FKF-frontmatter drift is benign (tier 1)** — a `log.md` is a C-lite git projection (plus any diff --git a/src/kit/skills/docs-hydrate-memory.md b/src/kit/skills/docs-hydrate-memory.md index f54f3d51..6f11417e 100644 --- a/src/kit/skills/docs-hydrate-memory.md +++ b/src/kit/skills/docs-hydrate-memory.md @@ -34,7 +34,7 @@ Mode is determined automatically by argument type (ingest/generate) or by the ex ### Index Ownership -Index files (`index.md` at the root, domain, and sub-domain tiers) are **generated artifacts** — `fab memory-index` is their single writer. The one hand-curated field is the `description:` frontmatter (on topic files and on domain/sub-domain indexes). When a new domain or sub-domain is created, its `index.md` **stub** — only the `description:` frontmatter one-liner, nothing else — is created **before** `fab memory-index` runs; the command fills in the generated body and round-trips the description. Never hand-edit generated index rows or "Last Updated" cells. Both modes below follow this model. +Index files (`index.md` at the root, domain, and sub-domain tiers) are **generated artifacts** — `fab memory-index` is their single writer. The one hand-curated field is the `description:` frontmatter (on topic files and on domain/sub-domain indexes). When a new domain or sub-domain is created, its `index.md` **stub** — only the `description:` frontmatter one-liner, nothing else — is created **before** `fab memory-index` runs; the command fills in the generated body and round-trips the description. Never hand-edit generated index rows. Both modes below follow this model. > **Refuse-before-regen guard (destructive-loss).** Before any `fab memory-index` regeneration step below, consult `fab memory-index --check`: on **exit 2** (destructive loss — a curated description would regenerate to `—`, a tombstone row would drop, or a custom grouping would flatten), **refuse to regenerate** and surface the pointer `→ run /docs-reorg-memory to remediate (it relocates removal-history rows to _shared/removed-domains.md and backfills description: frontmatter via /docs-hydrate-memory) before regenerating.` (`/docs-reorg-memory` is the orchestrator for all three tier-2 categories — it relocates tombstone rows itself and dispatches *this* skill's backfill mode; backfill alone does NOT relocate tombstones.) **No-op on born-compatible fab-kit trees** (always exit 0/1, never 2 — not dead code); it fires only on a pre-fab-kit tree reached via ingest/generate before backfill. Backfill mode itself only adds frontmatter, never destroys, so by the time *it* regenerates the guard is already a no-op. @@ -110,7 +110,7 @@ For each topic: ### Step 4: Regenerate Indexes (`fab memory-index`) -Run `fab memory-index` once to regenerate the root (domains-only), every domain index, and every sub-domain index from folder contents + `description:` frontmatter + git dates (the single writer — see Index Ownership; never hand-edit rows or "Last Updated" cells). Any non-fatal shape warnings it prints to stderr are advisory (over-wide / over-deep folders). +Run `fab memory-index` once to regenerate the root (domains-only), every domain index, and every sub-domain index from folder contents + `description:` frontmatter (the single writer — see Index Ownership; never hand-edit rows). The index carries no dates — it is a pure function of content. Any non-fatal shape warnings it prints to stderr are advisory (over-wide / over-deep folders). --- @@ -161,7 +161,7 @@ Mark ambiguous inferences with `[INFERRED]` inline near the relevant requirement ### Step 4: Regenerate Indexes -Same as ingest mode Step 4 — run `fab memory-index` to regenerate the root (domains-only), domain, and sub-domain indexes from folder contents + frontmatter + git dates. Do not hand-edit index rows. +Same as ingest mode Step 4 — run `fab memory-index` to regenerate the root (domains-only), domain, and sub-domain indexes from folder contents + frontmatter. Do not hand-edit index rows. --- @@ -193,7 +193,7 @@ For any domain/sub-domain folder lacking an `index.md` (or whose `index.md` lack Backfill is **caller-aware** about `fab memory-index`: - **Dispatched by `/docs-reorg-memory`** (the dispatch prompt carries the reorg-dispatched / defer-regen signal): do **NOT** run `fab memory-index`. reorg runs it exactly once at the end of its orchestration (after rebalance), so a regen here would be redundant work and would race reorg's single regen. -- **Invoked directly by a user** (no reorg signal): run `fab memory-index` as the final step, exactly like ingest and generate modes — root (domains-only) + every domain + every sub-domain index, regenerated from folder contents + frontmatter + git dates. +- **Invoked directly by a user** (no reorg signal): run `fab memory-index` as the final step, exactly like ingest and generate modes — root (domains-only) + every domain + every sub-domain index, regenerated from folder contents + frontmatter. --- diff --git a/src/kit/skills/fab-continue.md b/src/kit/skills/fab-continue.md index 3d5a5411..1fcacfb3 100644 --- a/src/kit/skills/fab-continue.md +++ b/src/kit/skills/fab-continue.md @@ -206,7 +206,7 @@ The applying agent triages review comments by priority — not all comments need - **No per-file `## Changelog`**: memory files no longer carry a `## Changelog` section (FKF §3.3) — instead, record what changed once via `fab status set-summary {change} ""` (the C-lite `summary:` source line, FKF §6.3, authored once at hydrate; `fab memory-index` joins it with git history to generate the per-folder `log.md`). - **Bundle-relative cross-links**: any memory↔memory link you write MUST use the bundle-relative `/...` form (resolved from `docs/memory/`, FKF §7); links *out* of the bundle (to source, specs, URLs) stay repo-relative/absolute-URL. - **Merge without duplication**: before appending to a target memory file, check it for an existing entry referencing this change (by change name) and update that entry in place instead of appending a duplicate — the same "replaced in place (not duplicated)" contract as `docs-hydrate-memory.md` and `_review.md`'s `## Deletion Candidates`. - - **Regenerate indexes**: run `fab memory-index` to regenerate the root (domains-only), domain, and sub-domain indexes — never hand-edit index rows or "Last Updated" cells. + - **Regenerate indexes**: run `fab memory-index` to regenerate the root (domains-only), domain, and sub-domain indexes — never hand-edit index rows. - **Refuse-before-regen guard (defense-in-depth)**: before that regen, consult `fab memory-index --check`; on **exit 2** (destructive loss) refuse to regenerate and surface the pointer `→ run /docs-reorg-memory to remediate ...` (the orchestrator that relocates tombstone rows and dispatches `/docs-hydrate-memory` backfill mode for descriptions; backfill alone does not relocate tombstones). This guard is a **no-op for born-compatible fab-kit trees** — a tree hydrated by the pipeline is always exit 0/1, never 2, so the guard never fires here (do not mistake it for dead code or remove it); it is defense-in-depth for the pathological case of a pre-fab-kit tree reaching the pipeline's hydrate stage. - **Shape SHOULD guidance**: aim for ~5–12 files/folder, depth ≤3, introduce a sub-domain only for a cohesive ≥8-file cluster; `_shared/` and `_unsorted/` are width-exempt. Heed any non-fatal shape warnings `fab memory-index` prints (advisory only). 5. Return completion status — the sequencer runs `fab status finish hydrate fab-continue` after the block returns (the block runs no `fab status` command) diff --git a/src/kit/skills/git-pr.md b/src/kit/skills/git-pr.md index 373e267d..de89b9a2 100644 --- a/src/kit/skills/git-pr.md +++ b/src/kit/skills/git-pr.md @@ -206,11 +206,11 @@ fi 1. Run `fab memory-index` (byte-stable; writes only `docs/memory/` index + log files; a no-op when nothing drifted). 2. If `docs/memory/` changed (`git diff --quiet -- docs/memory` exits non-zero): `git add docs/memory`, then a **SEPARATE** follow-up commit `git commit -m "docs: refresh memory indexes"`. Do **NOT** use `--amend` — keep 3a's authored content commit intact; squash collapses the pair on merge anyway. 3. If `git diff --quiet -- docs/memory` exits 0 (nothing drifted): make **no** commit — the guard suppresses an empty follow-up commit (Constitution III idempotency). -4. If the regen OR the commit fails → report the error and STOP. The 3a content commit is already made and intact; a failed refresh leaves a benign stale-date index, recoverable by re-running `fab memory-index` — never a torn state. +4. If the regen OR the commit fails → report the error and STOP. The 3a content commit is already made and intact; a failed refresh leaves a benign stale `log.md`, recoverable by re-running `fab memory-index` — never a torn state. Print (ONLY when a follow-up commit was actually made): ` ✓ commit — "docs: refresh memory indexes"` -> **Why here, why gated.** This is the first moment `git log` reports the change's real commit date (`fab memory-index` stamps "Last Updated" from committed dates), so the step lives in **ship** not hydrate — hydrate is entirely pre-commit, so no in-hydrate regen can see the change's own commit. There is no push here; 3b pushes both commits together. When `/git-pr` runs standalone (`{has_fab}` false) this sub-step is a **silent no-op**. +> **Why here, why gated.** This is the first moment `git log` reports the change's own content commit (`log.md` is a freeze-on-write projection of committed history, so it must capture this change's entry *now*, while the commit is still reachable — pre-squash). The step lives in **ship** not hydrate — hydrate is entirely pre-commit, so no in-hydrate regen can see the change's own commit. The **index** no longer depends on commit timing (it carries no dates — pure content), so its regen half is a reliable no-op here; `log.md` is the sole reason 3a-bis remains. There is no push here; 3b pushes both commits together. When `/git-pr` runs standalone (`{has_fab}` false) this sub-step is a **silent no-op**. #### 3b. Push (if has_unpushed or just committed) From 0de726338f5ee526da9f284ba47724a903620cbf Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:31:03 +0530 Subject: [PATCH 2/5] Update ship status and record PR URL --- .../.history.jsonl | 1 + .../.status.yaml | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl index 3da38b61..6d6f2b57 100644 --- a/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl @@ -14,3 +14,4 @@ {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T08:54:48Z"} {"event":"review","result":"passed","ts":"2026-06-25T08:54:48Z"} {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:58:06Z"} +{"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T09:00:59Z"} diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml index e11406cf..5509a831 100644 --- a/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml @@ -9,8 +9,8 @@ progress: apply: done review: done hydrate: done - ship: active - review-pr: pending + ship: done + review-pr: active plan: generated: true task_count: 19 @@ -33,8 +33,10 @@ stage_metrics: apply: {started_at: "2026-06-25T08:36:53Z", driver: fab-fff, iterations: 3, completed_at: "2026-06-25T08:44:18Z"} review: {started_at: "2026-06-25T08:44:18Z", driver: fab-fff, iterations: 3, completed_at: "2026-06-25T08:54:48Z"} hydrate: {started_at: "2026-06-25T08:54:48Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:58:06Z"} - ship: {started_at: "2026-06-25T08:58:06Z", driver: fab-fff, iterations: 1} -prs: [] + ship: {started_at: "2026-06-25T08:58:06Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T09:00:59Z"} + review-pr: {started_at: "2026-06-25T09:00:59Z", driver: git-pr, iterations: 1} +prs: + - https://github.com/sahil87/fab-kit/pull/446 true_impact: added: 0 deleted: 0 @@ -51,4 +53,4 @@ true_impact: computed_at_stage: hydrate summary: drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T08:58:06Z +last_updated: 2026-06-25T09:00:59Z From 8a38066c38341468feee44e7545b1c3f0e1277e8 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:40:20 +0530 Subject: [PATCH 3/5] fix: address review feedback from @Copilot Co-Authored-By: Claude Opus 4.8 (1M context) --- src/go/fab/cmd/fab/memory_index.go | 6 +++--- src/kit/migrations/2.6.6-to-2.7.0.md | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/go/fab/cmd/fab/memory_index.go b/src/go/fab/cmd/fab/memory_index.go index 624ca239..3f384bb2 100644 --- a/src/go/fab/cmd/fab/memory_index.go +++ b/src/go/fab/cmd/fab/memory_index.go @@ -44,9 +44,9 @@ func memoryIndexCmd() *cobra.Command { "every log.md from current git (the pre-freeze behavior, opt-in) — for a " + "corrupted log or a deliberate re-baseline. With --check, writes nothing " + "and classifies drift by severity in the exit code: 0 = clean, 1 = benign " + - "drift (regen changes content but destroys nothing — an improved " + - "description:, all log.md / FKF " + - "frontmatter drift is benign; for log.md a benign FAIL means the committed " + + "drift (regen changes content but destroys nothing — e.g. an improved " + + "`description:`, or any log.md / FKF frontmatter drift; for log.md a " + + "benign FAIL means the committed " + "log is missing a projected attributable (file-base, change-id) entry, or a " + "frozen line was hand-edited render-unstably — a committed log that is a " + "valid SUPERSET of the freeze-on-write merge PASSES), 2 = destructive loss " + diff --git a/src/kit/migrations/2.6.6-to-2.7.0.md b/src/kit/migrations/2.6.6-to-2.7.0.md index 3a16e880..0cbec3d8 100644 --- a/src/kit/migrations/2.6.6-to-2.7.0.md +++ b/src/kit/migrations/2.6.6-to-2.7.0.md @@ -49,7 +49,12 @@ a no-op diff. : > "$probe/fab/project/constitution.md" printf '%s\n' '---' 'description: "probe"' '---' '# Probe' > "$probe/docs/memory/probe/x.md" ( cd "$probe" && fab memory-index >/dev/null 2>&1 ) - if grep -q 'Last Updated' "$probe/docs/memory/probe/index.md" 2>/dev/null; then + # Abort unless the probe produced a two-column index. Both failure modes are + # treated identically: the probe index is MISSING (the binary errored / wrote + # nothing) OR it still carries a `Last Updated` header (an old three-column + # binary). A bare `grep -q` would silently CONTINUE on a missing file (grep + # exits non-zero when the path is absent), so guard the absence explicitly. + if [ ! -f "$probe/docs/memory/probe/index.md" ] || grep -q 'Last Updated' "$probe/docs/memory/probe/index.md" 2>/dev/null; then echo 'Aborted: this migration needs fab >= 2.7.0 (the two-column memory index). Upgrade the binary first: brew upgrade fab-kit.' rm -rf "$probe" exit 1 From f032f27dc749297ba313ec59c4cec3003c7b9418 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:41:28 +0530 Subject: [PATCH 4/5] Update review-pr status Co-Authored-By: Claude Opus 4.8 (1M context) --- .../.history.jsonl | 1 + .../260625-ugde-memory-index-drop-date-column/.status.yaml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl index 6d6f2b57..de1a720a 100644 --- a/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.history.jsonl @@ -15,3 +15,4 @@ {"event":"review","result":"passed","ts":"2026-06-25T08:54:48Z"} {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:58:06Z"} {"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T09:00:59Z"} +{"event":"review","result":"passed","ts":"2026-06-25T09:10:57Z"} diff --git a/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml index 5509a831..031437b9 100644 --- a/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml +++ b/fab/changes/260625-ugde-memory-index-drop-date-column/.status.yaml @@ -10,7 +10,7 @@ progress: review: done hydrate: done ship: done - review-pr: active + review-pr: done plan: generated: true task_count: 19 @@ -34,7 +34,7 @@ stage_metrics: review: {started_at: "2026-06-25T08:44:18Z", driver: fab-fff, iterations: 3, completed_at: "2026-06-25T08:54:48Z"} hydrate: {started_at: "2026-06-25T08:54:48Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:58:06Z"} ship: {started_at: "2026-06-25T08:58:06Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T09:00:59Z"} - review-pr: {started_at: "2026-06-25T09:00:59Z", driver: git-pr, iterations: 1} + review-pr: {started_at: "2026-06-25T09:00:59Z", driver: git-pr, iterations: 1, completed_at: "2026-06-25T09:10:57Z"} prs: - https://github.com/sahil87/fab-kit/pull/446 true_impact: @@ -53,4 +53,4 @@ true_impact: computed_at_stage: hydrate summary: drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T09:00:59Z +last_updated: 2026-06-25T09:10:57Z From 2e737e96d6d20db9002c7a9561a569beee9a58a1 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 16:39:04 +0530 Subject: [PATCH 5/5] docs: refresh memory log.md with ugde C-lite entries (post-rebase) The rebase placed the change's own commit in history, so fab memory-index projects its per-folder log.md entries (the post-commit projection 3a-bis normally captures at ship). Generated with the 2.7.0 (2-column) binary. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/memory/distribution/log.md | 4 ++++ docs/memory/memory-docs/log.md | 5 +++++ docs/memory/pipeline/log.md | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/docs/memory/distribution/log.md b/docs/memory/distribution/log.md index 68d2cb3b..9171e16a 100644 --- a/docs/memory/distribution/log.md +++ b/docs/memory/distribution/log.md @@ -1,6 +1,10 @@ # Log — Distribution Documentation +## 2026-06-25 +- **Update** [kit-architecture](/distribution/kit-architecture.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) +- **Update** [migrations](/distribution/migrations.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) + ## 2026-06-16 - **Update** [distribution](/distribution/distribution.md) — refactor: FKF Change 4/4 — Migrate docs/memory/ Tree to FKF + FKF-Aware Reorg Skills (#425) - **Update** [kit-architecture](/distribution/kit-architecture.md) — refactor: Ship Memory-File Template in Kit Cache (#432) diff --git a/docs/memory/memory-docs/log.md b/docs/memory/memory-docs/log.md index 44a9b524..ca1a3da4 100644 --- a/docs/memory/memory-docs/log.md +++ b/docs/memory/memory-docs/log.md @@ -1,6 +1,11 @@ # Log — Memory Docs Documentation +## 2026-06-25 +- **Update** [hydrate](/memory-docs/hydrate.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) +- **Update** [hydrate-generate](/memory-docs/hydrate-generate.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) +- **Update** [templates](/memory-docs/templates.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) + ## 2026-06-16 - **Update** [hydrate](/memory-docs/hydrate.md) — fix: Freeze-on-write log.md generation to eliminate squash-merge churn (#434) - **Update** [hydrate](/memory-docs/hydrate.md) — refactor: FKF Change 4/4 — Migrate docs/memory/ Tree to FKF + FKF-Aware Reorg Skills (#425) diff --git a/docs/memory/pipeline/log.md b/docs/memory/pipeline/log.md index 87c36288..b163847f 100644 --- a/docs/memory/pipeline/log.md +++ b/docs/memory/pipeline/log.md @@ -1,6 +1,10 @@ # Log — Pipeline Documentation +## 2026-06-25 +- **Update** [execution-skills](/pipeline/execution-skills.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) +- **Update** [schemas](/pipeline/schemas.md) — drops the index "Last Updated" column so generated memory indexes are content-only and idempotent; dated history stays in log.md (ugde) + ## 2026-06-16 - **Update** [change-lifecycle](/pipeline/change-lifecycle.md) — refactor: FKF Change 4/4 — Migrate docs/memory/ Tree to FKF + FKF-Aware Reorg Skills (#425) - **Update** [clarify](/pipeline/clarify.md) — refactor: FKF Change 4/4 — Migrate docs/memory/ Tree to FKF + FKF-Aware Reorg Skills (#425)