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.
- package/CHANGELOG.md +25 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +2 -2
- package/dist/cli/commands/code-mode-gate-should-pause-command.js +1 -1
- package/dist/cli/commands/comments-commands.d.ts +14 -0
- package/dist/cli/commands/comments-commands.js +96 -0
- package/dist/cli/commands/core/memory-command.js +10 -0
- package/dist/cli/commands/core/skill-command.js +1 -1
- package/dist/cli/commands/core/standards-command.js +1 -1
- package/dist/cli/commands/ecc-commands.d.ts +17 -22
- package/dist/cli/commands/ecc-commands.js +38 -26
- package/dist/cli/commands/prd-commands.js +8 -2
- package/dist/cli/program.js +4 -9
- package/dist/services/code/mode-gate-types.d.ts +1 -1
- package/dist/services/code/mode-gate-types.js +0 -1
- package/dist/services/code/mode-gate.js +1 -9
- package/dist/services/code/user-touchpoint-classifier.js +0 -7
- package/dist/services/code-review/ecc-bridge.d.ts +6 -6
- package/dist/services/comments/citation-rules.d.ts +154 -0
- package/dist/services/comments/citation-rules.js +241 -0
- package/dist/services/comments/comment-audit.d.ts +57 -0
- package/dist/services/comments/comment-audit.js +100 -0
- package/dist/services/comments/comment-citations.d.ts +60 -0
- package/dist/services/comments/comment-citations.js +186 -0
- package/dist/services/comments/comment-hygiene.d.ts +79 -0
- package/dist/services/comments/comment-hygiene.js +133 -0
- package/dist/services/comments/comment-prune.d.ts +88 -0
- package/dist/services/comments/comment-prune.js +148 -0
- package/dist/services/comments/prune-apply.d.ts +60 -0
- package/dist/services/comments/prune-apply.js +150 -0
- package/dist/services/comments/repo-path-probe.d.ts +43 -0
- package/dist/services/comments/repo-path-probe.js +78 -0
- package/dist/services/log/retention.d.ts +0 -16
- package/dist/services/log/retention.js +0 -17
- package/dist/services/memory/project-memory-service/index.d.ts +1 -1
- package/dist/services/memory/project-memory-service/index.js +1 -1
- package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +19 -7
- package/dist/services/memory/project-memory-service/store/atomic-write.js +120 -26
- package/dist/services/prd/handoff-frontmatter.js +61 -0
- package/dist/services/prd/handoff-gate-evidence.js +14 -10
- package/dist/services/prd/handoff-service.d.ts +11 -1
- package/dist/services/prd/handoff-service.js +11 -1
- package/dist/services/prd/handoff-types.d.ts +33 -1
- package/dist/services/recommendations/installed-capability-detector.d.ts +5 -5
- package/dist/services/recommendations/installed-capability-detector.js +11 -10
- package/dist/services/scan/archetype-detection.d.ts +37 -0
- package/dist/services/scan/archetype-detection.js +175 -2
- package/dist/services/scan/archetype-service.js +36 -22
- package/dist/services/scan/scan-types.d.ts +9 -0
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +2 -2
- package/dist/services/workspace/generated-artifacts-stamp.js +2 -2
- package/dist/services/workspace/workspace-service.js +1 -1
- package/package.json +5 -5
- package/scripts/install-skills.mjs +0 -177
- package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +3 -1
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +3 -2
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +29 -7
- package/skills/peaks-code/references/frontend-only-mode.md +2 -2
- package/skills/peaks-code/references/startup-sequence.md +0 -4
- package/dist/cli/commands/upgrade-commands.d.ts +0 -25
- package/dist/cli/commands/upgrade-commands.js +0 -154
- package/dist/services/upgrade/1x-detector-service.d.ts +0 -7
- package/dist/services/upgrade/1x-detector-service.js +0 -96
- package/dist/services/upgrade/gitignore-migrate-service.d.ts +0 -56
- package/dist/services/upgrade/gitignore-migrate-service.js +0 -170
- package/dist/services/upgrade/upgrade-service.d.ts +0 -81
- package/dist/services/upgrade/upgrade-service.js +0 -428
- 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;
|