@naswerks/engineering-loop 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "naswerks",
12
12
  "description": "The engineering loop: docs-* knowledge loop, spec-* execution loop, and init for a fresh repository.",
13
- "version": "0.4.0",
13
+ "version": "0.5.1",
14
14
  "source": "./"
15
15
  }
16
16
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naswerks",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "The engineering loop: a knowledge loop (docs-*) and an execution loop (spec-*) for repositories driven by hosted agent sessions, plus init to make a repository ready for both.",
5
5
  "author": { "name": "naswerks" },
6
6
  "license": "MIT",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ version is the one in `package.json` and `.claude-plugin/plugin.json`, which mus
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.1] - 2026-09-24
9
+
10
+ ### Fixed
11
+
12
+ - A doc on a curated menu meets the loop's standard when it names the code it describes in either form: a
13
+ `## Key Files` section, or a frontmatter `related_files` list. The doctor checked for the heading alone and
14
+ flagged docs that name their code in frontmatter.
15
+ - A patch release never asks a copy of the method docs to change, so `init doctor` and `init refresh` read a
16
+ stamp as behind only when its minor version is.
17
+
18
+ ## [0.5.0] - 2026-09-24
19
+
20
+ ### Added
21
+
22
+ - **The menu declares its shape.** `docs/_meta/doc-index.md` carries `<!-- naswerks-loop: menu=all -->` (it
23
+ lists every living doc: the repository has no other index; the default, and what a menu with no such line
24
+ means) or `<!-- naswerks-loop: menu=curated; index={path} -->` (the repository already indexes every doc in
25
+ `{path}`; the menu lists the docs its seats read). `docs-workflow.md` § The repository-owned sections
26
+ explains both, and the loop's standard for a doc on a curated menu: a header at a decided status and a
27
+ `## Key Files` section. `init` writes `menu=curated` when its scan finds an index the repository already
28
+ keeps; `docs-process` registers a new doc where the shape says; `docs-status` and `docs-audit-full` flag a
29
+ menu that has drifted; `init doctor` reads the shape, FAILs a doc the menu names that does not exist,
30
+ flags a living doc an all-menu misses, and holds every doc a curated menu names to the standard.
31
+ `pins/menu.test.mjs` holds both shapes.
32
+
33
+ ### Changed
34
+
35
+ - The map's Doc metadata row no longer asks whether the menu is kept by hand; the menu's shape line says
36
+ what the menu is.
37
+ - `docs-workflow.md` § The repository-owned sections no longer says which words this repository writes for
38
+ the loop's states; that is § Doc metadata's to say, so a repository that pastes the section verbatim no
39
+ longer inherits a line that is untrue for it.
40
+ - README: taking a new release is `claude plugin update`; the pins and the release path read as they are.
41
+
8
42
  ## [0.4.0] - 2026-09-23
9
43
 
10
44
  ### Changed
package/README.md CHANGED
@@ -20,7 +20,8 @@ claude plugin install naswerks@engineering-loop
20
20
 
21
21
  The marketplace is this repository, and the plugin is sourced from it: no registry sits between you and a
22
22
  release. Plugins load at session start, so start a new session (or run `/reload-plugins`) after installing.
23
- To pick up a new release later: `claude plugin marketplace update engineering-loop`, then the install line again.
23
+ To pick up a new release later: `claude plugin marketplace update engineering-loop`, then
24
+ `claude plugin update naswerks@engineering-loop` (an install over an existing install does nothing).
24
25
 
25
26
  Skills are namespaced by the plugin: `/naswerks:docs-write`, `/naswerks:spec-pipeline`, `/naswerks:init`.
26
27
 
@@ -48,6 +49,11 @@ sections lists what each must answer. One grep lists the four:
48
49
  rg -n '^#+ .*\(repository-owned\)' docs/_meta/
