peaks-loop 4.1.0 → 4.1.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/_register.js +2 -2
  5. package/dist/cli/commands/code-mode-gate-should-pause-command.js +1 -1
  6. package/dist/cli/commands/comments-commands.d.ts +14 -0
  7. package/dist/cli/commands/comments-commands.js +96 -0
  8. package/dist/cli/commands/core/memory-command.js +10 -0
  9. package/dist/cli/commands/core/skill-command.js +1 -1
  10. package/dist/cli/commands/core/standards-command.js +1 -1
  11. package/dist/cli/commands/ecc-commands.d.ts +17 -22
  12. package/dist/cli/commands/ecc-commands.js +38 -26
  13. package/dist/cli/commands/prd-commands.js +8 -2
  14. package/dist/cli/program.js +4 -9
  15. package/dist/services/code/mode-gate-types.d.ts +1 -1
  16. package/dist/services/code/mode-gate-types.js +0 -1
  17. package/dist/services/code/mode-gate.js +1 -9
  18. package/dist/services/code/user-touchpoint-classifier.js +0 -7
  19. package/dist/services/code-review/ecc-bridge.d.ts +6 -6
  20. package/dist/services/comments/citation-rules.d.ts +154 -0
  21. package/dist/services/comments/citation-rules.js +241 -0
  22. package/dist/services/comments/comment-audit.d.ts +57 -0
  23. package/dist/services/comments/comment-audit.js +100 -0
  24. package/dist/services/comments/comment-citations.d.ts +60 -0
  25. package/dist/services/comments/comment-citations.js +186 -0
  26. package/dist/services/comments/comment-hygiene.d.ts +79 -0
  27. package/dist/services/comments/comment-hygiene.js +133 -0
  28. package/dist/services/comments/comment-prune.d.ts +88 -0
  29. package/dist/services/comments/comment-prune.js +148 -0
  30. package/dist/services/comments/prune-apply.d.ts +60 -0
  31. package/dist/services/comments/prune-apply.js +150 -0
  32. package/dist/services/comments/repo-path-probe.d.ts +43 -0
  33. package/dist/services/comments/repo-path-probe.js +78 -0
  34. package/dist/services/log/retention.d.ts +0 -16
  35. package/dist/services/log/retention.js +0 -17
  36. package/dist/services/memory/project-memory-service/index.d.ts +1 -1
  37. package/dist/services/memory/project-memory-service/index.js +1 -1
  38. package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +19 -7
  39. package/dist/services/memory/project-memory-service/store/atomic-write.js +120 -26
  40. package/dist/services/prd/handoff-frontmatter.js +61 -0
  41. package/dist/services/prd/handoff-gate-evidence.js +14 -10
  42. package/dist/services/prd/handoff-service.d.ts +11 -1
  43. package/dist/services/prd/handoff-service.js +11 -1
  44. package/dist/services/prd/handoff-types.d.ts +33 -1
  45. package/dist/services/recommendations/installed-capability-detector.d.ts +5 -5
  46. package/dist/services/recommendations/installed-capability-detector.js +11 -10
  47. package/dist/services/scan/archetype-detection.d.ts +37 -0
  48. package/dist/services/scan/archetype-detection.js +175 -2
  49. package/dist/services/scan/archetype-service.js +36 -22
  50. package/dist/services/scan/scan-types.d.ts +9 -0
  51. package/dist/services/workspace/generated-artifacts-stamp.d.ts +2 -2
  52. package/dist/services/workspace/generated-artifacts-stamp.js +2 -2
  53. package/dist/services/workspace/workspace-service.js +1 -1
  54. package/package.json +5 -5
  55. package/scripts/install-skills.mjs +0 -177
  56. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +3 -1
  57. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  58. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +3 -2
  59. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +29 -7
  60. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  61. package/skills/peaks-code/references/startup-sequence.md +0 -4
  62. package/dist/cli/commands/upgrade-commands.d.ts +0 -25
  63. package/dist/cli/commands/upgrade-commands.js +0 -154
  64. package/dist/services/upgrade/1x-detector-service.d.ts +0 -7
  65. package/dist/services/upgrade/1x-detector-service.js +0 -96
  66. package/dist/services/upgrade/gitignore-migrate-service.d.ts +0 -56
  67. package/dist/services/upgrade/gitignore-migrate-service.js +0 -170
  68. package/dist/services/upgrade/upgrade-service.d.ts +0 -81
  69. package/dist/services/upgrade/upgrade-service.js +0 -428
  70. package/skills/peaks-code/references/step-0-55-1x-detection.md +0 -83
