@dzhechkov/keysarium 1.7.7 → 1.7.9
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/.dz-manifest.json +21 -9
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +2 -2
- package/sbom.json +38 -8
- package/templates/.claude/skills/feature-adr/SKILL.md +79 -1
- package/templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md +129 -0
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +10 -0
- package/templates/.claude/skills/feature-adr/modules/07-code.md +29 -0
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +162 -3
- package/templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs +201 -0
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +210 -28
- package/templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs +316 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Generalized from features/wave1-instrument-repair/check-plan-completeness.mjs (that copy is the
|
|
4
4
|
// historical artifact of its run and stays untouched); this one is parameterized by feature dir.
|
|
5
5
|
//
|
|
6
|
-
// USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2] [--require-requirements]
|
|
6
|
+
// USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2] [--require-requirements] [--no-require-task-lines]
|
|
7
7
|
// <feature-dir> defaults to the current working directory.
|
|
8
8
|
// --tier=S|M|L|XL closes the ADR-less dodge (see S-TIER HONESTY); omitting it keeps the
|
|
9
9
|
// heuristic, and the skip note then names the dodge out loud.
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// pipeline passes this flag, so the check is introduced two-shot (ADR-001 plan-inherits-
|
|
12
12
|
// requirements): WARN first so the corpus can be measured without repainting every green
|
|
13
13
|
// fixture red, FAIL once the planner prompt names the contract (feature-adr.js Step 6).
|
|
14
|
+
// C1 and C8 read the plan's TASK LINES by DEFAULT (C10 below; owner decision 2026-09-27,
|
|
15
|
+
// backlog 95565da0): an id cited only in prose, fenced code, an HTML comment, `## Amendments`
|
|
16
|
+
// or EXPECTED_CODE_TARGETS is not a task. --no-require-task-lines is the explicit way back
|
|
17
|
+
// to the raw-plan reader, where C10 only WARNs — for re-checking a plan written before the
|
|
18
|
+
// default changed. --require-task-lines is still accepted and means the default; passing
|
|
19
|
+
// BOTH is a contradiction and is refused (NOT-ESTABLISHED), never resolved silently.
|
|
14
20
|
//
|
|
15
21
|
// VERDICT CONTRACT (unchanged from the proven copy — never a silent pass):
|
|
16
22
|
// PASS exit 0 last line: `K2 plan-completeness: PASS (...)`
|
|
@@ -20,16 +26,26 @@
|
|
|
20
26
|
// ONE regex parses all three verdicts — the exit codes and their meanings are identical.)
|
|
21
27
|
// Set difference over IDENTIFIERS, not text similarity.
|
|
22
28
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
29
|
+
// C1 AND C8 SHARE ONE PLAN-SIDE READER (k2-reads-masked-plan, backlog c03903eb, 2026-09-27).
|
|
30
|
+
// Until then both were a grep over the RAW plan: an id in prose, a fenced code block, an HTML
|
|
31
|
+
// comment, the `## Amendments` section or the EXPECTED_CODE_TARGETS block counted as a task
|
|
32
|
+
// (named here as a limit since 2026-08-19 for C1 and 2026-09-16 for C8). `planTaskText()` now
|
|
33
|
+
// answers "which plan text is a task" ONCE, for both checks — so one phrase can never pass one
|
|
34
|
+
// check and fail the other. Introduced WARN-first (C10), because the corpus said so; the FAIL
|
|
35
|
+
// half became the DEFAULT on 2026-09-27 by owner decision, with this measurement on the table:
|
|
36
|
+
// MEASURED 2026-09-27 over all 472 features/*/06_implementation_plan.md, pipeline mode
|
|
37
|
+
// (--require-requirements, tier read from 00): of 101 green plans, the task-line reader turns 55
|
|
38
|
+
// red (an earlier snapshot of the same day; the count the owner decided on, taken later over 473
|
|
39
|
+
// plans, was 56 of 102), and even mask-only (prose kept) turns 11 red — the SPARC-GOAP ```yaml goal-state block and
|
|
40
|
+
// «ADR-001 governs every task» prose are the corpus's own idioms, not forgeries. So a plan that
|
|
41
|
+
// was green before 2026-09-27 may be red on a re-check: that is the decision, not a regression —
|
|
42
|
+
// cite the id on a task line, or re-check that plan with --no-require-task-lines and say so.
|
|
43
|
+
// NAMED LIMIT: `planClaimsAdrWork` (S-tier honesty) and C2 still read the raw plan — a CLAIM of
|
|
44
|
+
// ADR work in prose is still a claim, and C2 names test paths, not ids.
|
|
27
45
|
//
|
|
28
|
-
// KNOWN LIMITATION (
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// side) for C1 and C8 together, so a prose mention stops satisfying either check, is a separate
|
|
32
|
-
// backlog item — filed by the lead, not chased here.
|
|
46
|
+
// KNOWN LIMITATION (C9): only byte-identical copies are visible before the edit. Already drifted
|
|
47
|
+
// or intentionally different pinned copies remain the identity tests' job; the frozen
|
|
48
|
+
// features/wave1-instrument-repair/check-plan-completeness.mjs is a real example C9 cannot see.
|
|
33
49
|
//
|
|
34
50
|
// Checks:
|
|
35
51
|
// C1 every ADR file in 03_adr/ has >=1 task line in 06_implementation_plan.md citing it (ADR-00N)
|
|
@@ -37,6 +53,8 @@
|
|
|
37
53
|
// C3 the plan carries an EXPECTED_CODE_TARGETS: block, non-empty, and EVERY line parses to a
|
|
38
54
|
// plausible repo-relative path (no spaces unless quoted, no traversal, no markdown residue)
|
|
39
55
|
// — SFDIPOT condition: line-level validation, reject-with-reason, not just block presence
|
|
56
|
+
// C9 every tracked byte-identical twin of a target is also listed or explicitly waived with a
|
|
57
|
+
// reason; size-first narrowing, fixed exclusions, one FAIL per missing (target, twin) pair
|
|
40
58
|
// C4 the plan names the feature's OWN acid corpus (see "acid corpus" below)
|
|
41
59
|
// C5 the plan has an 'Inputs read:' line naming 03_adr, 05_architecture (wave-2 seam, cheap here)
|
|
42
60
|
// C8 every requirement id DECLARED in 01_requirements.md (FR-N, NFR-N, AC-N, C-N, with an optional
|
|
@@ -79,7 +97,9 @@
|
|
|
79
97
|
// supplied explicitly with `--acid=T1,T2,…`. If neither establishes a corpus, C4 is SKIPPED-with-note
|
|
80
98
|
// (a feature that declared no acid cases cannot be failed for not naming them).
|
|
81
99
|
import { maskMarkdown } from './markdown-masker.mjs';
|
|
82
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
100
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
101
|
+
import { execFileSync } from 'node:child_process';
|
|
102
|
+
import { createHash } from 'node:crypto';
|
|
83
103
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
84
104
|
|
|
85
105
|
const argv = process.argv.slice(2);
|
|
@@ -88,6 +108,8 @@ const tierArg = argv.find((a) => a.startsWith('--tier='));
|
|
|
88
108
|
const TIER = tierArg ? tierArg.slice('--tier='.length).trim().toUpperCase() : null;
|
|
89
109
|
const TIER_REQUIRES_ADR = TIER === 'M' || TIER === 'L' || TIER === 'XL';
|
|
90
110
|
const REQUIRE_REQUIREMENTS = argv.includes('--require-requirements');
|
|
111
|
+
const NO_REQUIRE_TASK_LINES = argv.includes('--no-require-task-lines');
|
|
112
|
+
const REQUIRE_TASK_LINES = !NO_REQUIRE_TASK_LINES;
|
|
91
113
|
const dirArg = argv.find((a) => !a.startsWith('--'));
|
|
92
114
|
const FDIR = resolve(dirArg && dirArg !== '' ? (isAbsolute(dirArg) ? dirArg : join(process.cwd(), dirArg)) : process.cwd());
|
|
93
115
|
const planPath = join(FDIR, '06_implementation_plan.md');
|
|
@@ -105,6 +127,7 @@ const safe = (v) => String(v)
|
|
|
105
127
|
.slice(0, 300);
|
|
106
128
|
const out = (s) => console.log(s);
|
|
107
129
|
const notEstablished = (why) => { out(`K2 plan-completeness: NOT-ESTABLISHED — ${safe(why)}`); process.exit(3); };
|
|
130
|
+
if (NO_REQUIRE_TASK_LINES && argv.includes('--require-task-lines')) notEstablished('both --require-task-lines and --no-require-task-lines were passed — pick one reader');
|
|
108
131
|
let failures = [], warnings = [], skips = [];
|
|
109
132
|
|
|
110
133
|
// Lead delta after Codex rounds 2+3 (2026-09-16, plan-inherits-requirements): on the DECLARATION side
|
|
@@ -128,6 +151,54 @@ if (!existsSync(FDIR)) notEstablished(`feature dir absent: ${FDIR}`);
|
|
|
128
151
|
if (!existsSync(planPath)) notEstablished('06_implementation_plan.md absent');
|
|
129
152
|
const plan = readFileSync(planPath, 'utf-8');
|
|
130
153
|
if (plan.trim().length < 200) notEstablished('plan suspiciously small (<200 chars)');
|
|
154
|
+
// The EXPECTED_CODE_TARGETS block as C3 reads it (C3 below reuses THIS match — one reader).
|
|
155
|
+
const blockM = plan.match(/EXPECTED_CODE_TARGETS:\s*\n((?:\s*[-*]\s*.+\n?)+)/);
|
|
156
|
+
|
|
157
|
+
// `## Amendments` as C6 reads it: up to three leading spaces, two to four hashes, trailing text
|
|
158
|
+
// allowed; the section ends at the next level-1..4 heading. Returned as LINE indices over masked
|
|
159
|
+
// lines — [headingLine, endLine). One definition for C6 and planTaskText, or the two drift.
|
|
160
|
+
function findAmendmentsSection(maskedLines) {
|
|
161
|
+
let head = -1, end = -1;
|
|
162
|
+
for (let i = 0; i < maskedLines.length; i++) {
|
|
163
|
+
const pl = maskedLines[i];
|
|
164
|
+
if (head < 0 && /^ {0,3}#{2,4}\s+Amendments\b/.test(pl)) head = i;
|
|
165
|
+
else if (head >= 0 && end < 0 && /^ {0,3}#{1,4}\s/.test(pl)) end = i;
|
|
166
|
+
}
|
|
167
|
+
if (head >= 0 && end < 0) end = maskedLines.length;
|
|
168
|
+
return { head, end };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// C10 — the TASK LINES of the plan, the text C1/C8 cite against (the default reader).
|
|
172
|
+
// Masked first (fences and HTML comments blanked, unclosed fence hidden — the K2 reader policy),
|
|
173
|
+
// then: drop the `## Amendments` section and the EXPECTED_CODE_TARGETS block, and keep only
|
|
174
|
+
// headings, list items and table rows. Table rows are kept deliberately, beyond the backlog's
|
|
175
|
+
// «headings/list items»: task tables (`| T1 | … |`) are a corpus form — MEASURED 2026-09-27,
|
|
176
|
+
// dropping them turns 12 more green plans red in pipeline mode (67 vs 55) and 26 more in WARN mode.
|
|
177
|
+
// NAMED LIMIT: a list item or table row that is itself prose («- Note: ADR-001 governs …») still
|
|
178
|
+
// counts — the reader tells structure from prose, not a task from a remark.
|
|
179
|
+
function planTaskText(planText) {
|
|
180
|
+
const lines = maskMarkdown(planText, { unclosed: 'mask' }).split('\n');
|
|
181
|
+
const am = findAmendmentsSection(lines);
|
|
182
|
+
let tFrom = -1, tTo = -1;
|
|
183
|
+
if (blockM) {
|
|
184
|
+
tFrom = planText.slice(0, blockM.index).split('\n').length - 1;
|
|
185
|
+
tTo = planText.slice(0, blockM.index + blockM[0].length - 1).split('\n').length - 1;
|
|
186
|
+
}
|
|
187
|
+
const keep = [];
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
if (am.head >= 0 && i >= am.head && i < am.end) continue;
|
|
190
|
+
if (tFrom >= 0 && i >= tFrom && i <= tTo) continue;
|
|
191
|
+
const l = lines[i];
|
|
192
|
+
if (/^ {0,3}#{1,6}\s/.test(l) || /^\s*(?:[-*+]|\d+[.)])\s+/.test(l) || /^\s*\|/.test(l)) keep.push(l);
|
|
193
|
+
}
|
|
194
|
+
return keep.join('\n');
|
|
195
|
+
}
|
|
196
|
+
const planTasks = planTaskText(plan);
|
|
197
|
+
// What C1/C8 cite against, and the ids that pass the raw read but have no task line (C10).
|
|
198
|
+
const citeText = REQUIRE_TASK_LINES ? planTasks : plan;
|
|
199
|
+
const outsideTasksOnly = { C1: [], C8: [] };
|
|
200
|
+
const citedOutsideOnly = (re) => re.test(plan) && !re.test(planTasks);
|
|
201
|
+
|
|
131
202
|
const adrFiles = existsSync(adrDir) ? readdirSync(adrDir).filter(f => f.endsWith('.md')).sort() : [];
|
|
132
203
|
const planClaimsAdrWork = /\bADR-\d+/.test(plan);
|
|
133
204
|
if (adrFiles.length === 0 && planClaimsAdrWork) notEstablished('no ADR files under 03_adr/, yet the plan cites ADR-<n> — completeness cannot be established');
|
|
@@ -235,7 +306,8 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
|
|
|
235
306
|
for (const n of headingNums) {
|
|
236
307
|
const id = `ADR-${n}`;
|
|
237
308
|
const re = new RegExp(`ADR-0*${Number(n)}\\b`);
|
|
238
|
-
if (!re.test(
|
|
309
|
+
if (!re.test(citeText)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it${citedOutsideOnly(re) ? ' (cited only outside task lines)' : ''}`);
|
|
310
|
+
else if (citedOutsideOnly(re)) outsideTasksOnly.C1.push(id);
|
|
239
311
|
}
|
|
240
312
|
continue;
|
|
241
313
|
}
|
|
@@ -243,7 +315,8 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
|
|
|
243
315
|
warnings.push(`C1: ${safe(f)} has no ADR-NNN heading — falling back to the filename prefix`);
|
|
244
316
|
const id = `ADR-${m[1]}`;
|
|
245
317
|
const re = new RegExp(`ADR-0*${Number(m[1])}\\b`);
|
|
246
|
-
if (!re.test(
|
|
318
|
+
if (!re.test(citeText)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it${citedOutsideOnly(re) ? ' (cited only outside task lines)' : ''}`);
|
|
319
|
+
else if (citedOutsideOnly(re)) outsideTasksOnly.C1.push(id);
|
|
247
320
|
}
|
|
248
321
|
|
|
249
322
|
// C2 — every Confirmation-listed test file path appears in the plan
|
|
@@ -334,7 +407,7 @@ function classifyTargetPath(path) {
|
|
|
334
407
|
}
|
|
335
408
|
|
|
336
409
|
// C3 — EXPECTED_CODE_TARGETS block, line-level validation
|
|
337
|
-
const
|
|
410
|
+
const listedTargets = new Set();
|
|
338
411
|
if (!blockM) failures.push('C3: no EXPECTED_CODE_TARGETS: block in the plan');
|
|
339
412
|
else {
|
|
340
413
|
const lines = blockM[1].split('\n').map(s => s.trim()).filter(Boolean);
|
|
@@ -343,6 +416,81 @@ else {
|
|
|
343
416
|
const path = ln.replace(/^[-*]\s*/, '').replace(/`/g, '').trim();
|
|
344
417
|
const reasons = classifyTargetPath(path);
|
|
345
418
|
if (reasons.length) failures.push(`C3: target line rejected: "${safe(ln)}" — ${reasons.join(', ')}`);
|
|
419
|
+
else listedTargets.add(path);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// C9 — every byte-identical copy is listed or carries a reasoned waiver (ADR-001).
|
|
424
|
+
const TWIN_EXCLUDED_PREFIXES = ['out/', '.claude/worktrees/', 'node_modules/'];
|
|
425
|
+
{
|
|
426
|
+
const excluded = (path) => TWIN_EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix) || path.includes('/' + prefix));
|
|
427
|
+
const waivers = [];
|
|
428
|
+
for (const line of maskMarkdown(plan, { unclosed: 'mask' }).split('\n')) {
|
|
429
|
+
if (!/^\s*(?:[-*]\s*)?TWIN_NOT_A_TARGET:/.test(line)) continue;
|
|
430
|
+
const match = line.match(/^\s*(?:[-*]\s*)?TWIN_NOT_A_TARGET:\s*`?([^\s`]+)`?\s*(?:—|--)\s*(\S.*)$/);
|
|
431
|
+
if (!match) {
|
|
432
|
+
failures.push(`C9: waiver "${safe(line.trim())}" carries no reason — a waiver without a reason is an allowlist entry`);
|
|
433
|
+
} else waivers.push({ path: match[1], used: false });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let tracked = null;
|
|
437
|
+
try {
|
|
438
|
+
tracked = execFileSync('git', ['ls-files', '-z'], {
|
|
439
|
+
cwd: process.cwd(), encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024,
|
|
440
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
441
|
+
}).split('\0').filter(Boolean);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
warnings.push(`C9: twin scan had no input — git ls-files failed in ${safe(process.cwd())}: ${safe(error.message)}`);
|
|
444
|
+
}
|
|
445
|
+
if (tracked !== null) {
|
|
446
|
+
// Stat the universe first; only size matches ever reach readFileSync / md5.
|
|
447
|
+
const sizes = new Map();
|
|
448
|
+
for (const path of new Set([...tracked, ...listedTargets])) {
|
|
449
|
+
if (excluded(path)) continue;
|
|
450
|
+
try {
|
|
451
|
+
const stat = statSync(path);
|
|
452
|
+
if (stat.isFile()) sizes.set(path, stat.size);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
// Missing targets are new files; tracked files can also have been deleted locally.
|
|
455
|
+
if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') warnings.push(`C9: cannot stat ${safe(path)}: ${safe(error.message)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const targetSizes = new Set([...listedTargets].map((path) => sizes.get(path)).filter((size) => size > 0));
|
|
459
|
+
const hashes = new Map();
|
|
460
|
+
const twinsByHash = new Map();
|
|
461
|
+
for (const [path, size] of sizes) {
|
|
462
|
+
if (!targetSizes.has(size)) continue;
|
|
463
|
+
try { hashes.set(path, createHash('md5').update(readFileSync(path)).digest('hex')); }
|
|
464
|
+
catch (error) { warnings.push(`C9: cannot hash ${safe(path)}: ${safe(error.message)}`); }
|
|
465
|
+
}
|
|
466
|
+
for (const path of tracked) {
|
|
467
|
+
const hash = hashes.get(path);
|
|
468
|
+
if (hash === undefined) continue;
|
|
469
|
+
if (!twinsByHash.has(hash)) twinsByHash.set(hash, []);
|
|
470
|
+
twinsByHash.get(hash).push(path);
|
|
471
|
+
}
|
|
472
|
+
let unlisted = 0;
|
|
473
|
+
for (const target of listedTargets) {
|
|
474
|
+
for (const twin of twinsByHash.get(hashes.get(target)) ?? []) {
|
|
475
|
+
if (twin === target) continue;
|
|
476
|
+
let waived = false;
|
|
477
|
+
for (const waiver of waivers) {
|
|
478
|
+
if (waiver.path.endsWith('/') ? twin.startsWith(waiver.path) : twin === waiver.path) {
|
|
479
|
+
waiver.used = true;
|
|
480
|
+
waived = true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (listedTargets.has(twin) || waived) continue;
|
|
484
|
+
unlisted++;
|
|
485
|
+
// Keep pairs separate: safe() truncates each echoed value at 300 characters.
|
|
486
|
+
failures.push(`C9: target ${safe(target)} has a byte-identical twin the plan does not list: ${safe(twin)} — list it or waive it (TWIN_NOT_A_TARGET: ${safe(twin)} — <why>)`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const waiver of waivers) if (!waiver.used) warnings.push(`C9: unused waiver ${safe(waiver.path)}`);
|
|
490
|
+
if (unlisted === 0) {
|
|
491
|
+
const onDisk = [...listedTargets].filter((path) => sizes.has(path)).length;
|
|
492
|
+
out(`NOTE C9: twin scan over ${listedTargets.size} target(s), ${onDisk} on disk, 0 unlisted twins (byte-identical only; drifted copies need identity tests)`);
|
|
493
|
+
}
|
|
346
494
|
}
|
|
347
495
|
}
|
|
348
496
|
|
|
@@ -393,16 +541,14 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
393
541
|
// phantom amendment or hand a real testless one someone else's marker. Third fence-blindness
|
|
394
542
|
// found in a checker today, so it is closed here by construction rather than by care.
|
|
395
543
|
const planLines = maskMarkdown(plan, { unclosed: 'mask' }).split('\n');
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
if (sectionStart >= 0 && sectionEnd < 0) sectionEnd = plan.length;
|
|
544
|
+
// The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
|
|
545
|
+
// hashes, and trailing text allowed. C6 required exactly `##` with nothing after, so the two
|
|
546
|
+
// tools disagreed about where the section even IS — the divergence this feature exists to end.
|
|
547
|
+
// The section boundary itself is `findAmendmentsSection` — shared with C10, one answer.
|
|
548
|
+
const amRange = findAmendmentsSection(planLines);
|
|
549
|
+
const lineOffset = (idx) => planLines.slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0);
|
|
550
|
+
const sectionStart = amRange.head < 0 ? -1 : lineOffset(amRange.head + 1);
|
|
551
|
+
const sectionEnd = amRange.head < 0 ? -1 : (amRange.end < planLines.length ? lineOffset(amRange.end) : plan.length);
|
|
406
552
|
// Sliced from the MASKED text so the offsets computed above line up with what is scanned.
|
|
407
553
|
const maskedPlan = planLines.join('\n');
|
|
408
554
|
const amSection = sectionStart >= 0 ? maskedPlan.slice(sectionStart, sectionEnd) : '';
|
|
@@ -417,7 +563,21 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
417
563
|
// bullet-less row was invisible here while being a row there (so it evaded this check), and
|
|
418
564
|
// this side lacked the range guard, so «AM-1..AM-4 are covered elsewhere» was falsely
|
|
419
565
|
// reported as a definition. One shape, one meaning.
|
|
420
|
-
|
|
566
|
+
// ЯЧЕЙКА ТАБЛИЦЫ ВНЕ СЕКЦИИ — НЕ ОПРЕДЕЛЕНИЕ. Внутри `## Amendments` строка таблицы
|
|
567
|
+
// законно объявляет поправку, поэтому там `|` остаётся допустимым началом строки. Снаружи
|
|
568
|
+
// же таблица — это СВОДКА, ссылающаяся на поправки, определённые в другом месте, и читать
|
|
569
|
+
// её как определение значит отказывать плану за оглавление.
|
|
570
|
+
// ИЗМЕРЕНО 2026-09-05: четыре ложных FAIL за один день на четырёх разных фичах
|
|
571
|
+
// (narrated-error-must-be-taught, fa-phase-statusline, core-boundary-guard,
|
|
572
|
+
// run-registry-liveness); каждый стоил ручной правки ведущего и перезапуска конвейера,
|
|
573
|
+
// то есть 5–10 минут и один цикл роутера. Один планировщик дошёл до того, что вписал в
|
|
574
|
+
// план оговорку «no line of this paragraph may begin with AM-» — прибор начал
|
|
575
|
+
// диктовать людям форму прозы, а это уже не проверка, а суеверие.
|
|
576
|
+
// ОДИН механизм, а не два: `|` УБРАН из класса начал строки. Первая редакция этой правки
|
|
577
|
+
// добавляла ещё и отдельную проверку `isTableRow`, и мутация, снимавшая только её,
|
|
578
|
+
// оставалась ЭКВИВАЛЕНТНОЙ — 77 тестов из 77 зелёные при «снятой» починке. Дублирующая
|
|
579
|
+
// защита не усиливает гейт, она прячет от мутационной пробы, что именно держит свойство.
|
|
580
|
+
const isDef = /^\s*(?:[-*]\s*)?\*{0,2}AM-(?:CP-)?\d+\*{0,2}\b(?!\s*\.)/.test(pl);
|
|
421
581
|
const inSection = sectionStart >= 0 && cursor2 >= sectionStart && cursor2 < sectionEnd;
|
|
422
582
|
if (isDef && !inSection) {
|
|
423
583
|
const tok = (/AM-(?:CP-)?\d+/.exec(pl) || ['AM-?'])[0];
|
|
@@ -479,7 +639,10 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
479
639
|
}
|
|
480
640
|
if (superseded) break;
|
|
481
641
|
}
|
|
482
|
-
|
|
642
|
+
// Сообщение называет ТУ ЖЕ форму, которую требует MARK выше. Прежняя редакция обещала
|
|
643
|
+
// простое `→ test <name>`, а проверка требовала имя и файл в обратных кавычках — человек,
|
|
644
|
+
// написавший ровно то, что просило сообщение, получал отказ снова и не понимал, почему.
|
|
645
|
+
if (!hasTest && !superseded) failures.push(`C6: ${defs[k].id} carries neither \`\u2192 test \`<имя>\` in \`<файл>\`\` (обратные кавычки обязательны, как и слово in) nor \`superseded by AM-N\` — an amendment without a confirmation is a wish, and a retracted one must say its successor`);
|
|
483
646
|
}
|
|
484
647
|
}
|
|
485
648
|
}
|
|
@@ -522,7 +685,9 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
522
685
|
if (reqIds.length === 0) {
|
|
523
686
|
warnings.push('C8: 01_requirements.md declares NO requirement ids in the contract shapes (FR-N / NFR-N / AC-N / C-N at line start) — nothing to cover');
|
|
524
687
|
} else {
|
|
525
|
-
const
|
|
688
|
+
const reqRe = (id) => new RegExp('\\b' + escapeReqId(id) + '\\b');
|
|
689
|
+
const missing = reqIds.filter((id) => !reqRe(id).test(citeText));
|
|
690
|
+
for (const id of reqIds) if (!missing.includes(id) && citedOutsideOnly(reqRe(id))) outsideTasksOnly.C8.push(id);
|
|
526
691
|
if (missing.length === 0) {
|
|
527
692
|
// Silence is not a verdict (K7): a check that ran and found nothing wrong must still print,
|
|
528
693
|
// or a reader cannot tell "C8 ran clean" from "C8 never ran". Deliberately NOT a PASS/FAIL/
|
|
@@ -530,7 +695,7 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
530
695
|
// mid-stream is exactly the G-F1 forgery class this script's `safe()` already defends against.
|
|
531
696
|
out(`NOTE C8: all ${reqIds.length} requirement ids referenced`);
|
|
532
697
|
} else if (REQUIRE_REQUIREMENTS) {
|
|
533
|
-
for (const id of missing) failures.push(`C8: ${id} (01_requirements.md) is not referenced by the plan`);
|
|
698
|
+
for (const id of missing) failures.push(`C8: ${id} (01_requirements.md) is not referenced by the plan${citedOutsideOnly(reqRe(id)) ? ' (cited only outside task lines)' : ''}`);
|
|
534
699
|
} else {
|
|
535
700
|
const shown = missing.slice(0, 12);
|
|
536
701
|
const more = missing.length > 12 ? `, …and ${missing.length - 12} more` : '';
|
|
@@ -540,6 +705,23 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
540
705
|
}
|
|
541
706
|
}
|
|
542
707
|
|
|
708
|
+
// C10 — ids that C1/C8 counted as covered, but whose ONLY citations sit outside task lines
|
|
709
|
+
// (prose, fenced code, an HTML comment, `## Amendments`, the EXPECTED_CODE_TARGETS block). Reached
|
|
710
|
+
// ONLY under --no-require-task-lines: by default C1/C8 already read task lines, so this list is
|
|
711
|
+
// empty by construction. One line per check, ids capped like C8's WARN.
|
|
712
|
+
for (const check of ['C1', 'C8']) {
|
|
713
|
+
const ids = [...new Set(outsideTasksOnly[check])];
|
|
714
|
+
if (ids.length === 0) continue;
|
|
715
|
+
const shown = ids.slice(0, 12);
|
|
716
|
+
const more = ids.length > 12 ? `, …and ${ids.length - 12} more` : '';
|
|
717
|
+
// Say what dropping the opt-out would do in THIS invocation (Codex astra r1 MINOR): C1 always
|
|
718
|
+
// FAILs by default; C8 FAILs only when --require-requirements is also passed — otherwise a WARN.
|
|
719
|
+
const consequence = check === 'C8' && !REQUIRE_REQUIREMENTS
|
|
720
|
+
? 'without --no-require-task-lines this stays a C8 WARN (C8 is enforced only with --require-requirements; add that flag and the default reader fails it)'
|
|
721
|
+
: `these ids count as covered only because of --no-require-task-lines; the default makes this a ${check} FAIL`;
|
|
722
|
+
warnings.push(`C10: ${ids.length} ${check} id(s) are cited only outside task lines (prose, fenced code, HTML comment, ## Amendments, EXPECTED_CODE_TARGETS): ${shown.join(', ')}${more} — a mention is not a task; ${consequence}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
543
725
|
// C5 — Inputs read line
|
|
544
726
|
if (!/Inputs read:/i.test(plan)) warnings.push('C5: no "Inputs read:" line (wave-2 seam, WARN only)');
|
|
545
727
|
else for (const need of ['03_adr','05_architecture']) if (!plan.includes(need)) warnings.push(`C5: Inputs read line missing ${need}`);
|