49
50
  ```
50
51
 
52
+ If the repository already keeps an index of every doc, declare the loop's menu curated — put
53
+ `<!-- naswerks-loop: menu=curated; index=docs/INDEX.md -->` (your index's path) in `docs/_meta/doc-index.md`
54
+ — and the menu lists only the docs your seats read, each held to the loop's standard; every other doc stays
55
+ in your own index. A repository with no index of its own keeps the default, a menu of every living doc.
56
+
51
57
  Every skill names the section it needs and never states the format itself, so the pack stays generic and
52
58
  your conventions live in your repository. **Taking a newer pack:** `/naswerks:init refresh` writes only
53
59
  those four sections and the version stamp into your copies — a dry run first, then `--write` on your word —
@@ -70,7 +76,7 @@ and never touches the rest of the text.
70
76
  | `spec-retro` | spec | the coordinator's close-out audit |
71
77
  | `spec-witness` | spec | a read-only verifier |
72
78
  | `spec-ignite` | spec | the ignition copilot for a hosted run |
73
- | `init` | — | make a repository ready for both loops |
79
+ | `init` | — | make a repository ready for both loops; `init doctor` checks one, `init refresh` takes a newer pack's repository-owned sections |
74
80
 
75
81
  `references/` carries the two method documents a repository copies into `docs/_meta/` and then
76
82
  rewrites where they say (repository-owned);
@@ -84,19 +90,26 @@ or the child may resolve `docs/` paths against the parent session's workspace.
84
90
 
85
91
  ## Does it work
86
92
 
87
- `npm test` runs the pins: one version across the package, the plugin manifest and the marketplace
88
- entry; every skill directory carries a `SKILL.md` named for its folder; no byte-order marks; the
89
- method documents carry the version stamp; the vocabulary censuses. `node bin/verify-staged.mjs
90
- --self-test` drives the receipt against a throwaway repository and reports each case.
93
+ `npm test` runs the pins: one version across the package, the plugin manifest, the marketplace entry and
94
+ both method documents' stamps; every skill directory carries a `SKILL.md` named for its folder; no
95
+ byte-order marks; the vocabulary and content censuses; no skill states a format a repository owns, and
96
+ every skill that needs one carries the same find-format line; exactly four repository-owned sections, found
97
+ by the one grep; the loop's own preferences stay stated in the method documents; the menu's two shapes; `init refresh` leaves
98
+ every line outside those sections byte for byte. `node bin/verify-staged.mjs --self-test` drives the
99
+ receipt against a throwaway repository and reports each case.
91
100
 
92
101
  ## Releasing
93
102
 
94
- The version lives in `package.json` and `.claude-plugin/plugin.json` (they must agree). A `v*` tag
95
- runs the pins and publishes to npm through trusted publishing; the GitHub release notes are the
96
- CHANGELOG section for that version. The marketplace serves the plugin from this repository at whatever
97
- `main` holds, so a CLI install never waits on npm; npm is the runner lane, and a refused publish leaves
98
- runners one version behind until the trusted publisher on npmjs.com matches this repository and
99
- `release.yml` (the workflow prints that remedy when it happens).
103
+ The version lives in `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`
104
+ (they must agree), in both method documents' stamps, and in a CHANGELOG section. The marketplace serves
105
+ the plugin from this repository at whatever `main` holds, so a CLI install never waits on npm; npm is the
106
+ runner lane.
107
+
108
+ To release: push the release commit to `main`, publish it to npm from the repository root (`npm login`,
109
+ then `npm publish --access public`), then push its `v*` tag. The tag's workflow runs the pins, finds the
110
+ version already on the registry and skips its own publish, and creates the GitHub release from the
111
+ CHANGELOG section. With a trusted publisher linked on npmjs.com for this repository and `release.yml`,
112
+ pushing the tag alone publishes; the workflow prints that remedy when a publish is refused.
100
113
 
101
114
  ## License
102
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naswerks/engineering-loop",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "The engineering loop as a Claude Code plugin: a knowledge loop (docs-*), an execution loop (spec-*), and init for a fresh repository. The package root is the plugin root.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- <!-- naswerks-loop: version=0.4.0 -->
1
+ <!-- naswerks-loop: version=0.5.1 -->
2
2
  # Docs Workflow
3
3
 
4
4
  > How knowledge flows through `docs/`: the loop, the folder rules, the templates, and the `docs-*` skills.
@@ -84,12 +84,12 @@ that replaces the section rewrites alongside it):
84
84
  | Section | What the skills read from it | Also echoed in |
85
85
  |---------|------------------------------|----------------|
86
86
  | **The archive convention** (this file) | the archive home for each kind of effort (hand-cranked, spec pipeline, follow-on round) and for audit reports · the shape inside it · what a working doc's `## Archive` line says, and whether an umbrella carries an `Archive:` line · the links a move repairs · what is frozen once filed | The Loop (the diagram and its prose) · Folders (`research/`, `working/`, `archive/`) · "Living docs vs session docs" · the skills table (`docs-process`) · Conventions (incident receipts, archived docs) · `engineering-loop.md` "The loop at a glance" |
