@naswerks/engineering-loop 0.5.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.5.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.5.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,16 @@ 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
+
8
18
  ## [0.5.0] - 2026-09-24
9
19
 
10
20
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naswerks/engineering-loop",
3
- "version": "0.5.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.5.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.
@@ -110,8 +110,9 @@ it once and never again. A line near its top says which of two shapes it has, an
110
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
111
 
112
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 has a `## Key Files` section. `init doctor`
114
- checks exactly that, for every doc a curated menu names.
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.
115
116
 
116
117
  **Silence.** A section that says nothing about an item leaves the loop's default: a menu with no shape line
117
118
  lists every living doc (`menu=all`), no gate runs on a commit, the base branch is the remote's
@@ -148,7 +149,8 @@ Keep what the skills need to find, change the words to yours:
148
149
  the menu then lists only the docs its seats read, each meeting the loop's standard, and every other doc
149
150
  stays in the repository's own index, kept the way it already is.
150
151
 
151
- **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.
152
154
  The loop text around them is this repository's copy from whenever it was taken: `init doctor` never grades
153
155
  it, and `init refresh` never touches it — refresh adds a section or a rails item a newer pack reads, and
154
156
  restamps.
@@ -1,4 +1,4 @@
1
- <!-- naswerks-loop: version=0.5.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-*`
@@ -39,7 +39,8 @@ just counts.
39
39
  - **menu drift** — read the menu's shape line (`rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`;
40
40
  none means `menu=all`). On `menu=all`: a living doc the menu does not name. On `menu=curated`: a doc the
41
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 a `## Key Files` section).
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).
43
44
 
44
45
  5. **Open issues.** Grep living docs for an `## Open Issues` section; list which docs/features carry
45
46
  unresolved, code-verified problems (and how many) so known risks surface at orientation instead of
@@ -190,13 +190,13 @@ FAIL, so a CI job or a control plane can call it.
190
190
  | Row | FAIL | DEGRADE | ok |
191
191
  |---|---|---|---|
192
192
  | each of the eight folders | missing | README missing | present |
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 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 |
194
194
  | each repository-owned heading, in its file | — | unmarked (an earlier spelling) or missing — `init refresh` marks or adds it | present |
195
195
  | each item of § This repository's rails (repository-owned) | — | missing, or still its absent-line | recorded |
196
196
  | `_meta/doc-index.md` and its three slot sections | missing | a slot carries only the absent-line | filled |
197
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
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 `## Key Files` | meets the loop's standard |
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 |
200
200
  | the four floor docs | missing | the word `draft` (read from either header form), or an `<!-- init: -->` left in | a decided status |
201
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 |
202
202
  | `CLAUDE.md` / `AGENTS.md` stanza · `.claude/settings.json` pin | — | missing | present |
@@ -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);
@@ -123,7 +122,7 @@ function docRow(relPath, required) {
123
122
  // every living doc; menu=curated lists the docs the seats read and names the index that lists every doc. A
124
123
  // menu with no shape line is menu=all. Every doc a menu names must exist (a row saying `not detected` is
125
124
  // exempt); a curated menu's docs must also meet the loop's standard: a status in either header form, not
126
- // draft, and a `## Key Files` section.
125
+ // draft, and names the code it describes: a `## Key Files` section, or a frontmatter `related_files` list.
127
126
  {
128
127
  const menu = read(join(repo, 'docs', '_meta', 'doc-index.md'));
129
128
  if (menu !== null) {
@@ -151,7 +150,7 @@ function docRow(relPath, required) {
151
150
  const st = statusOf(text);
152
151
  if (st === null) short.push('no status in either header form');
153
152
  else if (st.value === 'draft') short.push('status draft');
154
- if (!/^## Key Files\b/m.test(text)) short.push('no ## Key Files');
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)');
155
154
  row(short.length ? 'DEGRADE' : 'ok', `menu: docs/${p}`, short.length ? `below the loop's standard: ${short.join(', ')}` : "meets the loop's standard");
156
155
  }
157
156
  } else if (shape === 'all') {
@@ -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) {