@@ -0,0 +1,154 @@
1
+ /**
2
+ * What makes a backtick span a claim about this repository's file tree.
3
+ *
4
+ * These are the candidate rules, shared by the two readers of the same judgement:
5
+ * the comment classifier (`comment-hygiene.ts`) and, by literal, the repository's
6
+ * citation guard (`tests/unit/standards/repo-citation-integrity.test.ts`).
7
+ * `tests/unit/comments/citation-parity.test.ts` pins that the two spellings agree —
8
+ * a shape rule restated in a second place and then drifting is the defect this
9
+ * repository keeps documenting for itself.
10
+ *
11
+ * The rules are exclusions, and every one of them exists because of a measured
12
+ * false positive rather than a taste for tidiness. See `isCitationCandidate`.
13
+ */
14
+ /** Backtick span that has at least one `/` — a path citation, not a mention. */
15
+ export declare const PATH_SHAPED: RegExp;
16
+ /** First segments that make a span repo-relative rather than document-relative. */
17
+ export declare const REPO_ANCHORS: RegExp;
18
+ /** `path.ts:14-17` — strip before judging; the FILE must exist, the line may move. */
19
+ export declare const LINE_SUFFIX: RegExp;
20
+ /**
21
+ * Session-runtime dirs. A path starting with one of these is a runtime artifact
22
+ * written under `.peaks/_runtime/<sessionId>/` and abbreviated in prose; it is
23
+ * not a repo file, and reporting it as missing is how a guard starts being
24
+ * ignored. Same exemption list the citation guard carries.
25
+ */
26
+ export declare const SESSION_WORKSPACE_DIRS: Set<string>;
27
+ /** `<placeholder>`, an ellipsis, a glob segment — illustrative, not asserted. */
28
+ export declare const SYNTHETIC_SEGMENT: RegExp;
29
+ /**
30
+ * A segment that is exactly an ASCII ellipsis names a path *family*
31
+ * (`src/...`, `openspec/changes/...`), not a file. `SYNTHETIC_SEGMENT` above
32
+ * catches the Unicode `…` and the glob `*`; this catches `...`, which its
33
+ * character class admits.
34
+ */
35
+ export declare const ELLIPSIS_SEGMENT: RegExp;
36
+ export declare const BARE_TEST_FILENAME_SHAPE: RegExp;
37
+ /**
38
+ * Runtime state the CLI writes into a project. In a checkout of THIS repository
39
+ * these are absent by design — they are produced in the consuming project at run
40
+ * time — so a comment naming them makes no claim about this tree.
41
+ */
42
+ export declare const RUNTIME_STATE_PREFIXES: readonly [".peaks/_runtime/", ".peaks/cron/", ".peaks/cache/", ".peaks/_dogfood/", ".peaks/_sub_agents/"];
43
+ /**
44
+ * A file sitting flat in a project's `.peaks/` root is state the CLI writes there
45
+ * at run time — `.peaks/fork-state.json`, `.peaks/polyrepo.json`,
46
+ * `.peaks/role-registry.json`, `.peaks/.gitignore`, `.peaks/smoke-paths.json`.
47
+ *
48
+ * What this repository's own `.peaks/` commits is directories (`standards/`,
49
+ * `docs/`, `lint/`, `memory/`) plus four flat files, every one of which is
50
+ * present, so a citation to a tracked flat file resolves and this rule never fires
51
+ * for it. The shape is trusted over an entry list because an entry list has to
52
+ * grow with every runtime file the CLI adds — and the cost is stated: a future flat
53
+ * `.peaks/<file>` that is committed and later deleted would be hidden here.
54
+ * Repository convention is that top-level `.peaks/` entries are session or decision
55
+ * state, and a citation of that shape describes the consuming project.
56
+ */
57
+ export declare const RUNTIME_STATE_ROOT_FILE: RegExp;
58
+ /** Stems that name a shape rather than a file (`src/services/x/y.ts`, `foo.ts`). */
59
+ export declare const SYNTHETIC_STEMS: Set<string>;
60
+ /**
61
+ * Text that introduces a path as a shape to copy into the consumer's own
62
+ * repository, rather than a claim that this tree contains it.
63
+ */
64
+ export declare const ILLUSTRATION_CUE: RegExp;
65
+ /**
66
+ * A sentence that reasons about what a path *would* do, rather than reporting
67
+ * that a file is there.
68
+ *
69
+ * Two measured shapes, both read from the whole line because the cue sits on
70
+ * either side of the span:
71
+ *
72
+ * - a matching rule illustrated with made-up operands —
73
+ * `E.g. \`src/services/login\` should match \`src/services/login/handler.ts\``.
74
+ * Neither path is in any repository; the line is about the glob semantics.
75
+ * The `ILLUSTRATION_CUE` above cannot reach the second operand, because the
76
+ * cue must sit immediately before the span to fire, and here it does not.
77
+ * - a deliberate counterexample — `Written into \`.peaks/.gitignore\` they
78
+ * resolved to \`…\` and \`…\` — matching nothing, in every project`. The comment
79
+ * names two paths precisely because they match nothing; reporting them as
80
+ * missing files asks the reader to fix a bug the text is describing.
81
+ *
82
+ * The list is modal phrases only, and the boundary is load-bearing: the real
83
+ * finding at `src/services/workspace/claude-settings-template.ts:90` asserts that a
84
+ * handler "invokes the shipped script" by path, uses no modal, and stays reported —
85
+ * the script is not in the tree. Bare `resolves to` was tried and dropped from the
86
+ * list for the opposite reason: it also appears in live assertions about files that
87
+ * DO exist, and a cue that clears those is a cue that hides debt.
88
+ *
89
+ * (This rule's own header was reported by the scan it documents — the sentence
90
+ * above quoted the missing script's path verbatim, and quoting a dead path in prose
91
+ * about a dead path is still a dead path. Cited by file and line now.)
92
+ */
93
+ export declare const HYPOTHETICAL_CUE: RegExp;
94
+ /**
95
+ * Documented runtime paths that are legitimately absent from a checkout. Each
96
+ * entry needs a reason; an allowlist without one is how a guard dies.
97
+ */
98
+ export declare const OPTIONAL_RUNTIME_PATHS: Set<string>;
99
+ /** One path-shaped backtick span, with the offsets that bound it in its line. */
100
+ export type Citation = {
101
+ readonly span: string;
102
+ /** Offset of the opening backtick. */
103
+ readonly from: number;
104
+ /** Offset just past the closing backtick. */
105
+ readonly to: number;
106
+ };
107
+ /** What the candidate rule needs beyond the span itself. */
108
+ export type CitationContext = {
109
+ /** Repo-relative path of the citing file or document. */
110
+ readonly file: string;
111
+ /** True when a repo-relative path is a directory in the working tree. */
112
+ readonly dirExists: (relDir: string) => boolean;
113
+ };
114
+ /**
115
+ * The second resolution origin, made explicit: an unanchored span is a SIBLING
116
+ * reference, so its parent directory must exist next to the citing file — and only
117
+ * there.
118
+ *
119
+ * The repo root is deliberately not an origin for an unanchored span: writing a
120
+ * repo-root path is exactly what the anchors are for. The walk is narrower than the
121
+ * citation guard's every-ancestor version, and the reason is measured: with every
122
+ * ancestor tried, `hooks/hooks.json` cited from
123
+ * `src/services/doctor/doctor-service/checks/` was adopted by `src/services/hooks/`
124
+ * four levels up, and `memory/index.json` by `src/services/memory/` — 16 findings
125
+ * of paths inside installed packages and under
126
+ * `.peaks/_runtime/<sessionId>/`, reported as missing repository files. A
127
+ * sibling-only reading loses nothing real: a deleted file cited by the directory it
128
+ * lived in still has that parent dir, and is still reported.
129
+ */
130
+ export declare function isFileRelative(citation: string, ctx: CitationContext): boolean;
131
+ /**
132
+ * Is this span a claim about the working tree at all?
133
+ *
134
+ * These are the candidate exclusions the repository's citation guard applies to
135
+ * markdown and to `scripts/` comments, transplanted to source comments —
136
+ * deliberately the SAME rules, not a second definition of "citation". Porting them
137
+ * was the answer to a measurement: 188 findings for roughly 20 real dead
138
+ * references, because a comment that gives a shape as an example (`a/b`,
139
+ * `pages/api`), names a path inside an installed package (`hooks/hooks.json`), or
140
+ * documents runtime state the CLI writes in the user's project
141
+ * (`.peaks/cron/schedule.json`) all read as "missing file" to a probe that only
142
+ * knows `exists`.
143
+ *
144
+ * The direction to preserve is the guard's stated invariant: these must never hide
145
+ * an ANCHORED citation. A path beginning `src/`, `tests/`, `docs/`, `skills/`,
146
+ * `scripts/`, `packages/` or `.peaks/` claims a location in this repository, and
147
+ * nothing below lets it through unread.
148
+ *
149
+ * `namesNoDirectory` is set only by the bare-`*.test.ts`-filename reading, whose own
150
+ * pattern has already established the filename shape: a filename names no
151
+ * directory, so it cannot satisfy `PATH_SHAPED`, and it has no parent directory to
152
+ * read as a sibling.
153
+ */
154
+ export declare function isCitationCandidate(citation: Citation, line: string, ctx: CitationContext, namesNoDirectory?: boolean): boolean;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * What makes a backtick span a claim about this repository's file tree.
3
+ *
4
+ * These are the candidate rules, shared by the two readers of the same judgement:
5
+ * the comment classifier (`comment-hygiene.ts`) and, by literal, the repository's
6
+ * citation guard (`tests/unit/standards/repo-citation-integrity.test.ts`).
7
+ * `tests/unit/comments/citation-parity.test.ts` pins that the two spellings agree —
8
+ * a shape rule restated in a second place and then drifting is the defect this
9
+ * repository keeps documenting for itself.
10
+ *
11
+ * The rules are exclusions, and every one of them exists because of a measured
12
+ * false positive rather than a taste for tidiness. See `isCitationCandidate`.
13
+ */
14
+ /** Backtick span that has at least one `/` — a path citation, not a mention. */
15
+ export const PATH_SHAPED = /^[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)+$/;
16
+ /** First segments that make a span repo-relative rather than document-relative. */
17
+ export const REPO_ANCHORS = /^(\.peaks|\.claude|\.github|src|tests|docs|scripts|skills|packages|bin|openspec|contracts)\//;
18
+ /** `path.ts:14-17` — strip before judging; the FILE must exist, the line may move. */
19
+ export const LINE_SUFFIX = /:\d+(?:-\d+)?$/;
20
+ /**
21
+ * Session-runtime dirs. A path starting with one of these is a runtime artifact
22
+ * written under `.peaks/_runtime/<sessionId>/` and abbreviated in prose; it is
23
+ * not a repo file, and reporting it as missing is how a guard starts being
24
+ * ignored. Same exemption list the citation guard carries.
25
+ */
26
+ export const SESSION_WORKSPACE_DIRS = new Set([
27
+ 'prd',
28
+ 'rd',
29
+ 'qa',
30
+ 'sc',
31
+ 'txt',
32
+ 'audit',
33
+ 'ui',
34
+ 'session',
35
+ 'system'
36
+ ]);
37
+ /** `<placeholder>`, an ellipsis, a glob segment — illustrative, not asserted. */
38
+ export const SYNTHETIC_SEGMENT = /[<>*…]|etc\.$/;
39
+ /**
40
+ * A segment that is exactly an ASCII ellipsis names a path *family*
41
+ * (`src/...`, `openspec/changes/...`), not a file. `SYNTHETIC_SEGMENT` above
42
+ * catches the Unicode `…` and the glob `*`; this catches `...`, which its
43
+ * character class admits.
44
+ */
45
+ export const ELLIPSIS_SEGMENT = /(?:^|\/)\.\.\.(?:$|\/)/;
46
+ /**
47
+ * The bare-`*.test.ts`-filename shape, written once. A filename carries no slash,
48
+ * so `PATH_SHAPED` cannot see it; this is the second shape a candidate claim can
49
+ * take, and the guard reads the same character classes.
50
+ */
51
+ const BARE_FILENAME_NAME = String.raw `[A-Za-z0-9_][A-Za-z0-9_.-]*-[A-Za-z0-9_.-]*\.test\.ts`;
52
+ export const BARE_TEST_FILENAME_SHAPE = new RegExp(`^${BARE_FILENAME_NAME}$`);
53
+ /**
54
+ * Runtime state the CLI writes into a project. In a checkout of THIS repository
55
+ * these are absent by design — they are produced in the consuming project at run
56
+ * time — so a comment naming them makes no claim about this tree.
57
+ */
58
+ export const RUNTIME_STATE_PREFIXES = [
59
+ '.peaks/_runtime/',
60
+ '.peaks/cron/',
61
+ '.peaks/cache/',
62
+ // Both are gitignored at the repository root and written per run: `_dogfood/` by
63
+ // the dogfood harness, `_sub_agents/` by `peaks sub-agent dispatch`.
64
+ '.peaks/_dogfood/',
65
+ '.peaks/_sub_agents/'
66
+ ];
67
+ /**
68
+ * A file sitting flat in a project's `.peaks/` root is state the CLI writes there
69
+ * at run time — `.peaks/fork-state.json`, `.peaks/polyrepo.json`,
70
+ * `.peaks/role-registry.json`, `.peaks/.gitignore`, `.peaks/smoke-paths.json`.
71
+ *
72
+ * What this repository's own `.peaks/` commits is directories (`standards/`,
73
+ * `docs/`, `lint/`, `memory/`) plus four flat files, every one of which is
74
+ * present, so a citation to a tracked flat file resolves and this rule never fires
75
+ * for it. The shape is trusted over an entry list because an entry list has to
76
+ * grow with every runtime file the CLI adds — and the cost is stated: a future flat
77
+ * `.peaks/<file>` that is committed and later deleted would be hidden here.
78
+ * Repository convention is that top-level `.peaks/` entries are session or decision
79
+ * state, and a citation of that shape describes the consuming project.
80
+ */
81
+ export const RUNTIME_STATE_ROOT_FILE = /^\.peaks\/[^/]+$/;
82
+ /** Stems that name a shape rather than a file (`src/services/x/y.ts`, `foo.ts`). */
83
+ export const SYNTHETIC_STEMS = new Set([
84
+ 'x',
85
+ 'y',
86
+ 'z',
87
+ 'foo',
88
+ 'bar',
89
+ 'baz',
90
+ 'qux',
91
+ 'example',
92
+ 'sample',
93
+ 'dummy',
94
+ 'placeholder'
95
+ ]);
96
+ /**
97
+ * Text that introduces a path as a shape to copy into the consumer's own
98
+ * repository, rather than a claim that this tree contains it.
99
+ */
100
+ export const ILLUSTRATION_CUE = /(?:e\.g\.|i\.e\.|for example|such as|reference shape|Example:)\s*\(?\s*$/i;
101
+ /**
102
+ * A sentence that reasons about what a path *would* do, rather than reporting
103
+ * that a file is there.
104
+ *
105
+ * Two measured shapes, both read from the whole line because the cue sits on
106
+ * either side of the span:
107
+ *
108
+ * - a matching rule illustrated with made-up operands —
109
+ * `E.g. \`src/services/login\` should match \`src/services/login/handler.ts\``.
110
+ * Neither path is in any repository; the line is about the glob semantics.
111
+ * The `ILLUSTRATION_CUE` above cannot reach the second operand, because the
112
+ * cue must sit immediately before the span to fire, and here it does not.
113
+ * - a deliberate counterexample — `Written into \`.peaks/.gitignore\` they
114
+ * resolved to \`…\` and \`…\` — matching nothing, in every project`. The comment
115
+ * names two paths precisely because they match nothing; reporting them as
116
+ * missing files asks the reader to fix a bug the text is describing.
117
+ *
118
+ * The list is modal phrases only, and the boundary is load-bearing: the real
119
+ * finding at `src/services/workspace/claude-settings-template.ts:90` asserts that a
120
+ * handler "invokes the shipped script" by path, uses no modal, and stays reported —
121
+ * the script is not in the tree. Bare `resolves to` was tried and dropped from the
122
+ * list for the opposite reason: it also appears in live assertions about files that
123
+ * DO exist, and a cue that clears those is a cue that hides debt.
124
+ *
125
+ * (This rule's own header was reported by the scan it documents — the sentence
126
+ * above quoted the missing script's path verbatim, and quoting a dead path in prose
127
+ * about a dead path is still a dead path. Cited by file and line now.)
128
+ */
129
+ export const HYPOTHETICAL_CUE = /\b(?:should match|would (?:match|resolve|be|land|fail)|matching nothing|matched nothing|resolved to|do(?:es)? not match|never matched|hypothetical|counterexample|in that case)\b/i;
130
+ /**
131
+ * Documented runtime paths that are legitimately absent from a checkout. Each
132
+ * entry needs a reason; an allowlist without one is how a guard dies.
133
+ */
134
+ export const OPTIONAL_RUNTIME_PATHS = new Set([
135
+ // Legacy fallback location of the active-skill marker; the canonical path is
136
+ // `.peaks/_runtime/active-skill.json`, so this dotfile is absent by design.
137
+ '.peaks/.active-skill.json',
138
+ // Legacy back-compat location of the session binding file, read by
139
+ // `peaks session` when the canonical `.peaks/_runtime/session.json` is gone.
140
+ '.peaks/.session.json'
141
+ ]);
142
+ /**
143
+ * The second resolution origin, made explicit: an unanchored span is a SIBLING
144
+ * reference, so its parent directory must exist next to the citing file — and only
145
+ * there.
146
+ *
147
+ * The repo root is deliberately not an origin for an unanchored span: writing a
148
+ * repo-root path is exactly what the anchors are for. The walk is narrower than the
149
+ * citation guard's every-ancestor version, and the reason is measured: with every
150
+ * ancestor tried, `hooks/hooks.json` cited from
151
+ * `src/services/doctor/doctor-service/checks/` was adopted by `src/services/hooks/`
152
+ * four levels up, and `memory/index.json` by `src/services/memory/` — 16 findings
153
+ * of paths inside installed packages and under
154
+ * `.peaks/_runtime/<sessionId>/`, reported as missing repository files. A
155
+ * sibling-only reading loses nothing real: a deleted file cited by the directory it
156
+ * lived in still has that parent dir, and is still reported.
157
+ */
158
+ export function isFileRelative(citation, ctx) {
159
+ const parent = citation.slice(0, citation.lastIndexOf('/'));
160
+ const cut = ctx.file.lastIndexOf('/');
161
+ if (cut === -1)
162
+ return false;
163
+ return ctx.dirExists(`${ctx.file.slice(0, cut)}/${parent}`);
164
+ }
165
+ /**
166
+ * Spans that name something outside this repository: a package, a GitHub Action,
167
+ * runtime state the CLI writes in the consuming project, a path *family*, or the
168
+ * `./` form a document uses for a project it is installed into.
169
+ */
170
+ function namesSomethingElse(span) {
171
+ if (span.includes('@'))
172
+ return true; // npm package / GitHub-Action refs
173
+ if (RUNTIME_STATE_PREFIXES.some((prefix) => span.startsWith(prefix)))
174
+ return true;
175
+ if (RUNTIME_STATE_ROOT_FILE.test(span))
176
+ return true;
177
+ if (OPTIONAL_RUNTIME_PATHS.has(span))
178
+ return true;
179
+ if (ELLIPSIS_SEGMENT.test(span))
180
+ return true;
181
+ // A repo path in this tree is written from the repo root; `./x` is where the
182
+ // CONSUMING project must not put a file.
183
+ return span.startsWith('./');
184
+ }
185
+ /**
186
+ * Spans that are template text or an illustration rather than an assertion: wrapped
187
+ * in a `<…>` fill-in, introduced by an example cue, or built from stems that exist
188
+ * only to show a shape.
189
+ */
190
+ function illustratesRatherThanAsserts(citation, line, span) {
191
+ const before = line.slice(0, citation.from);
192
+ const after = line.slice(citation.to);
193
+ // Inside a `<…>` fill-in the span is template text meant to be replaced.
194
+ if (before.lastIndexOf('<') > before.lastIndexOf('>') && after.includes('>'))
195
+ return true;
196
+ if (ILLUSTRATION_CUE.test(before))
197
+ return true;
198
+ const basename = span.slice(span.lastIndexOf('/') + 1);
199
+ const dot = basename.indexOf('.');
200
+ return SYNTHETIC_STEMS.has(dot === -1 ? basename : basename.slice(0, dot));
201
+ }
202
+ /**
203
+ * Is this span a claim about the working tree at all?
204
+ *
205
+ * These are the candidate exclusions the repository's citation guard applies to
206
+ * markdown and to `scripts/` comments, transplanted to source comments —
207
+ * deliberately the SAME rules, not a second definition of "citation". Porting them
208
+ * was the answer to a measurement: 188 findings for roughly 20 real dead
209
+ * references, because a comment that gives a shape as an example (`a/b`,
210
+ * `pages/api`), names a path inside an installed package (`hooks/hooks.json`), or
211
+ * documents runtime state the CLI writes in the user's project
212
+ * (`.peaks/cron/schedule.json`) all read as "missing file" to a probe that only
213
+ * knows `exists`.
214
+ *
215
+ * The direction to preserve is the guard's stated invariant: these must never hide
216
+ * an ANCHORED citation. A path beginning `src/`, `tests/`, `docs/`, `skills/`,
217
+ * `scripts/`, `packages/` or `.peaks/` claims a location in this repository, and
218
+ * nothing below lets it through unread.
219
+ *
220
+ * `namesNoDirectory` is set only by the bare-`*.test.ts`-filename reading, whose own
221
+ * pattern has already established the filename shape: a filename names no
222
+ * directory, so it cannot satisfy `PATH_SHAPED`, and it has no parent directory to
223
+ * read as a sibling.
224
+ */
225
+ export function isCitationCandidate(citation, line, ctx, namesNoDirectory = false) {
226
+ const span = citation.span;
227
+ if (!(namesNoDirectory ? BARE_TEST_FILENAME_SHAPE.test(span) : PATH_SHAPED.test(span)))
228
+ return false;
229
+ if (namesSomethingElse(span))
230
+ return false;
231
+ if (illustratesRatherThanAsserts(citation, line, span))
232
+ return false;
233
+ // The line reasons about what a path would do; it does not claim one is there.
234
+ if (HYPOTHETICAL_CUE.test(line))
235
+ return false;
236
+ if (REPO_ANCHORS.test(span))
237
+ return true;
238
+ if (namesNoDirectory)
239
+ return true;
240
+ return isFileRelative(span, ctx);
241
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Project-wide comment scan: what the classifier finds across the enforced scope.
3
+ *
4
+ * Scope follows the repository's existing lint boundary — `src` plus each
5
+ * workspace package's `src`, as `.husky/lint-scope.mjs` defines it — rather than
6
+ * a second definition of "product code" written here. Two names for one rule is
7
+ * the failure the parity test in `tests/unit/comments/citation-parity.test.ts`
8
+ * exists to prevent.
9
+ *
10
+ * This is the read side only. Nothing here writes a file: the counts it produces
11
+ * are the numbers a ratchet would start from, and a ratchet seeded from a scan
12
+ * nobody has spot-checked is a gate that will be trained away the first time it
13
+ * refuses a legitimate change.
14
+ */
15
+ import { type CommentFileSummary, type CommentFinding, type CommentFindingKind } from './comment-hygiene.js';
16
+ /** Product-code directories, in report order. */
17
+ export declare const COMMENT_SCAN_SCOPE: string[];
18
+ /**
19
+ * Every source file under one scope dir, repo-relative, sorted.
20
+ *
21
+ * The relative form is not cosmetic: `citationResolves` tries a citation with no
22
+ * repo anchor against the citing file's own directory and its ancestors, so an
23
+ * absolute path here turns every sibling reference into a false "missing file".
24
+ * `relative()` + separator normalisation is what keeps that resolution honest on
25
+ * a Windows host.
26
+ */
27
+ export declare function scopeFiles(projectRoot: string, scopeDir: string): string[];
28
+ export type CommentAuditResult = {
29
+ readonly scannedFiles: number;
30
+ /** Files the caller named. Equal to `scannedFiles` unless one was unreadable. */
31
+ readonly askedFiles: number;
32
+ readonly commentLines: number;
33
+ readonly deadReferences: number;
34
+ readonly narrative: number;
35
+ readonly files: readonly CommentFileSummary[];
36
+ readonly findings: readonly CommentFinding[];
37
+ };
38
+ export type CommentAuditOptions = {
39
+ readonly projectRoot: string;
40
+ /** Narrow to one category, the way the prune will be able to. */
41
+ readonly kind?: CommentFindingKind;
42
+ /** Cap the finding list; totals are never capped. */
43
+ readonly limit?: number;
44
+ /**
45
+ * Scan EXACTLY these repo-relative files instead of walking the scope dirs.
46
+ *
47
+ * A ratchet row must be measured over the population its callers agree to, and a
48
+ * walk is not a scope: the walk sees untracked files the index never heard of and
49
+ * misses tracked files the disk dropped, so a row measured over a walk can move
50
+ * while nobody edits source. `git ls-files` filtered by the published scope rule is
51
+ * that population — the same list the eslint, prettier and silent-warning legs
52
+ * share (rid `2026-10-03-silent-warning-scope`).
53
+ */
54
+ readonly files?: readonly string[];
55
+ };
56
+ /** Scan the enforced scope. Synchronous by design: it is a CLI read path. */
57
+ export declare function auditComments(options: CommentAuditOptions): CommentAuditResult;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Project-wide comment scan: what the classifier finds across the enforced scope.
3
+ *
4
+ * Scope follows the repository's existing lint boundary — `src` plus each
5
+ * workspace package's `src`, as `.husky/lint-scope.mjs` defines it — rather than
6
+ * a second definition of "product code" written here. Two names for one rule is
7
+ * the failure the parity test in `tests/unit/comments/citation-parity.test.ts`
8
+ * exists to prevent.
9
+ *
10
+ * This is the read side only. Nothing here writes a file: the counts it produces
11
+ * are the numbers a ratchet would start from, and a ratchet seeded from a scan
12
+ * nobody has spot-checked is a gate that will be trained away the first time it
13
+ * refuses a legitimate change.
14
+ */
15
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
16
+ import { join, relative, resolve } from 'node:path';
17
+ import { scanComments, summarize } from './comment-hygiene.js';
18
+ import { createFsProbe, createRepoProbe } from './repo-path-probe.js';
19
+ /** Product-code directories, in report order. */
20
+ export const COMMENT_SCAN_SCOPE = ['src', join('packages', 'peaks-loop-mut', 'src')];
21
+ const SOURCE_SUFFIXES = ['.ts', '.tsx', '.mts', '.cts'];
22
+ function isSourceFile(name) {
23
+ return SOURCE_SUFFIXES.some((suffix) => name.endsWith(suffix));
24
+ }
25
+ /**
26
+ * Every source file under one scope dir, repo-relative, sorted.
27
+ *
28
+ * The relative form is not cosmetic: `citationResolves` tries a citation with no
29
+ * repo anchor against the citing file's own directory and its ancestors, so an
30
+ * absolute path here turns every sibling reference into a false "missing file".
31
+ * `relative()` + separator normalisation is what keeps that resolution honest on
32
+ * a Windows host.
33
+ */
34
+ export function scopeFiles(projectRoot, scopeDir) {
35
+ const root = resolve(projectRoot, scopeDir);
36
+ const base = resolve(projectRoot);
37
+ if (!existsSync(root))
38
+ return [];
39
+ const out = [];
40
+ const walk = (absDir) => {
41
+ for (const entry of readdirSync(absDir, { withFileTypes: true })) {
42
+ const abs = join(absDir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ walk(abs);
45
+ }
46
+ else if (isSourceFile(entry.name)) {
47
+ out.push(relative(base, abs).replace(/\\/g, '/'));
48
+ }
49
+ }
50
+ };
51
+ walk(root);
52
+ return out.sort();
53
+ }
54
+ function totalOf(files, key) {
55
+ return files.reduce((sum, file) => sum + file[key], 0);
56
+ }
57
+ /** Scan the enforced scope. Synchronous by design: it is a CLI read path. */
58
+ export function auditComments(options) {
59
+ const { projectRoot } = options;
60
+ const exists = createRepoProbe(projectRoot);
61
+ const installed = createFsProbe(projectRoot);
62
+ const kinds = options.kind === undefined ? undefined : [options.kind];
63
+ const population = options.files ?? scopePopulation(projectRoot);
64
+ const findings = [];
65
+ const summaries = [];
66
+ // Files actually READ, not files handed in. A tracked path the disk no longer
67
+ // carries has to show up as `scanned < asked`, which is how a leg refuses instead
68
+ // of printing a count measured over a population nobody agreed to.
69
+ let scanned = 0;
70
+ for (const file of population) {
71
+ if (!existsSync(resolve(projectRoot, file)))
72
+ continue;
73
+ scanned += 1;
74
+ const source = readFileSync(resolve(projectRoot, file), 'utf8');
75
+ const input = { file, source };
76
+ const found = scanComments(input, {
77
+ exists,
78
+ installed,
79
+ ...(kinds === undefined ? {} : { kinds })
80
+ });
81
+ if (found.length === 0)
82
+ continue;
83
+ summaries.push(summarize(input, found));
84
+ findings.push(...found);
85
+ }
86
+ const capped = options.limit === undefined ? findings : findings.slice(0, options.limit);
87
+ return {
88
+ scannedFiles: scanned,
89
+ askedFiles: population.length,
90
+ commentLines: summaries.reduce((n, file) => n + file.commentLines, 0),
91
+ deadReferences: totalOf(summaries, 'deadReferences'),
92
+ narrative: totalOf(summaries, 'narrative'),
93
+ files: summaries.sort((a, b) => b.deadReferences + b.narrative - (a.deadReferences + a.narrative)),
94
+ findings: capped
95
+ };
96
+ }
97
+ /** The walked population, used only when a caller did not hand an explicit list. */
98
+ function scopePopulation(projectRoot) {
99
+ return COMMENT_SCAN_SCOPE.flatMap((scopeDir) => scopeFiles(projectRoot, scopeDir));
100
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Comment-only views of a source file, and whether a citation it makes holds.
3
+ *
4
+ * The shape and candidate rules live in `citation-rules.ts`, shared with the
5
+ * repository's citation guard; this module holds the two things that rule needs in
6
+ * order to be applied: the comment lines of a file, and the probes that answer
7
+ * "does this path resolve here".
8
+ *
9
+ * Deliberately NOT `ts.getLeadingCommentRanges`:
10
+ * `src/services/qa/bdd-test-style-verifier.ts` records that the API returns zero
11
+ * ranges for comment-only blocks, which would silently read a header comment as no
12
+ * comment at all. Line scanning preserves both order and line numbers, which the
13
+ * worklist and the prune both need.
14
+ */
15
+ import { type Citation } from './citation-rules.js';
16
+ /** One source line, with its 1-based number, as the classifier sees it. */
17
+ export type CommentLine = {
18
+ readonly line: number;
19
+ readonly text: string;
20
+ };
21
+ /**
22
+ * The comment lines of a file: full-line comments, plus the trailing comment of a
23
+ * code line. Non-comment lines are omitted but their numbers survive, so a report
24
+ * can point at the code a comment is attached to.
25
+ */
26
+ export declare function commentLines(source: string): CommentLine[];
27
+ /**
28
+ * The path-shaped backtick spans in a line, in order, with their bounds kept.
29
+ *
30
+ * The bounds are not decoration: two of the candidate rules read the text OUTSIDE
31
+ * the span — an illustration cue sits immediately before it, a `<…>` fill-in wraps
32
+ * it — so a caller that discards positions cannot apply them.
33
+ */
34
+ export declare function citations(text: string): Citation[];
35
+ /** Every path-shaped span in a line, without its bounds. */
36
+ export declare function citedPaths(text: string): string[];
37
+ /**
38
+ * The workspace package a citing file belongs to, or null at the repository root.
39
+ *
40
+ * In a pnpm workspace, `src/x.ts` written inside `packages/<name>/src/` names that
41
+ * package's own file, not a repository-root one: three comments in this repository cite
42
+ * `packages/peaks-loop-internal-runtime/src/status-protocol.ts` and
43
+ * `packages/peaks-loop-shared-channel/src/index.ts` by their package-relative spelling,
44
+ * and both files exist. Treating those as dead references would seed a ratchet with
45
+ * findings that refuse the next honest package-internal citation, so the package root is
46
+ * an origin — for a citing file that is itself inside a package, and never for a
47
+ * top-level one.
48
+ */
49
+ export declare function packageRootOf(citingFileRel: string): string | null;
50
+ /**
51
+ * Does this path-shaped citation resolve anywhere it is allowed to resolve?
52
+ *
53
+ * `installed` must be a filesystem-only probe, not the ignore-widened one:
54
+ * `node_modules` is itself in `.gitignore`, so an ignore-aware answer here is
55
+ * "yes, installed" for every first segment, and the whole dead-reference count
56
+ * collapses to zero while looking perfectly healthy. Measured, not theorised.
57
+ */
58
+ export declare function citationResolves(citation: string, citingFileRel: string, exists: (relPath: string) => boolean, installed?: (relPath: string) => boolean): boolean;
59
+ /** The default existence probe, against a real working tree. */
60
+ export declare function treeExists(repoRoot: string): (relPath: string) => boolean;