87
- | **Doc metadata** (this file) | the header — its form and its place · the sweep: one command, one line per doc · the word for each loop state (below) · where the verified date lives, and the rule that moves it · whether `## Lineage` stays · the gates a doc passes, and the folders they exempt · whether `doc-index.md` is kept by hand or generated | the skills table (`docs-status`) · Conventions (the machine layer; dates in the header) · `engineering-loop.md`'s own header line and its `## Lineage` |
87
+ | **Doc metadata** (this file) | the header — its form and its place · the sweep: one command, one line per doc · the word for each loop state (below) · where the verified date lives, and the rule that moves it · whether `## Lineage` stays · the gates a doc passes, and the folders they exempt | the skills table (`docs-status`) · Conventions (the machine layer; dates in the header) · `engineering-loop.md`'s own header line and its `## Lineage` |
88
88
  | **Templates** (this file) | each template's header lines, at the state its writer names | — |
89
89
  | **This repository's rails** (`engineering-loop.md`) | the policy document, and how a seat reaches it · the gates a commit passes · the suite before a PR · the base branch, the PR target, the branch names and how a PR is opened · the staging rule · the repository's own skills a seat meets | the POLICY row of "Where a rule lives" · the Hard rules, which the rails add to and never subtract from |
90
90
 
91
91
  **The loop's states.** The skills speak in four states, and § Doc metadata says which word this repository
92
- writes for each. Here the words are the states themselves.
92
+ writes for each.
93
93
 
94
94
  | State | Written by | Means |
95
95
  |-------|------------|-------|
@@ -100,8 +100,22 @@ writes for each. Here the words are the states themselves.
100
100
 
101
101
  *Stale* is a date, not a state: `docs-status` computes it from the verified date, and no seat writes it.
102
102
 
103
- **Silence.** A section that says nothing about an item leaves the loop's default: the menu is kept by hand
104
- (`docs-process` adds a row for a new feature doc), no gate runs on a commit, the base branch is the remote's
103
+ **The menu.** `doc-index.md` is the menu the seats read, and the whole file is the repository's: `init` writes
104
+ it once and never again. A line near its top says which of two shapes it has, and one grep reads it:
105
+ `rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`.
106
+
107
+ | Shape line | The menu lists | A new living doc goes |
108
+ |------------|----------------|-----------------------|
109
+ | `<!-- naswerks-loop: menu=all -->` | every living doc — the repository has no other index | onto the menu |
110
+ | `<!-- naswerks-loop: menu=curated; index={path} -->` | the docs this repository's seats read; `{path}` indexes every doc | into `{path}`, the way that index is kept, and onto the menu only when it meets the loop's standard and a seat should read it |
111
+
112
+ **The loop's standard for a doc on a curated menu:** it exists, it carries the header § Doc metadata
113
+ prescribes at a decided status (not the word for `draft`), and it names the code it describes — a
114
+ `## Key Files` section, or a frontmatter `related_files` list. `init doctor` checks exactly that, for every doc
115
+ a curated menu names.
116
+
117
+ **Silence.** A section that says nothing about an item leaves the loop's default: a menu with no shape line
118
+ lists every living doc (`menu=all`), no gate runs on a commit, the base branch is the remote's
105
119
  default (`git symbolic-ref refs/remotes/origin/HEAD`), and a seat with no PR route asks the human once and
106
120
  suggests recording the answer in the rails. Any other unanswered item is a question for the human, never a
107
121
  guess.
@@ -131,8 +145,12 @@ Keep what the skills need to find, change the words to yours:
131
145
  `spec-pipeline` then records the resolved path in the umbrella's blockquote as an `Archive:` line.
132
146
  - **Your rails.** Fill each item of `engineering-loop.md` § This repository's rails from what the
133
147
  repository already wrote down — a policy document, hooks, CI, a pull-request template.
148
+ - **Your menu.** A repository that already indexes its docs declares its menu curated and names that index:
149
+ the menu then lists only the docs its seats read, each meeting the loop's standard, and every other doc
150
+ stays in the repository's own index, kept the way it already is.
134
151
 
135
- **The version stamp** on line 1 names the pack version whose repository-owned sections this copy carries.
152
+ **The version stamp** on line 1 names the pack version whose repository-owned sections this copy carries. A
153
+ patch release never asks a copy to change, so a stamp is behind only when its minor version is.
136
154
  The loop text around them is this repository's copy from whenever it was taken: `init doctor` never grades
