@dzhechkov/keysarium 1.7.6 → 1.7.8
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 +16 -8
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/package.json +2 -2
- package/sbom.json +27 -7
- package/templates/.claude/skills/feature-adr/SKILL.md +79 -1
- 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 +69 -1
- 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/.dz-manifest.json
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
"version": 3,
|
|
4
4
|
"pack": "keysarium",
|
|
5
5
|
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "LICENSE",
|
|
8
|
+
"sha256": "3b93f7070f6843f37e0cae255f7caff22b466a4017fd2020d7d3216237e5a701"
|
|
9
|
+
},
|
|
6
10
|
{
|
|
7
11
|
"path": "README.md",
|
|
8
|
-
"sha256": "
|
|
12
|
+
"sha256": "384c02be209742869d2a88e87746abad359d4099acb0146a68b9b86dd13d47dd"
|
|
9
13
|
},
|
|
10
14
|
{
|
|
11
15
|
"path": "README.ru.md",
|
|
@@ -17,7 +21,7 @@
|
|
|
17
21
|
},
|
|
18
22
|
{
|
|
19
23
|
"path": "package.json",
|
|
20
|
-
"sha256": "
|
|
24
|
+
"sha256": "1dd2e14eb1a9f3108c5bf607490bf36127a655cfc8aa89136a2f164332757c6c"
|
|
21
25
|
},
|
|
22
26
|
{
|
|
23
27
|
"path": "src/cli.js",
|
|
@@ -345,7 +349,7 @@
|
|
|
345
349
|
},
|
|
346
350
|
{
|
|
347
351
|
"path": "templates/.claude/skills/feature-adr/SKILL.md",
|
|
348
|
-
"sha256": "
|
|
352
|
+
"sha256": "28c265603481f976a52ea4c627e11194f3c0283801ea93d02cbb9f4248b43c68"
|
|
349
353
|
},
|
|
350
354
|
{
|
|
351
355
|
"path": "templates/.claude/skills/feature-adr/examples/sample-feature-output.md",
|
|
@@ -381,15 +385,15 @@
|
|
|
381
385
|
},
|
|
382
386
|
{
|
|
383
387
|
"path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
|
|
384
|
-
"sha256": "
|
|
388
|
+
"sha256": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
|
|
385
389
|
},
|
|
386
390
|
{
|
|
387
391
|
"path": "templates/.claude/skills/feature-adr/modules/07-code.md",
|
|
388
|
-
"sha256": "
|
|
392
|
+
"sha256": "db79f8d026cc47edd1e5d2f30d0e1455c55c570dd022d549d440985a3e5940a5"
|
|
389
393
|
},
|
|
390
394
|
{
|
|
391
395
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
392
|
-
"sha256": "
|
|
396
|
+
"sha256": "d7a967926feec9b0ec173ac7042ae3abd368644d8e1dba9a81a770794417fcb3"
|
|
393
397
|
},
|
|
394
398
|
{
|
|
395
399
|
"path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
|
|
@@ -479,9 +483,13 @@
|
|
|
479
483
|
"path": "templates/.claude/skills/feature-adr/references/qe-checklist.md",
|
|
480
484
|
"sha256": "238d8896996dc53559f58b24aa7eca966cc8845c3b9dda6d982c717c657b91e4"
|
|
481
485
|
},
|
|
486
|
+
{
|
|
487
|
+
"path": "templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs",
|
|
488
|
+
"sha256": "19c3991bfb5e88205c9ff6d4d44037c136a190581bab199d3928196e4616fe15"
|
|
489
|
+
},
|
|
482
490
|
{
|
|
483
491
|
"path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
484
|
-
"sha256": "
|
|
492
|
+
"sha256": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
|
|
485
493
|
},
|
|
486
494
|
{
|
|
487
495
|
"path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
@@ -989,5 +997,5 @@
|
|
|
989
997
|
}
|
|
990
998
|
]
|
|
991
999
|
},
|
|
992
|
-
"signature": "
|
|
1000
|
+
"signature": "fTGSL3MpU5nWefu5K27ZehkXuMSbnESMohNdhhWggVsNtHkbsRP+fYgSdHRJEK7Qx86dOYvgywTLcveS5vjZCA=="
|
|
993
1001
|
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dzhechko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
# @dzhechkov/keysarium
|
|
2
2
|
|
|
3
|
+
Current package version: `1.7.8`. <!-- dz:version -->
|
|
4
|
+
|
|
5
|
+
Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/keysarium
|
|
6
|
+
|
|
3
7
|
**AI Case Research Toolkit for Claude Code**
|
|
4
8
|
|
|
5
9
|
Full 7-phase pipeline for AI case studies, hackathons, and casariums. Provides skills, commands, rules, shards, agent templates, and brain portability for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
6
10
|
|
|
11
|
+
Bundled feature-adr usage guidance records observed stage metadata/source IDs through existing
|
|
12
|
+
writers and reads explicit Wf/FA project evidence. Unknown dimensions and full prices stay null;
|
|
13
|
+
independent verification, family estimates and billing absence remain visible.
|
|
14
|
+
|
|
15
|
+
The bundled feature-adr Step 8 now carries explicit existing round/run/task identity through its plain
|
|
16
|
+
Claude bridge contract using the execution project. Preflight conflicts refuse before review; matching
|
|
17
|
+
and legacy-window assurance remain distinct. Native Workflow QE does not add a duplicate bridge review.
|
|
18
|
+
|
|
19
|
+
The bundled feature-adr Code module now ships the shared `scripts/build-coder-context.mjs` helper.
|
|
20
|
+
It supplies bounded literal requirements, plan tasks and ADR Decision/Confirmation; plain coding
|
|
21
|
+
reads or embeds its successful block, and Workflow fingerprints current inputs before code reuse.
|
|
22
|
+
Required missing or incomplete context refuses coding. This helper is separate from advisory recall.
|
|
23
|
+
|
|
7
24
|
---
|
|
8
25
|
|
|
9
26
|
|
|
@@ -473,3 +490,22 @@ harness-core's `src/markdown-masker.ts`. It runs without a core build. Amendment
|
|
|
473
490
|
and K2 share the parser while retaining their existing unclosed-block and indentation policies.
|
|
474
491
|
The four-space indented-code gap remains open for amendment checks and K2; swarm briefs retain their
|
|
475
492
|
existing masking of indented code. Versions are unchanged in this staged change.
|
|
493
|
+
|
|
494
|
+
### Codex companion for feature-adr
|
|
495
|
+
|
|
496
|
+
`dz statusline --watch --project "/path/to/worktree" --brain "/path/to/shared-brain"
|
|
497
|
+
--slug "feature-slug" --run-id "stable-run-id"` adds an explicitly launched adjacent terminal
|
|
498
|
+
companion, including Plain runs. Until installed, invoke the worktree-built
|
|
499
|
+
`node packages/@dzhechkov/harness-cli/dist/bin.js statusline --watch ...`. This extends the existing
|
|
500
|
+
command inventory. Canonical feature-adr guidance supplies the quoted producer/observer recipe: record
|
|
501
|
+
a stable run ID and actual tier at the START of each step, then `done` on real completion; recall/teach
|
|
502
|
+
remain scoped to the shared brain. Project run state and brain counts are separate. One slug retains
|
|
503
|
+
one latest run; report freshness is not process liveness and stage position is not passed gates.
|
|
504
|
+
|
|
505
|
+
The readonly companion requires a dedicated stdout TTY, uses serial 2-second refreshes (0.25–60
|
|
506
|
+
allowed), sanitizes/clips text and handles resize. Below 40x8 it shows a size warning. Ctrl-C/SIGTERM
|
|
507
|
+
exit 0; output failure 1; invalid/piped watch 2. Failed/absent counts and optional values are explicit;
|
|
508
|
+
watch v1 ETA is unavailable and global source inventory omitted. No stdin/raw mode, models, logical
|
|
509
|
+
store writes or automatic terminal/settings changes. Normal SQLite ephemeral WAL/SHM sidecars are
|
|
510
|
+
permitted. One-shot Claude text/JSON/ETA remain unchanged. Codex native footer capability is not
|
|
511
|
+
asserted; parity names manual `dz statusline --watch` access.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/keysarium",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"scripts": {},
|
|
5
5
|
"description": "AI Case Research Toolkit for Claude Code — full 7-phase pipeline with feature-adr, dream cycles, background workers, multi-platform support, reward learning, and Cloud.ru AI Factory mapping (--ai-factory flag)",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"url": "git+https://github.com/djd1m/dz-harness.git",
|
|
39
39
|
"directory": "packages/@dzhechkov/keysarium"
|
|
40
40
|
},
|
|
41
|
-
"homepage": "https://
|
|
41
|
+
"homepage": "https://aicoding.space",
|
|
42
42
|
"bugs": {
|
|
43
43
|
"url": "https://github.com/djd1m/dz-harness/issues"
|
|
44
44
|
},
|
package/sbom.json
CHANGED
|
@@ -9,13 +9,23 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"components": [
|
|
12
|
+
{
|
|
13
|
+
"type": "file",
|
|
14
|
+
"name": "LICENSE",
|
|
15
|
+
"hashes": [
|
|
16
|
+
{
|
|
17
|
+
"alg": "SHA-256",
|
|
18
|
+
"content": "3b93f7070f6843f37e0cae255f7caff22b466a4017fd2020d7d3216237e5a701"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
12
22
|
{
|
|
13
23
|
"type": "file",
|
|
14
24
|
"name": "README.md",
|
|
15
25
|
"hashes": [
|
|
16
26
|
{
|
|
17
27
|
"alg": "SHA-256",
|
|
18
|
-
"content": "
|
|
28
|
+
"content": "384c02be209742869d2a88e87746abad359d4099acb0146a68b9b86dd13d47dd"
|
|
19
29
|
}
|
|
20
30
|
]
|
|
21
31
|
},
|
|
@@ -49,7 +59,7 @@
|
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
"name": "dz:canonical-json-sha256-v2",
|
|
52
|
-
"value": "
|
|
62
|
+
"value": "1dd2e14eb1a9f3108c5bf607490bf36127a655cfc8aa89136a2f164332757c6c"
|
|
53
63
|
}
|
|
54
64
|
]
|
|
55
65
|
},
|
|
@@ -869,7 +879,7 @@
|
|
|
869
879
|
"hashes": [
|
|
870
880
|
{
|
|
871
881
|
"alg": "SHA-256",
|
|
872
|
-
"content": "
|
|
882
|
+
"content": "28c265603481f976a52ea4c627e11194f3c0283801ea93d02cbb9f4248b43c68"
|
|
873
883
|
}
|
|
874
884
|
]
|
|
875
885
|
},
|
|
@@ -959,7 +969,7 @@
|
|
|
959
969
|
"hashes": [
|
|
960
970
|
{
|
|
961
971
|
"alg": "SHA-256",
|
|
962
|
-
"content": "
|
|
972
|
+
"content": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
|
|
963
973
|
}
|
|
964
974
|
]
|
|
965
975
|
},
|
|
@@ -969,7 +979,7 @@
|
|
|
969
979
|
"hashes": [
|
|
970
980
|
{
|
|
971
981
|
"alg": "SHA-256",
|
|
972
|
-
"content": "
|
|
982
|
+
"content": "db79f8d026cc47edd1e5d2f30d0e1455c55c570dd022d549d440985a3e5940a5"
|
|
973
983
|
}
|
|
974
984
|
]
|
|
975
985
|
},
|
|
@@ -979,7 +989,7 @@
|
|
|
979
989
|
"hashes": [
|
|
980
990
|
{
|
|
981
991
|
"alg": "SHA-256",
|
|
982
|
-
"content": "
|
|
992
|
+
"content": "d7a967926feec9b0ec173ac7042ae3abd368644d8e1dba9a81a770794417fcb3"
|
|
983
993
|
}
|
|
984
994
|
]
|
|
985
995
|
},
|
|
@@ -1203,13 +1213,23 @@
|
|
|
1203
1213
|
}
|
|
1204
1214
|
]
|
|
1205
1215
|
},
|
|
1216
|
+
{
|
|
1217
|
+
"type": "file",
|
|
1218
|
+
"name": "templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs",
|
|
1219
|
+
"hashes": [
|
|
1220
|
+
{
|
|
1221
|
+
"alg": "SHA-256",
|
|
1222
|
+
"content": "19c3991bfb5e88205c9ff6d4d44037c136a190581bab199d3928196e4616fe15"
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
},
|
|
1206
1226
|
{
|
|
1207
1227
|
"type": "file",
|
|
1208
1228
|
"name": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
1209
1229
|
"hashes": [
|
|
1210
1230
|
{
|
|
1211
1231
|
"alg": "SHA-256",
|
|
1212
|
-
"content": "
|
|
1232
|
+
"content": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
|
|
1213
1233
|
}
|
|
1214
1234
|
]
|
|
1215
1235
|
},
|
|
@@ -562,13 +562,91 @@ only the statusline:
|
|
|
562
562
|
|
|
563
563
|
**Record the panel at the START of every step, not only at Steps 0/8/9.** The panel shows the last step
|
|
564
564
|
that reported; a pipeline that reports three times per run shows a stale step for most of its life. Emit
|
|
565
|
-
`dz statusline --fa-record --slug <slug> --step "<Step N Name>" --recalled <n> --stored <n>` as the first
|
|
565
|
+
`dz statusline --fa-record --project "<worktree>" --slug "<slug>" --run-id "<stable-run-id>" --tier M --step "<Step N Name>" --recalled <n> --stored <n>` as the first
|
|
566
566
|
action of each step. The recall/teach counts only change at Steps 0/8/9; the *step label* changes at every
|
|
567
567
|
one of them.
|
|
568
568
|
|
|
569
569
|
*Honesty note:* the panel is live only insofar as the pipeline records state — it reflects what the
|
|
570
570
|
pipeline actually did with the loop (recalls that ran, stores that landed), not an aspirational count.
|
|
571
571
|
|
|
572
|
+
### Plain observed usage receipts
|
|
573
|
+
|
|
574
|
+
Use the existing witnessed writer for usage you actually observed. At a real stage boundary,
|
|
575
|
+
capture the execution project, stable run/task, verbatim stage, actual model/family/role, attempt,
|
|
576
|
+
tier/mode and source window/IDs available to this host. Missing fields remain null with their reason;
|
|
577
|
+
do not infer input/cache from total, price from a model family, or tokens from invocation budgets.
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
dz feature-adr-record --kind ledger --stage "$CURRENT_STAGE" --project "$EXECUTION_PROJECT" \
|
|
581
|
+
--row "$OBSERVED_STAGE_ROW_JSON" --rollout-id "$ACTUAL_SESSION_ID" --turn-id "$ACTUAL_TURN_ID" --json
|
|
582
|
+
dz usage --by-stage --project "$EXECUTION_PROJECT" --source fa-ledger --run "$STABLE_RUN_ID" --json
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
`OBSERVED_STAGE_ROW_JSON` is real host metadata: `runId`, `taskId`, `stage`, `model`, `family`, `role`,
|
|
586
|
+
`attempt`, `tier`, `mode`, `tokens`/dimensions when observed, and optional separate `estimate` with
|
|
587
|
+
tokens/costUsd/method/source/capturedAt. Omit selectors not known; window/cwd/model-only correlation
|
|
588
|
+
is labelled legacy-window and cannot claim exact source verification. Source roots can be named with
|
|
589
|
+
`--codex-sessions`. Exact IDs are validated against existing receipts; no new IDs or recall/teach occur.
|
|
590
|
+
The source receipt subset is captured once; later source append cannot enlarge the old row.
|
|
591
|
+
Captured payload integrity and every pricing-bearing dimension must match the original scoped source.
|
|
592
|
+
A reported monetary amount belongs to one observation, not each expanded token receipt. Preserve an
|
|
593
|
+
actual observation ID/scope/basis in optional `reportedCostObservation: { id, scope, basis }` within
|
|
594
|
+
`--row` when known; otherwise a captured source scope supplies a stable identity and unrelated money
|
|
595
|
+
attribution stays unavailable. Reimports of the same observation count once; conflicting amounts are
|
|
596
|
+
diagnosed. Missing money differs from zero. Exports must avoid every selected authoritative source,
|
|
597
|
+
including custom run directories and symlink aliases. Invalid Claude counters retain nulls/diagnostics;
|
|
598
|
+
a declared invalid total cannot derive a replacement, and accounting validity does not rewrite the
|
|
599
|
+
actual generation outcome.
|
|
600
|
+
|
|
601
|
+
For Wf, use `--source workflow-budget --run <id>` and optional `--run-dir <dir>` for its existing
|
|
602
|
+
budget/trace/state. Auto source collisions require an explicit source; joined Wf summary projections
|
|
603
|
+
never add another cost. Preserve reported total basis and cache/reasoning subsets. Wf budget.spent
|
|
604
|
+
counts dispatch units; native Workflow's existing budget delta is output-only. Neither is raw total.
|
|
605
|
+
|
|
606
|
+
Reports separate conservation, expected inventory and independent amount verification. Without
|
|
607
|
+
a same-scope witness, verified totals are null even when reported values conserve. Unknown rates or
|
|
608
|
+
split keep primary estimated USD null; static family estimates, provider-reported USD and caller
|
|
609
|
+
pre-run estimates are separate, never current exact prices or billed amounts. Billing remains unobserved.
|
|
610
|
+
|
|
611
|
+
### Codex companion terminal (Plain included)
|
|
612
|
+
|
|
613
|
+
Open an adjacent terminal or a manual tmux split and launch the observer explicitly. Codex does not
|
|
614
|
+
have a dz native command-provider footer. Keep the producer's project, slug and stable run ID equal to
|
|
615
|
+
the observer's; use the actual complexity tier at the START of every active step. Brain is the shared
|
|
616
|
+
learning store for recall/teach, while project is the worktree containing run slots and branch.
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
# Set these to your actual absolute paths. Use the worktree build until the change is installed.
|
|
620
|
+
PANEL_CLI="/path/to/worktree/packages/@dzhechkov/harness-cli/dist/bin.js"
|
|
621
|
+
PANEL_PROJECT="/path/to/worktree"
|
|
622
|
+
PANEL_BRAIN="/path/to/canonical-brain"
|
|
623
|
+
PANEL_SLUG="feature-slug"
|
|
624
|
+
PANEL_RUN="feature-20261002-1" # choose once per invocation, retain at every step
|
|
625
|
+
node "$PANEL_CLI" statusline --watch --project "$PANEL_PROJECT" --brain "$PANEL_BRAIN" \
|
|
626
|
+
--slug "$PANEL_SLUG" --run-id "$PANEL_RUN" --interval 2
|
|
627
|
+
# In the producer terminal, at the START of each real step (example: tier M):
|
|
628
|
+
node "$PANEL_CLI" statusline --fa-record --project "$PANEL_PROJECT" --slug "$PANEL_SLUG" \
|
|
629
|
+
--run-id "$PANEL_RUN" --tier M --step "Step 7 Code" --recalled 3 --stored 0
|
|
630
|
+
# Only after the run actually completes; this is not a claim that QE passed:
|
|
631
|
+
node "$PANEL_CLI" statusline --fa-record --project "$PANEL_PROJECT" --slug "$PANEL_SLUG" \
|
|
632
|
+
--run-id "$PANEL_RUN" --tier M --step "done" --recalled 3 --stored 0
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Use `dz recall ... --project "$PANEL_BRAIN"` and `dz teach ... --project "$PANEL_BRAIN"` for actual
|
|
636
|
+
learning. Supply measured cumulative counters, not the example numbers. The observer counts its brain
|
|
637
|
+
source directly; the slot's producer pool is not a shared-brain inventory. One slug holds one latest
|
|
638
|
+
run, so a replacement makes an exact old selection missing. No selectors means visibly automatic
|
|
639
|
+
selection. Freshness measures producer-report age, not process liveness: fresh <30 minutes, stale
|
|
640
|
+
30–<90, expired >=90; completed remains completed. Stage position is not completed gates.
|
|
641
|
+
|
|
642
|
+
Watch v1 displays ETA unavailable, unknown optional values and unavailable failed/absent learning
|
|
643
|
+
sources, and omits ambiguous global source inventory. It uses escaped ASCII text, a dedicated stdout
|
|
644
|
+
TTY, 0.25–60 second intervals (default 2), and a size warning below 40 columns/8 rows. Ctrl-C/SIGTERM
|
|
645
|
+
exit 0; output failures exit 1; piped output and watch+JSON/install/record combinations exit 2.
|
|
646
|
+
Observation never reads stdin, runs models or writes logical store state. SQLite-managed ephemeral
|
|
647
|
+
WAL/SHM files are permitted; no application locks, repair or schema changes occur. One-shot Claude
|
|
648
|
+
statusline/JSON and its ETA keep their existing behavior. Do not start a terminal automatically.
|
|
649
|
+
|
|
572
650
|
### What changes with `--full-qe`
|
|
573
651
|
|
|
574
652
|
Full agentic-qe protocols for the same 9 core skills. No new agents, just deeper methodology.
|
|
@@ -235,6 +235,16 @@ C2 recognises JS/TS, pytest, Go, Rust, JVM and .NET test paths, extensible per p
|
|
|
235
235
|
|
|
236
236
|
Never proceed on a non-zero exit, and never treat empty output as a pass — the last line
|
|
237
237
|
(`K2 plan-completeness: PASS|FAIL|NOT-ESTABLISHED`) is the verdict, and its absence is not one.
|
|
238
|
+
|
|
239
|
+
**Where an id counts (the default since 2026-09-27, owner decision).** C1 and C8 read the plan's
|
|
240
|
+
TASK LINES only: a heading, a list item or a table row. An `ADR-<n>` or `FR-<n>` that appears only
|
|
241
|
+
in a prose paragraph, a fenced code block (the SPARC-GOAP ```yaml goal state included), an HTML
|
|
242
|
+
comment, the `## Amendments` section or the `EXPECTED_CODE_TARGETS:` block is a mention, not a task,
|
|
243
|
+
and the gate FAILs it with `(cited only outside task lines)`. Write the id on the FIRST line of the
|
|
244
|
+
task that implements it: a wrapped continuation line of a list item is not read either. Measured on the archive when the default changed: 56 of 102 plans that had
|
|
245
|
+
passed would fail this reader, so a plan written before that date may be red on a re-check — move
|
|
246
|
+
the citation onto a task line, or re-check that one plan with `--no-require-task-lines` and say so
|
|
247
|
+
in the checkpoint banner.
|
|
238
248
|
What it checks: C1 every ADR **decision** (a `# ADR-NNN` / `## ADR-NNN` heading INSIDE the file, not
|
|
239
249
|
just the filename prefix — a file with several headings owes several plan citations) has a plan task
|
|
240
250
|
citing it · C2 every ADR Confirmation test path is named in the plan · C3 the `EXPECTED_CODE_TARGETS:`
|
|
@@ -26,6 +26,35 @@ opus (complex code generation)
|
|
|
26
26
|
|
|
27
27
|
### 1. Pre-Implementation Checklist
|
|
28
28
|
|
|
29
|
+
### Current literal context (plain and delegated coding)
|
|
30
|
+
|
|
31
|
+
Before coding or delegating, run the installed helper beside this module. Set `CONTEXT_HELPER` to
|
|
32
|
+
the absolute `scripts/build-coder-context.mjs` path of the skill installation you are reading;
|
|
33
|
+
set `FEATURE_DIR` to the absolute target `features/<slug>` directory and use the actual tier:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node "$CONTEXT_HELPER" "$FEATURE_DIR" --tier=M
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The command emits exactly one JSON envelope and exits 0 only for `status: "complete"`. On any
|
|
40
|
+
nonzero exit, unavailable/incomplete status, malformed JSON or required missing/empty section,
|
|
41
|
+
stop before coding and repair the named input. Never paste a partial result as complete. A missing
|
|
42
|
+
helper requires restoring this skill installation, not inventing a replacement block.
|
|
43
|
+
|
|
44
|
+
For every delegated coder assignment, paste the successful envelope's literal `promptBlock` into
|
|
45
|
+
the actual prompt, then append the existing advisory decision-recall block once. Keep the source
|
|
46
|
+
paths below for deeper reading. For single-agent in-session coding, read this generated block
|
|
47
|
+
directly before implementing. Requirements and plan are included in full; each ADR supplies its
|
|
48
|
+
Decision and Confirmation with source labels. M/L/XL require at least one ADR; S can have none.
|
|
49
|
+
|
|
50
|
+
The same canonical helper is used by the programmatic Workflow before code checkpoint lookup.
|
|
51
|
+
It fingerprints full current inputs and binds the prompt separately, so changed documents cannot
|
|
52
|
+
reuse old code. The plain mode boundary is an executable helper command plus these required
|
|
53
|
+
read/embedding instructions; there is no separately automated plain dispatcher. Pure/fixture tests
|
|
54
|
+
do not establish a live model relay's authenticity. Helper bounds are 64 documents, 256 KiB/file,
|
|
55
|
+
1 MiB read and 96 KiB UTF-8 for the entire labelled block; exceeded bounds refuse without trimming.
|
|
56
|
+
These are document limits, not a new limit on the existing coder wrapper's final prompt.
|
|
57
|
+
|
|
29
58
|
Before writing any code:
|
|
30
59
|
- [ ] Read existing similar implementations in codebase
|
|
31
60
|
- [ ] Identify naming conventions (files, classes, functions, variables)
|
|
@@ -23,6 +23,30 @@ sonnet (analytical evaluation)
|
|
|
23
23
|
|
|
24
24
|
## Protocol
|
|
25
25
|
|
|
26
|
+
### Bridge identity for plain Step 8
|
|
27
|
+
|
|
28
|
+
When the plain host invokes the existing Claude review bridge, take round number, pipeline run and
|
|
29
|
+
task from the current round receipt/state in the execution project. Pass the fields actually present:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dz qe-bridge --family claude --slug "$FEATURE_SLUG" --project "$EXECUTION_PROJECT" \
|
|
33
|
+
--round "$CURRENT_ROUND" --round-run "$CURRENT_ROUND_RUN" --task "$CURRENT_TASK" \
|
|
34
|
+
--coder-family codex
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Do not mint missing identifiers or point `--project` at a separate learning brain. Explicit identity
|
|
38
|
+
must agree with one readable open round before any probe or review child; conflicts refuse with exit 2.
|
|
39
|
+
The bridge freezes that snapshot for the signoff. Signoff `roundRun` is the pipeline run; its existing
|
|
40
|
+
`runId` identifies the separate bridge invocation and audit filenames. No-flags standalone reviews
|
|
41
|
+
retain best-effort lookup and visibly unbound provenance when authority is absent or unavailable.
|
|
42
|
+
|
|
43
|
+
Round close validates every present identity field before selection. Full, partial and genuinely
|
|
44
|
+
identity-free window assurance appear as `round-run-task`, `partial-identity` and `legacy-window`.
|
|
45
|
+
Malformed or foreign claims cannot become a manual-close fallback. This contract governs the
|
|
46
|
+
existing plain bridge and control-review caller; native Workflow QE already runs its own review and
|
|
47
|
+
must not invoke a second bridge to manufacture this receipt. Local fake-child tests do not establish
|
|
48
|
+
a live Claude model roundtrip or secure every native Codex QE receipt.
|
|
49
|
+
|
|
26
50
|
### 1. Smoke Tests (All tiers)
|
|
27
51
|
|
|
28
52
|
```
|
|
@@ -205,7 +229,9 @@ list), grep for unfinished-stub markers: `TODO` / `FIXME` / `HACK` / `XXX` / `PL
|
|
|
205
229
|
file:line, UNLESS the line carries an inline `no-stubs: <reason>` waiver WITH a non-empty reason, or
|
|
206
230
|
`.dz/guard.json` `stubWaivers` lists the path WITH a reason. A REASONLESS waiver is itself a HIGH gap,
|
|
207
231
|
never an exemption. Cross-check mechanically: `dz guard check --op publish --json` runs the same scan
|
|
208
|
-
as the SOFT `no-stubs` rule over the working-tree diff.
|
|
232
|
+
as the SOFT `no-stubs` rule over the working-tree diff.
|
|
233
|
+
After the Step-7 code has landed, run `dz guard check --op code --json` and treat a HARD `block` verdict
|
|
234
|
+
as a HIGH finding naming the drifted file. When you QUOTE a marker in `08_qe_report.md`,
|
|
209
235
|
backtick it so the report itself scans clean (the claim-check forbidden-phrase convention). Record the
|
|
210
236
|
verdict in the ADR Fitness section.
|
|
211
237
|
|
|
@@ -311,6 +337,9 @@ Compile all findings into a structured report:
|
|
|
311
337
|
✅ READY FOR MERGE | ❌ NEEDS FIXES | ⚠️ CONDITIONAL APPROVAL
|
|
312
338
|
```
|
|
313
339
|
|
|
340
|
+
Settle a cross-family re-QE debt with `dz reqe --slug <s> --done --report <08b>`.
|
|
341
|
+
For `dz reqe --done`, exit 3 means the review settled but named BLOCKER/HIGH findings — stop and surface them to the owner.
|
|
342
|
+
|
|
314
343
|
### 7.1 Findings ledger (machine-readable)
|
|
315
344
|
|
|
316
345
|
Prose is for people; `dz score`/`dz recap` need a machine-readable surface too (qe-findings-record,
|
|
@@ -356,6 +385,45 @@ An empty (header-only) table is read as `hollow: true` — worse than no table a
|
|
|
356
385
|
claims a ledger exists and says nothing. If there are no findings, omit the section entirely rather
|
|
357
386
|
than writing an empty table.
|
|
358
387
|
|
|
388
|
+
### 7.2 Ledger row for this QE step (mandatory)
|
|
389
|
+
|
|
390
|
+
Plain mode (this SKILL, not the ultracode `Workflow`) has no in-process `appendRunCostRow` — without
|
|
391
|
+
this step the QE instrument's own pass leaves ZERO trace in the run-cost ledger (aqe-ledger-row,
|
|
392
|
+
owner audit 17.09, decision #3).
|
|
393
|
+
|
|
394
|
+
**Resume guard — check this FIRST, before writing anything.** If QE was RESTORED from a checkpoint
|
|
395
|
+
for THIS run (`features/<slug>/.fa-state/checkpoints.jsonl` already contains a line for stage `qe`
|
|
396
|
+
that matches this run) — the row for this review was already written by the run that actually did
|
|
397
|
+
it. Do **NOT** run the command below in that case; instead write exactly this line into
|
|
398
|
+
`08_qe_report.md`: `qe ledger row skipped — stage resumed`. Writing the row anyway would double-pay
|
|
399
|
+
the same review in the ledger (aqe-ledger-row fix-round-1/#3, Codex r1 HIGH #3 — plain mode had no
|
|
400
|
+
resume guard at all before this).
|
|
401
|
+
|
|
402
|
+
Otherwise — QE ran fresh in this pass — after `08_qe_report.md` is written and the grade is final, run:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
dz feature-adr-record --kind ledger --stage qe --slug <slug> --row '<json>' --json
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
fix-round-1 (Codex r1 HIGH finding 4, ADR-001 D5): no `--auto` here. `--auto` is the trusted marker
|
|
409
|
+
of an AUTOMATED pipeline run and requires an experiment envelope that the manual path does not have;
|
|
410
|
+
this plain-mode row is written without it.
|
|
411
|
+
|
|
412
|
+
`<json>` carries the same fields the ultracode pipeline writes for this row: `reviewer` (the model
|
|
413
|
+
that reviewed, or `null` if unknown — never guessed), `reviewerFamily` (`claude`|`codex`|`null` when
|
|
414
|
+
the reviewer identity is not one of the two known families — never guessed as `claude`), `qeRole`
|
|
415
|
+
(`qe-code-reviewer` for a Claude reviewer; `codex-review`/`codex-exec` for a codex reviewer by scope
|
|
416
|
+
mode; `null` otherwise), `grade`, `gradeSource` (if known), `findings` (gap count), `findingsBySeverity`
|
|
417
|
+
(normalized `sev` -> count, unknown severities counted as `other`, never dropped), `findingsSource`
|
|
418
|
+
(`gaps` or `no-gaps-array`), `claimCheck` (if run), `crossFamily` (bool: reviewer family != coder
|
|
419
|
+
family — `null` when either family is unknown, since a same/cross-family verdict cannot be asserted
|
|
420
|
+
without both), `qeScope` (if the reviewer ran scoped, e.g. codex `mode`/`ref`/`files`). Insert the
|
|
421
|
+
writer's verdict (`{verdict:"written"|...}`) verbatim into `08_qe_report.md` so the record is
|
|
422
|
+
auditable from the artifact itself. NAMED LIMIT (layer 4 of the cost-of-detection ladder, said
|
|
423
|
+
honestly): this step is a skill instruction, not a code gate — nothing forces the agent to run it or
|
|
424
|
+
to check the resume guard first; only its PRESENCE in this module is deterministically pinned (grep
|
|
425
|
+
for the command above and for the resume-guard sentence).
|
|
426
|
+
|
|
359
427
|
### 8. QE Pattern Store (Direct Mode only)
|
|
360
428
|
|
|
361
429
|
When `{AGENTIC_QE_MODE}` = `direct` | `direct-extended`, after the gap loop is closed and the verdict is set:
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One parser for workflow and plain Step 7. Importing this module performs no IO.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const SCHEMA = 'fa-coder-context-1';
|
|
9
|
+
const MAX_DOCUMENTS = 64;
|
|
10
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
11
|
+
const MAX_TOTAL_BYTES = 1024 * 1024;
|
|
12
|
+
const MAX_PROMPT_BYTES = 96 * 1024;
|
|
13
|
+
const sha = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
14
|
+
const kindOf = (path) => path === '01_requirements.md' ? 'requirements'
|
|
15
|
+
: path === '06_implementation_plan.md' ? 'plan'
|
|
16
|
+
: /^03_adr\/[0-9]{3}-[^/\\\x00-\x1f]+\.md$/.test(path) ? 'adr' : null;
|
|
17
|
+
const diagnostic = (source, reason, section) => ({ source, reason, severity: 'error', ...(section ? { section } : {}) });
|
|
18
|
+
|
|
19
|
+
// One lexical pass recognizes comments only outside Markdown code/escapes, retaining offsets.
|
|
20
|
+
function markdown(text) {
|
|
21
|
+
const headings = [];
|
|
22
|
+
const visible = [];
|
|
23
|
+
let fence = null;
|
|
24
|
+
let comment = false;
|
|
25
|
+
let inlineEnd = -1;
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const line of text.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
28
|
+
const clean = line.replace(/\r?\n$/, '').replace(/^\uFEFF/, '');
|
|
29
|
+
const delimiter = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(clean);
|
|
30
|
+
if (fence) {
|
|
31
|
+
if (delimiter && delimiter[1][0] === fence.char && delimiter[1].length >= fence.length && delimiter[2].trim() === '') fence = null;
|
|
32
|
+
else visible.push(line);
|
|
33
|
+
} else if (!comment && inlineEnd <= offset && delimiter && !(delimiter[1][0] === '`' && delimiter[2].includes('`'))) {
|
|
34
|
+
fence = { char: delimiter[1][0], length: delimiter[1].length };
|
|
35
|
+
} else {
|
|
36
|
+
const wasComment = comment;
|
|
37
|
+
let masked = '';
|
|
38
|
+
for (let i = 0; i < line.length;) {
|
|
39
|
+
if (comment) {
|
|
40
|
+
if (line.startsWith('-->', i)) { masked += ' '; i += 3; comment = false; }
|
|
41
|
+
else { masked += /[\r\n]/.test(line[i]) ? line[i] : ' '; i++; }
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (offset + i < inlineEnd) { masked += line[i++]; continue; }
|
|
45
|
+
// An escaped '<' cannot start a comment; paired backslashes still allow the next opener.
|
|
46
|
+
if (line[i] === '\\' && i + 1 < line.length && /[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~]/.test(line[i + 1])) {
|
|
47
|
+
masked += line.slice(i, i + 2); i += 2; continue;
|
|
48
|
+
}
|
|
49
|
+
if (line[i] === '`') {
|
|
50
|
+
const run = /^`+/.exec(line.slice(i))[0];
|
|
51
|
+
const rest = text.slice(offset + i + run.length);
|
|
52
|
+
// Code spans can wrap within a paragraph, but cannot hide a subsequent block heading/fence.
|
|
53
|
+
const boundary = /\n[ \t]*(?:\r?\n|#{1,6}[ \t]|`{3,}|~{3,})/.exec(rest);
|
|
54
|
+
const paragraph = boundary ? rest.slice(0, boundary.index) : rest;
|
|
55
|
+
const close = [...paragraph.matchAll(/`+/g)].find((match) => match[0].length === run.length);
|
|
56
|
+
if (close) inlineEnd = offset + i + run.length + close.index + run.length;
|
|
57
|
+
masked += run; i += run.length; continue;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith('<!--', i)) { comment = true; masked += ' '; i += 4; continue; }
|
|
60
|
+
masked += line[i++];
|
|
61
|
+
}
|
|
62
|
+
const heading = !wasComment && /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/.exec(masked.replace(/\r?\n$/, '').replace(/^\uFEFF/, ''));
|
|
63
|
+
if (heading) headings.push({ depth: heading[1].length, title: heading[2].replace(/[ \t]+#+[ \t]*$/, '').trim(), start: offset, body: offset + line.length });
|
|
64
|
+
else visible.push(masked);
|
|
65
|
+
}
|
|
66
|
+
offset += line.length;
|
|
67
|
+
}
|
|
68
|
+
return { headings, unclosed: fence !== null, unclosedComment: comment, substantive: visible.join('').trim() !== '' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pure: consumes already-read documents only, and hashes the same snapshot used for the block. */
|
|
72
|
+
export function buildCoderContext({ tier, documents }) {
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
const sources = [];
|
|
75
|
+
const blocks = [];
|
|
76
|
+
let bounded = true;
|
|
77
|
+
if (!['S', 'M', 'L', 'XL'].includes(tier)) diagnostics.push(diagnostic('tier', 'invalid-tier'));
|
|
78
|
+
if (!Array.isArray(documents)) documents = [];
|
|
79
|
+
if (documents.length > MAX_DOCUMENTS) { diagnostics.push(diagnostic('documents', 'document-limit')); bounded = false; }
|
|
80
|
+
const sorted = [...documents].sort((a, b) => String(a?.path).localeCompare(String(b?.path), 'en'));
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
let total = 0;
|
|
83
|
+
for (const doc of sorted.slice(0, MAX_DOCUMENTS)) {
|
|
84
|
+
const path = doc?.path;
|
|
85
|
+
const kind = typeof path === 'string' && path.length <= 512 ? kindOf(path) : null;
|
|
86
|
+
if (!kind || typeof doc.text !== 'string') { diagnostics.push(diagnostic(String(path ?? 'documents'), 'invalid-source')); bounded = false; continue; }
|
|
87
|
+
if (seen.has(path)) { diagnostics.push(diagnostic(path, 'duplicate-source')); bounded = false; continue; }
|
|
88
|
+
seen.add(path);
|
|
89
|
+
const bytes = Buffer.byteLength(doc.text, 'utf8'); total += bytes;
|
|
90
|
+
const status = bytes > MAX_FILE_BYTES ? 'oversized' : 'read';
|
|
91
|
+
sources.push({ path, kind, bytes, digest: status === 'read' ? sha(doc.text) : null, status });
|
|
92
|
+
if (status !== 'read') { diagnostics.push(diagnostic(path, 'file-limit')); bounded = false; continue; }
|
|
93
|
+
const parsed = markdown(doc.text);
|
|
94
|
+
if (parsed.unclosed) diagnostics.push(diagnostic(path, 'unclosed-fence'));
|
|
95
|
+
if (parsed.unclosedComment) diagnostics.push(diagnostic(path, 'unclosed-comment'));
|
|
96
|
+
if (kind !== 'adr') {
|
|
97
|
+
if (!parsed.substantive) diagnostics.push(diagnostic(path, 'empty-section', 'body'));
|
|
98
|
+
blocks.push('\n\n### Source: ' + path + ' — body\n' + doc.text);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const section of ['Decision', 'Confirmation']) {
|
|
102
|
+
const matches = parsed.headings.filter((h) => h.depth >= 2 && h.title.toLowerCase() === section.toLowerCase());
|
|
103
|
+
if (matches.length !== 1) { diagnostics.push(diagnostic(path, matches.length ? 'duplicate-section' : 'missing-section', section)); continue; }
|
|
104
|
+
const heading = matches[0];
|
|
105
|
+
const next = parsed.headings.find((h) => h.start > heading.start && h.depth <= heading.depth);
|
|
106
|
+
const body = doc.text.slice(heading.body, next ? next.start : doc.text.length);
|
|
107
|
+
if (!markdown(body).substantive) diagnostics.push(diagnostic(path, 'empty-section', section));
|
|
108
|
+
blocks.push('\n\n### Source: ' + path + ' — ' + section + '\n' + body);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (total > MAX_TOTAL_BYTES) { diagnostics.push(diagnostic('documents', 'aggregate-limit')); bounded = false; }
|
|
112
|
+
for (const path of ['01_requirements.md', '06_implementation_plan.md']) {
|
|
113
|
+
if (!seen.has(path)) {
|
|
114
|
+
sources.push({ path, kind: kindOf(path), bytes: 0, digest: null, status: 'missing' });
|
|
115
|
+
diagnostics.push(diagnostic(path, 'missing-source'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (tier !== 'S' && !sources.some((s) => s.kind === 'adr')) diagnostics.push(diagnostic('03_adr', 'missing-source'));
|
|
119
|
+
let promptBlock = '\n\n## Current coder context (literal source snapshot)' + blocks.join('');
|
|
120
|
+
if (tier === 'S' && !sources.some((s) => s.kind === 'adr')) promptBlock += '\n\nADR sources: none selected (tier S).';
|
|
121
|
+
if (Buffer.byteLength(promptBlock, 'utf8') > MAX_PROMPT_BYTES) {
|
|
122
|
+
diagnostics.push(diagnostic('promptBlock', 'prompt-limit')); promptBlock = '';
|
|
123
|
+
}
|
|
124
|
+
return { schema: SCHEMA, status: diagnostics.length ? 'incomplete' : 'complete', promptBlock,
|
|
125
|
+
digest: bounded ? sha(JSON.stringify([SCHEMA, tier, sorted.map((d) => [d.path, d.text])])) : null,
|
|
126
|
+
sources: sources.sort((a, b) => a.path.localeCompare(b.path, 'en')), diagnostics };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hostContext(feature, tier) {
|
|
130
|
+
const failures = [];
|
|
131
|
+
const documents = [];
|
|
132
|
+
const failedSources = [];
|
|
133
|
+
let root;
|
|
134
|
+
const fail = (path, reason, status = 'unreadable', bytes = 0) => {
|
|
135
|
+
failures.push(diagnostic(path, reason));
|
|
136
|
+
failedSources.push({ path, kind: kindOf(path) ?? 'adr', bytes, digest: null, status });
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
if (lstatSync(feature).isSymbolicLink() || !lstatSync(feature).isDirectory()) throw new Error('invalid-root');
|
|
140
|
+
root = realpathSync(feature);
|
|
141
|
+
} catch { return { schema: SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic(feature, 'invalid-root')] }; }
|
|
142
|
+
let paths = ['01_requirements.md', '06_implementation_plan.md'];
|
|
143
|
+
const adrDir = join(root, '03_adr');
|
|
144
|
+
try {
|
|
145
|
+
const stat = lstatSync(adrDir);
|
|
146
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) fail('03_adr', 'invalid-file', 'invalid');
|
|
147
|
+
else paths.push(...readdirSync(adrDir).filter((name) => /^[0-9]{3}-.+\.md$/.test(name)).sort().map((name) => '03_adr/' + name));
|
|
148
|
+
} catch (error) { if (error.code !== 'ENOENT') fail('03_adr', 'read-failure'); }
|
|
149
|
+
if (paths.length > MAX_DOCUMENTS) return { schema: SCHEMA, status: 'incomplete', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic('documents', 'document-limit')] };
|
|
150
|
+
let total = 0;
|
|
151
|
+
for (const path of paths) {
|
|
152
|
+
let fd;
|
|
153
|
+
try {
|
|
154
|
+
const file = join(root, path);
|
|
155
|
+
const stat = lstatSync(file);
|
|
156
|
+
if (stat.isSymbolicLink() || !stat.isFile()) { fail(path, 'invalid-file', 'invalid'); continue; }
|
|
157
|
+
const actual = realpathSync(file);
|
|
158
|
+
const rel = relative(root, actual);
|
|
159
|
+
if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) { fail(path, 'outside-root', 'invalid'); continue; }
|
|
160
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
161
|
+
const opened = fstatSync(fd);
|
|
162
|
+
// Compare the opened descriptor with the contained file checked before open.
|
|
163
|
+
if (!opened.isFile() || opened.ino !== stat.ino || opened.dev !== stat.dev || realpathSync(file) !== actual) { fail(path, 'invalid-file', 'invalid'); continue; }
|
|
164
|
+
const chunks = []; let bytes = 0;
|
|
165
|
+
while (true) {
|
|
166
|
+
const chunk = Buffer.alloc(Math.min(16384, MAX_FILE_BYTES + 1 - bytes));
|
|
167
|
+
const count = readSync(fd, chunk, 0, chunk.length, null);
|
|
168
|
+
if (count === 0) break;
|
|
169
|
+
bytes += count; total += count; chunks.push(chunk.subarray(0, count));
|
|
170
|
+
if (bytes > MAX_FILE_BYTES || total > MAX_TOTAL_BYTES) break;
|
|
171
|
+
}
|
|
172
|
+
if (bytes > MAX_FILE_BYTES) { fail(path, 'file-limit', 'oversized', bytes); continue; }
|
|
173
|
+
if (total > MAX_TOTAL_BYTES) { fail(path, 'aggregate-limit', 'oversized', bytes); break; }
|
|
174
|
+
try { documents.push({ path, text: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks)) }); }
|
|
175
|
+
catch { fail(path, 'invalid-utf8', 'invalid', bytes); }
|
|
176
|
+
} catch (error) { fail(path, error.code === 'ENOENT' ? 'missing-source' : 'read-failure', error.code === 'ENOENT' ? 'missing' : 'unreadable'); }
|
|
177
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
178
|
+
}
|
|
179
|
+
const result = buildCoderContext({ tier, documents });
|
|
180
|
+
if (failures.length) {
|
|
181
|
+
const failed = new Set(failedSources.map((s) => s.path));
|
|
182
|
+
result.sources = result.sources.filter((s) => !failed.has(s.path)).concat(failedSources).sort((a, b) => a.path.localeCompare(b.path, 'en'));
|
|
183
|
+
result.diagnostics = result.diagnostics.filter((d) => !(failed.has(d.source) && d.reason === 'missing-source')).concat(failures);
|
|
184
|
+
result.status = failures.every((d) => ['file-limit', 'aggregate-limit'].includes(d.reason)) ? 'incomplete' : 'unavailable';
|
|
185
|
+
result.digest = null;
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function main(argv) {
|
|
191
|
+
const feature = argv[0];
|
|
192
|
+
const tier = argv.find((arg) => arg.startsWith('--tier='))?.slice(7);
|
|
193
|
+
let result;
|
|
194
|
+
if (!feature || !['S', 'M', 'L', 'XL'].includes(tier) || argv.some((arg, i) => i > 0 && arg !== '--tier=' + tier)) {
|
|
195
|
+
result = { schema: SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic('arguments', 'invalid-arguments')] };
|
|
196
|
+
} else result = hostContext(resolve(feature), tier);
|
|
197
|
+
console.log(JSON.stringify(result));
|
|
198
|
+
return result.status === 'complete' ? 0 : 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) process.exitCode = main(process.argv.slice(2));
|
|
@@ -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}`);
|