137
155
  it, and `init refresh` never touches it — refresh adds a section or a rails item a newer pack reads, and
138
156
  restamps.
@@ -1,4 +1,4 @@
1
- <!-- naswerks-loop: version=0.4.0 -->
1
+ <!-- naswerks-loop: version=0.5.1 -->
2
2
  # The Engineering Loop — flow, seats, skills, and the ceremony
3
3
 
4
4
  > The quick intro to how work gets built with this loop: the two loops (`docs-*` knowledge, `spec-*`
@@ -18,7 +18,10 @@ what to audit next.
18
18
  oldest-first.
19
19
 
20
20
  2. **Feature completeness** — quick `docs/features/` folder pass (per `docs-audit-feature`'s
21
- folder mode): any features in code with no doc, or docs whose feature is gone.
21
+ folder mode): any features in code with no doc, or docs whose feature is gone. Then the menu against its
22
+ shape line (`rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`; none means `menu=all`): on
23
+ `menu=all`, living docs it does not name; on `menu=curated`, docs it names that fall short of the loop's
24
+ standard (`docs/_meta/docs-workflow.md` § The repository-owned sections).
22
25
 
23
26
  3. **Convention spot-check** — sample 2-3 source files per major convention against the
24
27
  `docs/patterns/` docs; note drift.
@@ -92,9 +92,13 @@ branch, which is the truth for that PR, and you say so at close.
92
92
  speculation. A sizable design idea goes to `docs-backlog` (research/) instead. `docs-status`
93
93
  surfaces these so they aren't lost.
94
94
 
95
- 3. **New feature?** If the work added a feature, ensure it has a doc in `docs/features/` (one doc per
96
- feature) and add it to the menu in `docs/_meta/doc-index.md` — unless `docs-workflow.md` § Doc metadata
97
- (repository-owned) says a repository tool generates the menu, in which case leave it alone.
95
+ 3. **A new living doc?** If the work added a feature, ensure it has a doc in `docs/features/` (one doc per
96
+ feature). Then register every living doc this run created where the menu's shape line says
97
+ (`rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`; `docs/_meta/docs-workflow.md` § The
98
+ repository-owned sections explains the two shapes). **`menu=all`, or no shape line:** add its row to the
99
+ menu in `docs/_meta/doc-index.md`. **`menu=curated; index={path}`:** add it to `{path}` the way that
100
+ index is kept — a generated index is never edited by hand — and add a menu row only when the doc meets
101
+ the loop's standard and a seat should read it; say which you did in the summary.
98
102
 
99
103
  4. **Closing grep** — if the session doc notes deletions or renames, grep all living
100
104
  docs (`guides/ patterns/ infrastructure/ features/ _meta/`) for stale references and fix them. Do
@@ -36,6 +36,11 @@ just counts.
36
36
  broken breadcrumb).
37
37
  - **low-lineage feature** — where the section keeps `## Lineage`: a feature doc with no Lineage rows =
38
38
  ungrounded; worth wiring its lineage.
39
+ - **menu drift** — read the menu's shape line (`rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`;
40
+ none means `menu=all`). On `menu=all`: a living doc the menu does not name. On `menu=curated`: a doc the
41
+ menu names that falls short of the loop's standard (`docs/_meta/docs-workflow.md` § The
42
+ repository-owned sections — a header at a decided status, and its code named in a `## Key Files`
43
+ section or a frontmatter `related_files` list).
39
44
 
40
45
  5. **Open issues.** Grep living docs for an `## Open Issues` section; list which docs/features carry
41
46
  unresolved, code-verified problems (and how many) so known risks surface at orientation instead of
@@ -38,7 +38,8 @@ docs/
38
38
  _meta/docs-workflow.md a copy of the pack's references/docs-workflow.md (stamped); its sections marked
39
39
  (repository-owned) are the repository's to rewrite to its own conventions
40
40
  _meta/engineering-loop.md a copy of references/engineering-loop.md (stamped), under the same rule
41
- _meta/doc-index.md the menu, written LAST — every doc that exists + three repo slots:
41
+ _meta/doc-index.md the menu, written LAST — its shape line (menu=all: every doc that exists;
42
+ menu=curated: the docs init drafted, when the repository indexes its own) + three repo slots:
42
43
  ## Role reading lists · ## Apps and their build commands · ## The evidence tools
43
44
  research/ working/ archive/ guides/ patterns/ infrastructure/ features/ each with a README.md
44
45
  patterns/code-organization.md patterns/backend-patterns.md
@@ -104,7 +105,9 @@ what it says` — and cite from it. A draft written mid-scan is a draft written
104
105
  analyzer configs (`.stylecop`, `.globalconfig`, `Directory.Build.props` rules such as
105
106
  `TreatWarningsAsErrors`, `Nullable`, banned-API lists) · `tsconfig*.json` strictness ·
106
107
  `BannedSymbols.txt`, `FLAKY-TESTS.md` · CODEOWNERS · `.husky/`, `.githooks/` · PR templates · the
107
- existing `docs/` tree · `.claude/settings.json`.
108
+ existing `docs/` tree · `.claude/settings.json` · an index the repository already keeps of every doc (a
109
+ `docs/INDEX.md` or `docs/index.md` that lists them, a `docs/SUMMARY.md`, a `mkdocs.yml` nav, a
110
+ docusaurus or vitepress sidebar).
108
111
 
109
112
  ## The write order — create-if-missing at every step
110
113
 
@@ -134,8 +137,12 @@ not. `init` never overwrites, never reorders, never deletes.
134
137
  Evidence per concern: a `generated/` folder or codegen config; a state library; a UI library; a tokens
135
138
  file or token layer; a saga / workflow / state-machine mechanism; a realtime library on either side; a
136
139
  queue, worker or scheduler.
137
- 5. `docs/_meta/doc-index.md` from its template, LAST, listing every doc that now exists under the four
138
- living folders (drafted or repo-owned), the three repo slots filled from the scan.
140
+ 5. `docs/_meta/doc-index.md` from its template, LAST, with its shape line (`docs/_meta/docs-workflow.md`
141
+ § The repository-owned sections, the menu). **`menu=all`** when the repository keeps no other index: the
142
+ menu lists every doc that now exists under the four living folders (drafted or repo-owned). **`menu=curated;
143
+ index={path}`** when pass 4 found an index the repository already keeps of every doc: the menu lists only
144
+ the docs `init` drafted, and the report says the owner adds each further doc that meets the loop's
145
+ standard. Either way, the three repo slots are filled from the scan.
139
146
  6. The Start-here stanza (`templates/claude-md-stanza.md`) appended to `CLAUDE.md` (created if absent)
140
147
  and to `AGENTS.md` (only if present); skipped when the opening marker
141
148
  `<!-- naswerks-loop:start-here -->` is already there.
@@ -183,13 +190,15 @@ FAIL, so a CI job or a control plane can call it.
183
190
  | Row | FAIL | DEGRADE | ok |
184
191
  |---|---|---|---|
185
192
  | each of the eight folders | missing | README missing | present |
186
- | `_meta/docs-workflow.md`, `_meta/engineering-loop.md` | missing, or no stamp on line 1 (or on the first line after a frontmatter block) | stamp behind the pack's version (`init refresh` restamps) | stamp equals it |
193
+ | `_meta/docs-workflow.md`, `_meta/engineering-loop.md` | missing, or no stamp on line 1 (or on the first line after a frontmatter block) | stamp's minor version behind the pack's (`init refresh` restamps; a patch never asks a copy to change) | stamp's minor equals the pack's |
187
194
  | each repository-owned heading, in its file | — | unmarked (an earlier spelling) or missing — `init refresh` marks or adds it | present |
188
195
  | each item of § This repository's rails (repository-owned) | — | missing, or still its absent-line | recorded |
189
196
  | `_meta/doc-index.md` and its three slot sections | missing | a slot carries only the absent-line | filled |
197
+ | the menu's shape line | an unknown shape | a curated menu names no index, or an index that does not exist | `menu=all` (also a menu with no shape line), or `menu=curated` with its index |
198
+ | every living doc a `menu=all` menu should list | — | a doc under the four living folders the menu does not name | named |
199
+ | every doc a menu names | the path does not exist (a row saying `not detected` is exempt) | on a curated menu: no status in either header form, the word `draft`, or no named code (a `## Key Files` section or a frontmatter `related_files` list) | meets the loop's standard |
190
200
  | the four floor docs | missing | the word `draft` (read from either header form), or an `<!-- init: -->` left in | a decided status |
191
201
  | the seven standard docs | absent AND no doc-index row saying `not detected` | the word `draft` when present; absent with its row | present and decided, or absent with its row |
192
- | every path a § Role reading lists row names | the row's path does not exist | — | resolves |
193
202
  | `CLAUDE.md` / `AGENTS.md` stanza · `.claude/settings.json` pin | — | missing | present |
194
203
  | `verify-staged` | — | not on PATH (the row carries the fallback) | resolvable |
195
204
  | every `docs/research/*/00-ignition-brief.md` | the five problems the control plane's parser emits (no `## The kick`; the kick has no blockquote; no `## The sequence`; the sequence holds no table; a row names no kind from `coordinator build fixit review retro witness docs-process`), or a spec a row names is missing from disk | — | clean |
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
5
  import { join, resolve, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { execFileSync } from 'node:child_process';
8
- import { OWNED, compareVersions, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
8
+ import { OWNED, compareVersions, compareMinor, frontmatterListCount, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
9
9
 
10
10
  const repo = resolve(process.argv[2] || process.cwd());
11
11
  const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
@@ -40,9 +40,8 @@ for (const name of ['docs-workflow.md', 'engineering-loop.md']) {
40
40
  const m = STAMP.exec(firstBodyLine(text));
41
41
  if (!m) { row('FAIL', `docs/_meta/${name}`, 'no naswerks-loop stamp on line 1 (or on the first line after a frontmatter block) — an unstamped copy cannot be told from a stale one'); continue; }
42
42
  if (!packVersion) { row('DEGRADE', `docs/_meta/${name}`, `stamped ${m[1]}; the pack's own version could not be read`); continue; }
43
- const cmp = compareVersions(m[1], packVersion);
44
- if (cmp < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} — run init refresh: it adds this version's repository-owned sections and restamps, and never touches the loop text`);
45
- else row('ok', `docs/_meta/${name}`, `stamp ${m[1]}`);
43
+ if (compareMinor(m[1], packVersion) < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} — run init refresh: it adds this version's repository-owned sections and restamps, and never touches the loop text`);
44
+ else row('ok', `docs/_meta/${name}`, compareVersions(m[1], packVersion) < 0 ? `stamp ${m[1]} (the pack is ${packVersion}: a patch never asks a copy to change)` : `stamp ${m[1]}`);
46
45
 
47
46
  // ── 2b. the repository-owned sections: present under the heading a skill looks them up by ─────────
48
47
  const lines = linesOf(text);
@@ -118,13 +117,46 @@ function docRow(relPath, required) {
118
117
  if (/<!-- init:/.test(text)) row('DEGRADE', `docs/${relPath}`, 'an <!-- init: --> instruction was left in the file');
119
118
  }
120
119
 
121
- // ── 4b. every path a Role reading lists row names resolves ─────────────────────────────────────
122
- if (roleRows !== null) {
123
- for (const line of roleRows.split('\n').filter((l) => l.trim().startsWith('|') && !/^\|\s*-/.test(l.trim()) && !/^\|\s*Doc\s*\|/i.test(l.trim()))) {
124
- for (const m of line.matchAll(/`((?:patterns|infrastructure|guides|features)\/[A-Za-z0-9_.-]+\.md)`|\]\((?:\.\.\/)?((?:patterns|infrastructure|guides|features)\/[A-Za-z0-9_.-]+\.md)\)/g)) {
125
- const p = m[1] || m[2];
126
- if (/not detected/i.test(line)) continue;
127
- row(existsSync(join(repo, 'docs', p)) ? 'ok' : 'FAIL', `doc-index row: docs/${p}`, existsSync(join(repo, 'docs', p)) ? '' : 'the row names a path that does not exist');
120
+ // ── 4b. the menu: its shape, and every doc it names ────────────────────────────────────────────
121
+ // doc-index.md declares one of two shapes (docs-workflow.md § The repository-owned sections): menu=all lists
122
+ // every living doc; menu=curated lists the docs the seats read and names the index that lists every doc. A
123
+ // menu with no shape line is menu=all. Every doc a menu names must exist (a row saying `not detected` is
124
+ // exempt); a curated menu's docs must also meet the loop's standard: a status in either header form, not
125
+ // draft, and names the code it describes: a `## Key Files` section, or a frontmatter `related_files` list.
126
+ {
127
+ const menu = read(join(repo, 'docs', '_meta', 'doc-index.md'));
128
+ if (menu !== null) {
129
+ const line = /<!-- naswerks-loop: menu=([A-Za-z-]+)(?:;\s*index=(\S+?))?\s*-->/.exec(menu);
130
+ const shape = line ? line[1] : 'all';
131
+ if (!['all', 'curated'].includes(shape)) row('FAIL', 'menu shape', `menu=${shape} is not a shape the loop knows (all | curated)`);
132
+ else if (shape === 'curated') {
133
+ const index = line[2];
134
+ if (!index) row('DEGRADE', 'menu shape', 'menu=curated names no index of every doc, so docs-process cannot say where a new doc is registered');
135
+ else if (!existsSync(join(repo, index))) row('DEGRADE', 'menu shape', `menu=curated; its index ${index} does not exist`);
136
+ else row('ok', 'menu shape', `menu=curated; index=${index}`);
137
+ } else row('ok', 'menu shape', line ? 'menu=all' : 'menu=all (no shape line: the default)');
138
+
139
+ const named = new Set();
140
+ for (const l of menu.split('\n')) {
141
+ if (/not detected/i.test(l)) continue;
142
+ for (const m of l.matchAll(/(?:\]\(|`)(?:\.\.\/)?((?:guides|patterns|infrastructure|features)\/[A-Za-z0-9_./-]+\.md)/g)) named.add(m[1]);
143
+ }
144
+ const missing = [...named].filter((p) => !existsSync(join(repo, 'docs', p))).sort();
145
+ for (const p of missing) row('FAIL', `menu: docs/${p}`, 'the menu names a doc that does not exist');
146
+ if (shape === 'curated') {
147
+ for (const p of [...named].filter((q) => !missing.includes(q)).sort()) {
148
+ const text = read(join(repo, 'docs', p));
149
+ const short = [];
150
+ const st = statusOf(text);
151
+ if (st === null) short.push('no status in either header form');
152
+ else if (st.value === 'draft') short.push('status draft');
153
+ if (!/^## Key Files\b/m.test(text) && frontmatterListCount(text, 'related_files') === 0) short.push('names no code (no ## Key Files section, no related_files list)');
154
+ row(short.length ? 'DEGRADE' : 'ok', `menu: docs/${p}`, short.length ? `below the loop's standard: ${short.join(', ')}` : "meets the loop's standard");
155
+ }
156
+ } else if (shape === 'all') {
157
+ const unnamed = livingDocs().filter((p) => !named.has(p) && !menu.includes(p));
158
+ for (const p of unnamed) row('DEGRADE', `menu: docs/${p}`, 'a living doc the menu does not name (menu=all) — add its row, or declare the menu curated');
159
+ if (!unnamed.length && !missing.length) row('ok', 'menu entries', `names every living doc (${named.size}), each present`);
128
160
  }
129
161
  }
130
162
  }
@@ -249,6 +281,24 @@ function frontmatterOf(text) {
249
281
  }
250
282
  return out;
251
283
  }
284
+ // The living docs a menu=all menu lists: every .md directly under the four living folders, and each
285
+ // feature folder's own docs one level down (a filed effort's notes sit deeper and are history, not living).
286
+ function livingDocs() {
287
+ const out = [];
288
+ for (const f of ['guides', 'patterns', 'infrastructure', 'features']) {
289
+ const dir = join(repo, 'docs', f);
290
+ if (!existsSync(dir)) continue;
291
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
292
+ if (e.isFile() && e.name.endsWith('.md') && e.name !== 'README.md') out.push(`${f}/${e.name}`);
293
+ else if (f === 'features' && e.isDirectory()) {
294
+ for (const g of readdirSync(join(dir, e.name), { withFileTypes: true })) {
295
+ if (g.isFile() && g.name.endsWith('.md') && g.name !== 'README.md') out.push(`features/${e.name}/${g.name}`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ return out.sort();
301
+ }
252
302
  function firstBodyLine(text) {
253
303
  let body = text;
254
304
  if (text.startsWith('---\n')) { const end = text.indexOf('\n---', 4); if (end >= 0) body = text.slice(end + 4).replace(/^\n+/, ''); }
@@ -27,6 +27,32 @@ export function compareVersions(a, b) {
27
27
  return 0;
28
28
  }
29
29
 
30
+ // A patch release never changes what a copy of the method docs carries, so a stamp is behind only when its
31
+ // major.minor is.
32
+ export function compareMinor(a, b) {
33
+ const pa = a.split(/[.-]/).map((x) => parseInt(x, 10) || 0), pb = b.split(/[.-]/).map((x) => parseInt(x, 10) || 0);
34
+ for (let i = 0; i < 2; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
35
+ return 0;
36
+ }
37
+
38
+ // How many entries a frontmatter key lists: an inline `[a, b]`, indented `- item` lines, or one scalar. Zero
39
+ // when the key is absent or empty.
40
+ export function frontmatterListCount(text, key) {
41
+ const t = text.replace(/\r\n/g, '\n');
42
+ if (!t.startsWith('---\n')) return 0;
43
+ const end = t.indexOf('\n---', 4);
44
+ if (end < 0) return 0;
45
+ const lines = t.slice(4, end).split('\n');
46
+ const i = lines.findIndex((l) => l.startsWith(`${key}:`));
47
+ if (i < 0) return 0;
48
+ const inline = lines[i].slice(key.length + 1).trim();
49
+ if (inline.startsWith('[')) return inline.replace(/^\[|\]$/g, '').split(',').map((x) => x.trim()).filter(Boolean).length;
50
+ if (inline) return 1;
51
+ let n = 0;
52
+ for (let j = i + 1; j < lines.length && /^\s+-\s*\S/.test(lines[j]); j++) n++;
53
+ return n;
54
+ }
55
+
30
56
  // A file as lines that keep their own terminators, so an untouched line is written back byte for byte.
31
57
  export function linesOf(text) {
32
58
  return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
@@ -6,7 +6,7 @@
6
6
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { join, resolve, dirname } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { OWNED, STAMP, compareVersions, linesOf, bare, eolOf, h2Index, findSection, railsItems, stampIndex, mapOf } from './owned.mjs';
9
+ import { OWNED, STAMP, compareVersions, compareMinor, linesOf, bare, eolOf, h2Index, findSection, railsItems, stampIndex, mapOf } from './owned.mjs';
10
10
 
11
11
  const args = process.argv.slice(2);
12
12
  const write = args.includes('--write');
@@ -92,13 +92,14 @@ for (const file of ['docs-workflow.md', 'engineering-loop.md']) {
92
92
  const stampAt = stampIndex(lines);
93
93
  const current = STAMP.exec(bare(lines[stampAt]))[2];
94
94
  const cmp = compareVersions(current, packVersion);
95
- if (cmp < 0) {
95
+ if (compareMinor(current, packVersion) < 0) {
96
96
  const raw = lines[stampAt];
97
97
  const bom = raw.startsWith('') ? '' : '';
98
98
  lines[stampAt] = bom + raw.slice(bom.length).replace(STAMP, `$1${packVersion}$3`);
99
99
  row(shown, 'stamp', `${current} to ${packVersion}`);
100
100
  changed = true;
101
101
  } else if (cmp > 0) row(shown, 'stamp', `${current} — ahead of this pack; left as it is`);
102
+ else if (cmp < 0) row(shown, 'stamp', `${current} — the pack is ${packVersion}, a patch: a patch never asks a copy to change`);
102
103
  else row(shown, 'stamp', `${current} — current`);
103
104
 
104
105
  if (changed) {
@@ -53,7 +53,7 @@ Do this immediately, whatever your assignment turns out to be. It needs no role
53
53
 
54
54
  | Doc | What it gives you |
55
55
  |---|---|
56
- | `docs/_meta/doc-index.md` | the menu — which living doc answers which question |
56
+ | `docs/_meta/doc-index.md` | the menu — which living doc answers which question: every living doc, or on a curated menu the ones meant for seats (its shape line says which) |
57
57
  | `docs/_meta/docs-workflow.md` | the folders, the templates, the two loops — and, in its sections marked (repository-owned), this repository's doc header and where an effort is filed |
58
58
  | `docs/_meta/engineering-loop.md` | the seats, the ceremony, **the comms protocol you follow** — and this repository's rails (repository-owned) |
59
59
 
@@ -2,6 +2,12 @@
2
2
 
3
3
  > The menu. Read this at session start, pick the 2-3 docs that match your task, read those for context.
4
4
  > This is the curated short list — the compiled layer you read *before* diving into source.
5
+ <!-- naswerks-loop: menu=all -->
6
+ <!-- init: the shape line above stays in the output. It says menu=all unless the scan found an index the
7
+ repository already keeps of every doc (a docs/INDEX.md or docs/index.md that lists them, a
8
+ docs/SUMMARY.md, a mkdocs.yml nav, a docusaurus or vitepress sidebar); then it says
9
+ menu=curated; index={that path}, and the tables below list only the docs init drafted plus the Meta
10
+ rows. The owner adds each further doc that meets the loop's standard. -->
5
11
 
6
12
  ---
7
13