@dzhechkov/keysarium 1.7.8 → 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 +9 -5
- package/README.md +18 -1
- package/package.json +1 -1
- package/sbom.json +14 -4
- package/templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md +129 -0
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +127 -1
- package/templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs +316 -0
package/.dz-manifest.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.md",
|
|
12
|
-
"sha256": "
|
|
12
|
+
"sha256": "13e7e27322403ce45af5be7df1fa565178b5df73ffeea1b0bbe6b3faddce9f45"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.ru.md",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "package.json",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "96bcc52bfe2180fd9976589895f1f328f6c8ac62e24d22042d1105a5c16f4148"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "src/cli.js",
|
|
@@ -373,7 +373,7 @@
|
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
"path": "templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md",
|
|
376
|
-
"sha256": "
|
|
376
|
+
"sha256": "b6e505083c1b8bc92243379b72ff4667b5b91ef65fd9c2ef7407129018b3e56b"
|
|
377
377
|
},
|
|
378
378
|
{
|
|
379
379
|
"path": "templates/.claude/skills/feature-adr/modules/04-ddd.md",
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
},
|
|
394
394
|
{
|
|
395
395
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
396
|
-
"sha256": "
|
|
396
|
+
"sha256": "4aa7ef1dea08b8d1995d968a317da5d36a4859ec299637bdc60170cf577c87af"
|
|
397
397
|
},
|
|
398
398
|
{
|
|
399
399
|
"path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
|
|
@@ -491,6 +491,10 @@
|
|
|
491
491
|
"path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
492
492
|
"sha256": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
|
|
493
493
|
},
|
|
494
|
+
{
|
|
495
|
+
"path": "templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs",
|
|
496
|
+
"sha256": "80f9691b8e94bf310461aeaaaa23af6e336d41d0962fdf137737639655cbdeb8"
|
|
497
|
+
},
|
|
494
498
|
{
|
|
495
499
|
"path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
496
500
|
"sha256": "82c3c48c25f400f3a64d1caba3d47357f3db87460f0746f88706f901eb6ff689"
|
|
@@ -997,5 +1001,5 @@
|
|
|
997
1001
|
}
|
|
998
1002
|
]
|
|
999
1003
|
},
|
|
1000
|
-
"signature": "
|
|
1004
|
+
"signature": "n1PCRDF3YW7Tcdp+QdGzlK+p7PQGfraCKUO8yQxu6zrvXx6WUALgVsvmfAwh+QXmPZzINT6kYnKmzijtcEkrCw=="
|
|
1001
1005
|
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dzhechkov/keysarium
|
|
2
2
|
|
|
3
|
-
Current package version: `1.7.
|
|
3
|
+
Current package version: `1.7.9`. <!-- dz:version -->
|
|
4
4
|
|
|
5
5
|
Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/keysarium
|
|
6
6
|
|
|
@@ -509,3 +509,20 @@ watch v1 ETA is unavailable and global source inventory omitted. No stdin/raw mo
|
|
|
509
509
|
store writes or automatic terminal/settings changes. Normal SQLite ephemeral WAL/SHM sidecars are
|
|
510
510
|
permitted. One-shot Claude text/JSON/ETA remain unchanged. Codex native footer capability is not
|
|
511
511
|
asserted; parity names manual `dz statusline --watch` access.
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
Review convergence in `/feature-adr` now uses one installed Node gate in Plain and native Workflow.
|
|
515
|
+
Step 3.5 closes before planning; Step 8 closes before completion and delivery, including S tier and
|
|
516
|
+
resumed runs. The host measures current artifact bytes and preserves originating reviewer conditions;
|
|
517
|
+
focused rework includes an explicit author delta/new-risk assessment and independent own-condition
|
|
518
|
+
verification. Serious primary or precision findings remain visible at the round ceiling.
|
|
519
|
+
|
|
520
|
+
Missing or stale receipts, legacy resume, unavailable reviewers and read-only Codex mode A pause
|
|
521
|
+
with host-driven repair instructions. Parallel native design can require focused review after sibling
|
|
522
|
+
artifacts settle. Clean initial supported review closes without invented conditions or an extra review.
|
|
523
|
+
This is a receipt consistency/freshness gate, not reviewer authentication or semantic proof; existing
|
|
524
|
+
Confirmation, scope, family and budget gates still apply. No automatic repair loop or new CLI command.
|
|
525
|
+
|
|
526
|
+
Repeated prepare retains all pending paths until verified closure. Actual fallback families bind before dispatch while prior owners remain required. Same-slug QE repair uses current own-reviewer evidence bound to the complete historical checkpoint; original findings/grades remain visible, and failed Confirmation still blocks delivery.
|
|
527
|
+
|
|
528
|
+
Before fallback, host prepare retains independently written pending receipt findings under the original owner even if that reviewer returned null. Unreconciled pending evidence pauses dispatch for originating-reviewer repair; it cannot be replaced by a clean fallback to obtain closure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/keysarium",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.9",
|
|
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",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "13e7e27322403ce45af5be7df1fa565178b5df73ffeea1b0bbe6b3faddce9f45"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "dz:canonical-json-sha256-v2",
|
|
62
|
-
"value": "
|
|
62
|
+
"value": "96bcc52bfe2180fd9976589895f1f328f6c8ac62e24d22042d1105a5c16f4148"
|
|
63
63
|
}
|
|
64
64
|
]
|
|
65
65
|
},
|
|
@@ -939,7 +939,7 @@
|
|
|
939
939
|
"hashes": [
|
|
940
940
|
{
|
|
941
941
|
"alg": "SHA-256",
|
|
942
|
-
"content": "
|
|
942
|
+
"content": "b6e505083c1b8bc92243379b72ff4667b5b91ef65fd9c2ef7407129018b3e56b"
|
|
943
943
|
}
|
|
944
944
|
]
|
|
945
945
|
},
|
|
@@ -989,7 +989,7 @@
|
|
|
989
989
|
"hashes": [
|
|
990
990
|
{
|
|
991
991
|
"alg": "SHA-256",
|
|
992
|
-
"content": "
|
|
992
|
+
"content": "4aa7ef1dea08b8d1995d968a317da5d36a4859ec299637bdc60170cf577c87af"
|
|
993
993
|
}
|
|
994
994
|
]
|
|
995
995
|
},
|
|
@@ -1233,6 +1233,16 @@
|
|
|
1233
1233
|
}
|
|
1234
1234
|
]
|
|
1235
1235
|
},
|
|
1236
|
+
{
|
|
1237
|
+
"type": "file",
|
|
1238
|
+
"name": "templates/.claude/skills/feature-adr/scripts/check-review-convergence.mjs",
|
|
1239
|
+
"hashes": [
|
|
1240
|
+
{
|
|
1241
|
+
"alg": "SHA-256",
|
|
1242
|
+
"content": "80f9691b8e94bf310461aeaaaa23af6e336d41d0962fdf137737639655cbdeb8"
|
|
1243
|
+
}
|
|
1244
|
+
]
|
|
1245
|
+
},
|
|
1236
1246
|
{
|
|
1237
1247
|
"type": "file",
|
|
1238
1248
|
"name": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
@@ -27,6 +27,135 @@ sonnet (analytical quality assessment — multiple parallel agents)
|
|
|
27
27
|
|
|
28
28
|
## Protocol
|
|
29
29
|
|
|
30
|
+
## Review convergence gate (Plain and native Workflow)
|
|
31
|
+
|
|
32
|
+
Review follows **initial review → host rework → focused verification → closure**, or explicit owner
|
|
33
|
+
escalation. A grade/GO label does not establish closure. Run the same installed Node gate before
|
|
34
|
+
planning (phase `ideation`) and before Step-8 completion/delivery (phase `qe`), including resume.
|
|
35
|
+
|
|
36
|
+
The host prepares a measured artifact snapshot BEFORE independent review. Resolve the executable
|
|
37
|
+
from this installed skill's `scripts/check-review-convergence.mjs`; do not use a scratch replacement.
|
|
38
|
+
For example, from the execution project, using the actual originating role/family:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node "$FEATURE_ADR_SKILL/scripts/check-review-convergence.mjs" prepare \
|
|
42
|
+
--repo "$EXECUTION_PROJECT" --feature "features/$FEATURE_SLUG" --phase ideation \
|
|
43
|
+
--reviewers '[{"id":"qcsd-quality","family":"owner-exception"},{"id":"qcsd-risk","family":"owner-exception"},{"id":"qcsd-testability","family":"owner-exception"}]'
|
|
44
|
+
node "$FEATURE_ADR_SKILL/scripts/check-review-convergence.mjs" evaluate \
|
|
45
|
+
--repo "$EXECUTION_PROJECT" --feature "features/$FEATURE_SLUG" --phase ideation
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For QE use `--phase qe` and the actual `qe-primary` reviewer, plus `qe-precision` when configured.
|
|
49
|
+
`family` is `codex`, `claude`, or an explicitly authorized `owner-exception`. Existing family/routing
|
|
50
|
+
policy remains binding; this receipt does not authorize an exception. Only the host calls `prepare`.
|
|
51
|
+
It retains originating reviewer roles and prior condition meanings in the existing feature checkpoint
|
|
52
|
+
`.fa-state/review-convergence-<phase>-host.json`; neither the latest report nor an empty request array
|
|
53
|
+
can replace that lineage. Do not edit/delete host lineage to obtain a clean bootstrap. A new nonce is
|
|
54
|
+
issued when measured bytes/path membership change; repeated preparation cannot remove conditions. Pending changed paths accumulate across every prepare, including path additions/deletions, until an established closure; failed/refused evaluations never clear them.
|
|
55
|
+
|
|
56
|
+
The snapshot measures requirements, every direct ADR, architecture, and other available design
|
|
57
|
+
inputs. QE also measures the plan, ideation report, every declared target, and host-discovered Git
|
|
58
|
+
changes/additions/deletions against `.fa-state/base-ref` (HEAD when absent). Receipt/checkpoint files,
|
|
59
|
+
QE report and generated `architecture/map.json` are outputs excluded to avoid circular hashes.
|
|
60
|
+
Explicit planned deletions carry a null digest. Traversal, symlinks and non-files refuse. The host
|
|
61
|
+
must include relevant unchanged dependencies in declared targets; hashing cannot discover semantic
|
|
62
|
+
dependencies. Parallel Workflow design may change after ideation review: that stale review pauses
|
|
63
|
+
before planning and needs focused review of the settled snapshot.
|
|
64
|
+
|
|
65
|
+
All three QCSD core roles remain originating reviewers. The coordinator does not manufacture their
|
|
66
|
+
receipts from grades or report prose. The JSON example below shows one entry; include all three
|
|
67
|
+
actual roles in `reviews`, with their actual families. Each independent reviewer writes its OWN
|
|
68
|
+
entry, preserving other reviewer entries and the actual
|
|
69
|
+
author handoff, in `.fa-state/review-convergence-<phase>-review.json`. A genuinely clean initial
|
|
70
|
+
review needs no invented condition or extra review. Its complete receipt shape is:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"schema": "fa-review-convergence-1", "phase": "ideation",
|
|
75
|
+
"nonce": "<copy host snapshot nonce>", "revision": "<copy host snapshot revision>",
|
|
76
|
+
"author": null,
|
|
77
|
+
"reviews": [{
|
|
78
|
+
"reviewer": "qcsd-quality", "family": "owner-exception", "independent": true, "verdict": "clean",
|
|
79
|
+
"conditions": [], "verifications": [],
|
|
80
|
+
"newRisks": {"assessment": "none identified after complete scope review", "evidence": "<actual review evidence>", "conditions": []},
|
|
81
|
+
"authorAssessmentChecked": false, "deltaVerification": null
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`verdict` is `clean`, `conditional`, or `no-go`. Conditions have closed fields
|
|
87
|
+
`{id, owner, severity, classification, requirement, scope, evidence}`: stable unique ID, originating
|
|
88
|
+
role ID, BLOCKER/CRITICAL/HIGH/WARNING/SUGGESTION, structural/wording, exact acceptance meaning,
|
|
89
|
+
nonempty repo-relative scope paths, and original evidence. Carry every prior own condition unchanged
|
|
90
|
+
on every pass. Reviewer verifications have closed fields
|
|
91
|
+
`{id, revision, classification, evidence, implementationVerified}`. Only the originating independent
|
|
92
|
+
role can verify its own condition at the current revision; author/lead/scribe assertions cannot.
|
|
93
|
+
|
|
94
|
+
Rework requires the actual author object
|
|
95
|
+
`{addressed, changedScope, classification, delta, evidence, newRisks}`. Addressed IDs name prior
|
|
96
|
+
conditions; changedScope includes ALL host-measured changes and affected dependencies. Delta/evidence
|
|
97
|
+
explain the actual fix. Both author and reviewer `newRisks` require explicit assessment/evidence and a
|
|
98
|
+
conditions array, including an explicit none-identified assessment when empty. The reviewer must set
|
|
99
|
+
`authorAssessmentChecked:true` and supply independent
|
|
100
|
+
`deltaVerification:{classification,evidence,implementationVerified}`. Structural changes affect
|
|
101
|
+
behavior, contracts, safeguards, architecture or acceptance meaning and require affected implementation
|
|
102
|
+
and test verification. Wording changes affect expression only; the independent reviewer checks that
|
|
103
|
+
classification, so an author's wording label cannot weaken structural verification.
|
|
104
|
+
|
|
105
|
+
Focused scope is outstanding conditions plus actual changes and affected dependencies. New serious
|
|
106
|
+
risks always expand that scope. Retain ALL conditions and serious primary/precision findings until
|
|
107
|
+
own-reviewer verification; a token/round ceiling escalates unresolved risk, never approves it.
|
|
108
|
+
|
|
109
|
+
Gate exits: 0 with `prepared` (prepare only) or `closed` (evaluate only); 1 `unresolved`; 3
|
|
110
|
+
`not-established`. Inspect the structured reasons/unresolved IDs. Missing executable/report, malformed
|
|
111
|
+
or contradictory output, foreign/duplicate/omitted conditions, stale phase/nonce/manifest or unavailable
|
|
112
|
+
reviewer refuses. Native Workflow strictly checks stdout AND exit before planning and before
|
|
113
|
+
completion/delivery/success tags. Supported native/Mode-B/fallback/precision reviewers receive this
|
|
114
|
+
contract; the read-only Codex mode-A route cannot accept it and pauses for a supported independent
|
|
115
|
+
review. A scribe never creates reviewer evidence from grade. Historical resume without this contract
|
|
116
|
+
needs fresh evidence; resume rechecks bytes and every configured route, including unwitnessed precision.
|
|
117
|
+
|
|
118
|
+
Host prepare first reads the existing reviewer receipt, including valid partial own entries written
|
|
119
|
+
before an agent/transport failure. It preserves independently written conditions under their original
|
|
120
|
+
role/family before binding any actual fallback. This imports findings, never closure or author/scribe
|
|
121
|
+
claims. A stale receipt cannot originate new host conditions; known prior meanings remain immutable.
|
|
122
|
+
`pending-reviewer-receipt-needs-origin-review` pauses before native fallback dispatch when pending
|
|
123
|
+
evidence cannot be reconciled. Keep the original receipt and host lineage. Have its originating
|
|
124
|
+
reviewer repair malformed/foreign fields or refresh unknown stale own findings at the unchanged
|
|
125
|
+
host identity/current snapshot. Repeat prepare for the selected actual base-role families, complete
|
|
126
|
+
the returned full reviewer roster with actual own verification, then evaluate and resume the same
|
|
127
|
+
slug. No receipt/lineage deletion or relabeling is a recovery action. The existing QE checkpoint
|
|
128
|
+
validator rejects a paused non-QE result, so same-slug resume re-enters only that reviewer stage;
|
|
129
|
+
unrelated coder checkpoints remain eligible for reuse.
|
|
130
|
+
|
|
131
|
+
Bind the actual fallback family with host prepare BEFORE reviewer dispatch. An unused role may change family; once review evidence originated, its role/family and prior conditions remain required, and the new family receives a distinct `<base-id>:<family>` role. Reviewers read the returned full roster and never relabel old ownership.
|
|
132
|
+
|
|
133
|
+
On refusal, the host settles artifacts, calls prepare with the selected actual base-role families, supplies
|
|
134
|
+
the author handoff, requests focused own-condition verification, then evaluates and resumes the same
|
|
135
|
+
slug. This is host-driven repair, not an automatic repair loop. Existing Confirmation, amendment,
|
|
136
|
+
scope, testing, independent-family and budget gates remain conjunctive. This gate establishes receipt
|
|
137
|
+
consistency and current artifact binding, not semantic truth or authenticated reviewer identity.
|
|
138
|
+
|
|
139
|
+
For a same-slug QE resume blocked by historical serious findings, read-only mode A, or an
|
|
140
|
+
unwitnessed precision append, preserve the complete original checkpoint, findings and grades.
|
|
141
|
+
Prepare returns `checkpointDigest`: SHA-256 of `JSON.stringify` of the latest `qe` result in
|
|
142
|
+
`.fa-state/checkpoints.jsonl`. Each actual originating reviewer supplies optional closed-field
|
|
143
|
+
`checkpointVerification:{checkpointDigest,evidence,supported:true,route,findings,reportDigest}`
|
|
144
|
+
in its own review entry, in addition to the ordinary current-revision independent verification.
|
|
145
|
+
`route` is native/mode-b/fallback and evidence describes the actual supported focused review;
|
|
146
|
+
mode A, author/scribe flags, or the old grade cannot establish this proof. `findings` maps EVERY
|
|
147
|
+
original serious gap with `{digest,conditionId}`, where digest is SHA-256(JSON.stringify(gap))
|
|
148
|
+
and conditionId names that reviewer's preserved serious condition with its current structural
|
|
149
|
+
implementation verification. Both primary and precision origins must verify their own findings.
|
|
150
|
+
Use `reportDigest:null` normally; an unwitnessed precision append requires that originating
|
|
151
|
+
precision reviewer to verify the actual complete `08_qe_report.md` and supply its SHA-256.
|
|
152
|
+
The report must contain Primary QE pass, Precision QE pass, and Combined Step-8 grade sections.
|
|
153
|
+
Then evaluate with the same phase/repo/feature and re-invoke the same slug. Only a closed gate
|
|
154
|
+
with complete checkpoint binding can reconcile historical refusals on an actual resumed QE stage;
|
|
155
|
+
live findings, changed checkpoints/artifacts and a failed Confirmation gate still block delivery.
|
|
156
|
+
This procedure runs no coder or automatic repair loop, and retains the historical result for audit.
|
|
157
|
+
|
|
158
|
+
|
|
30
159
|
### 1. Flag Detection (MANDATORY)
|
|
31
160
|
|
|
32
161
|
Scan feature requirements + ADR decisions and SET these flags:
|
|
@@ -23,6 +23,132 @@ sonnet (analytical evaluation)
|
|
|
23
23
|
|
|
24
24
|
## Protocol
|
|
25
25
|
|
|
26
|
+
## Review convergence gate (Plain and native Workflow)
|
|
27
|
+
|
|
28
|
+
Review follows **initial review → host rework → focused verification → closure**, or explicit owner
|
|
29
|
+
escalation. A grade/GO label does not establish closure. Run the same installed Node gate before
|
|
30
|
+
planning (phase `ideation`) and before Step-8 completion/delivery (phase `qe`), including resume.
|
|
31
|
+
|
|
32
|
+
The host prepares a measured artifact snapshot BEFORE independent review. Resolve the executable
|
|
33
|
+
from this installed skill's `scripts/check-review-convergence.mjs`; do not use a scratch replacement.
|
|
34
|
+
For example, from the execution project, using the actual originating role/family:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node "$FEATURE_ADR_SKILL/scripts/check-review-convergence.mjs" prepare \
|
|
38
|
+
--repo "$EXECUTION_PROJECT" --feature "features/$FEATURE_SLUG" --phase ideation \
|
|
39
|
+
--reviewers '[{"id":"qcsd","family":"owner-exception"}]'
|
|
40
|
+
node "$FEATURE_ADR_SKILL/scripts/check-review-convergence.mjs" evaluate \
|
|
41
|
+
--repo "$EXECUTION_PROJECT" --feature "features/$FEATURE_SLUG" --phase ideation
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The ideation example above is illustrative; QCSD uses its three actual core role entries. For QE use `--phase qe` and the actual `qe-primary` reviewer, plus `qe-precision` when configured.
|
|
45
|
+
`family` is `codex`, `claude`, or an explicitly authorized `owner-exception`. Existing family/routing
|
|
46
|
+
policy remains binding; this receipt does not authorize an exception. Only the host calls `prepare`.
|
|
47
|
+
It retains originating reviewer roles and prior condition meanings in the existing feature checkpoint
|
|
48
|
+
`.fa-state/review-convergence-<phase>-host.json`; neither the latest report nor an empty request array
|
|
49
|
+
can replace that lineage. Do not edit/delete host lineage to obtain a clean bootstrap. A new nonce is
|
|
50
|
+
issued when measured bytes/path membership change; repeated preparation cannot remove conditions. Pending changed paths accumulate across every prepare, including path additions/deletions, until an established closure; failed/refused evaluations never clear them.
|
|
51
|
+
|
|
52
|
+
The snapshot measures requirements, every direct ADR, architecture, and other available design
|
|
53
|
+
inputs. QE also measures the plan, ideation report, every declared target, and host-discovered Git
|
|
54
|
+
changes/additions/deletions against `.fa-state/base-ref` (HEAD when absent). Receipt/checkpoint files,
|
|
55
|
+
QE report and generated `architecture/map.json` are outputs excluded to avoid circular hashes.
|
|
56
|
+
Explicit planned deletions carry a null digest. Traversal, symlinks and non-files refuse. The host
|
|
57
|
+
must include relevant unchanged dependencies in declared targets; hashing cannot discover semantic
|
|
58
|
+
dependencies. Parallel Workflow design may change after ideation review: that stale review pauses
|
|
59
|
+
before planning and needs focused review of the settled snapshot.
|
|
60
|
+
|
|
61
|
+
Each independent reviewer writes its OWN entry, preserving other reviewer entries and the actual
|
|
62
|
+
author handoff, in `.fa-state/review-convergence-<phase>-review.json`. A genuinely clean initial
|
|
63
|
+
review needs no invented condition or extra review. Its complete receipt shape is:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"schema": "fa-review-convergence-1", "phase": "ideation",
|
|
68
|
+
"nonce": "<copy host snapshot nonce>", "revision": "<copy host snapshot revision>",
|
|
69
|
+
"author": null,
|
|
70
|
+
"reviews": [{
|
|
71
|
+
"reviewer": "qcsd", "family": "owner-exception", "independent": true, "verdict": "clean",
|
|
72
|
+
"conditions": [], "verifications": [],
|
|
73
|
+
"newRisks": {"assessment": "none identified after complete scope review", "evidence": "<actual review evidence>", "conditions": []},
|
|
74
|
+
"authorAssessmentChecked": false, "deltaVerification": null
|
|
75
|
+
}]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`verdict` is `clean`, `conditional`, or `no-go`. Conditions have closed fields
|
|
80
|
+
`{id, owner, severity, classification, requirement, scope, evidence}`: stable unique ID, originating
|
|
81
|
+
role ID, BLOCKER/CRITICAL/HIGH/WARNING/SUGGESTION, structural/wording, exact acceptance meaning,
|
|
82
|
+
nonempty repo-relative scope paths, and original evidence. Carry every prior own condition unchanged
|
|
83
|
+
on every pass. Reviewer verifications have closed fields
|
|
84
|
+
`{id, revision, classification, evidence, implementationVerified}`. Only the originating independent
|
|
85
|
+
role can verify its own condition at the current revision; author/lead/scribe assertions cannot.
|
|
86
|
+
|
|
87
|
+
Rework requires the actual author object
|
|
88
|
+
`{addressed, changedScope, classification, delta, evidence, newRisks}`. Addressed IDs name prior
|
|
89
|
+
conditions; changedScope includes ALL host-measured changes and affected dependencies. Delta/evidence
|
|
90
|
+
explain the actual fix. Both author and reviewer `newRisks` require explicit assessment/evidence and a
|
|
91
|
+
conditions array, including an explicit none-identified assessment when empty. The reviewer must set
|
|
92
|
+
`authorAssessmentChecked:true` and supply independent
|
|
93
|
+
`deltaVerification:{classification,evidence,implementationVerified}`. Structural changes affect
|
|
94
|
+
behavior, contracts, safeguards, architecture or acceptance meaning and require affected implementation
|
|
95
|
+
and test verification. Wording changes affect expression only; the independent reviewer checks that
|
|
96
|
+
classification, so an author's wording label cannot weaken structural verification.
|
|
97
|
+
|
|
98
|
+
Focused scope is outstanding conditions plus actual changes and affected dependencies. New serious
|
|
99
|
+
risks always expand that scope. Retain ALL conditions and serious primary/precision findings until
|
|
100
|
+
own-reviewer verification; a token/round ceiling escalates unresolved risk, never approves it.
|
|
101
|
+
|
|
102
|
+
Gate exits: 0 with `prepared` (prepare only) or `closed` (evaluate only); 1 `unresolved`; 3
|
|
103
|
+
`not-established`. Inspect the structured reasons/unresolved IDs. Missing executable/report, malformed
|
|
104
|
+
or contradictory output, foreign/duplicate/omitted conditions, stale phase/nonce/manifest or unavailable
|
|
105
|
+
reviewer refuses. Native Workflow strictly checks stdout AND exit before planning and before
|
|
106
|
+
completion/delivery/success tags. Supported native/Mode-B/fallback/precision reviewers receive this
|
|
107
|
+
contract; the read-only Codex mode-A route cannot accept it and pauses for a supported independent
|
|
108
|
+
review. A scribe never creates reviewer evidence from grade. Historical resume without this contract
|
|
109
|
+
needs fresh evidence; resume rechecks bytes and every configured route, including unwitnessed precision.
|
|
110
|
+
|
|
111
|
+
Host prepare first reads the existing reviewer receipt, including valid partial own entries written
|
|
112
|
+
before an agent/transport failure. It preserves independently written conditions under their original
|
|
113
|
+
role/family before binding any actual fallback. This imports findings, never closure or author/scribe
|
|
114
|
+
claims. A stale receipt cannot originate new host conditions; known prior meanings remain immutable.
|
|
115
|
+
`pending-reviewer-receipt-needs-origin-review` pauses before native fallback dispatch when pending
|
|
116
|
+
evidence cannot be reconciled. Keep the original receipt and host lineage. Have its originating
|
|
117
|
+
reviewer repair malformed/foreign fields or refresh unknown stale own findings at the unchanged
|
|
118
|
+
host identity/current snapshot. Repeat prepare for the selected actual base-role families, complete
|
|
119
|
+
the returned full reviewer roster with actual own verification, then evaluate and resume the same
|
|
120
|
+
slug. No receipt/lineage deletion or relabeling is a recovery action. The existing QE checkpoint
|
|
121
|
+
validator rejects a paused non-QE result, so same-slug resume re-enters only that reviewer stage;
|
|
122
|
+
unrelated coder checkpoints remain eligible for reuse.
|
|
123
|
+
|
|
124
|
+
Bind the actual fallback family with host prepare BEFORE reviewer dispatch. An unused role may change family; once review evidence originated, its role/family and prior conditions remain required, and the new family receives a distinct `<base-id>:<family>` role. Reviewers read the returned full roster and never relabel old ownership.
|
|
125
|
+
|
|
126
|
+
On refusal, the host settles artifacts, calls prepare with the selected actual base-role families, supplies
|
|
127
|
+
the author handoff, requests focused own-condition verification, then evaluates and resumes the same
|
|
128
|
+
slug. This is host-driven repair, not an automatic repair loop. Existing Confirmation, amendment,
|
|
129
|
+
scope, testing, independent-family and budget gates remain conjunctive. This gate establishes receipt
|
|
130
|
+
consistency and current artifact binding, not semantic truth or authenticated reviewer identity.
|
|
131
|
+
|
|
132
|
+
For a same-slug QE resume blocked by historical serious findings, read-only mode A, or an
|
|
133
|
+
unwitnessed precision append, preserve the complete original checkpoint, findings and grades.
|
|
134
|
+
Prepare returns `checkpointDigest`: SHA-256 of `JSON.stringify` of the latest `qe` result in
|
|
135
|
+
`.fa-state/checkpoints.jsonl`. Each actual originating reviewer supplies optional closed-field
|
|
136
|
+
`checkpointVerification:{checkpointDigest,evidence,supported:true,route,findings,reportDigest}`
|
|
137
|
+
in its own review entry, in addition to the ordinary current-revision independent verification.
|
|
138
|
+
`route` is native/mode-b/fallback and evidence describes the actual supported focused review;
|
|
139
|
+
mode A, author/scribe flags, or the old grade cannot establish this proof. `findings` maps EVERY
|
|
140
|
+
original serious gap with `{digest,conditionId}`, where digest is SHA-256(JSON.stringify(gap))
|
|
141
|
+
and conditionId names that reviewer's preserved serious condition with its current structural
|
|
142
|
+
implementation verification. Both primary and precision origins must verify their own findings.
|
|
143
|
+
Use `reportDigest:null` normally; an unwitnessed precision append requires that originating
|
|
144
|
+
precision reviewer to verify the actual complete `08_qe_report.md` and supply its SHA-256.
|
|
145
|
+
The report must contain Primary QE pass, Precision QE pass, and Combined Step-8 grade sections.
|
|
146
|
+
Then evaluate with the same phase/repo/feature and re-invoke the same slug. Only a closed gate
|
|
147
|
+
with complete checkpoint binding can reconcile historical refusals on an actual resumed QE stage;
|
|
148
|
+
live findings, changed checkpoints/artifacts and a failed Confirmation gate still block delivery.
|
|
149
|
+
This procedure runs no coder or automatic repair loop, and retains the historical result for audit.
|
|
150
|
+
|
|
151
|
+
|
|
26
152
|
### Bridge identity for plain Step 8
|
|
27
153
|
|
|
28
154
|
When the plain host invokes the existing Claude review bridge, take round number, pipeline run and
|
|
@@ -56,7 +182,7 @@ a live Claude model roundtrip or secure every native Codex QE receipt.
|
|
|
56
182
|
- [ ] Type checker passes (if configured)
|
|
57
183
|
```
|
|
58
184
|
|
|
59
|
-
For S-tier:
|
|
185
|
+
For S-tier: passing smoke tests still requires the review-convergence contract above before checkpoint/completion; a smoke pass alone is not closure.
|
|
60
186
|
|
|
61
187
|
### 2. Test Generation (M+)
|
|
62
188
|
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Host checkpoint lineage, not a reviewer-supplied prior array, owns historical conditions.
|
|
3
|
+
// This checks consistency/freshness; it does not authenticate models or judge their evidence.
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, relative, resolve, sep } from 'node:path'
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
export const SCHEMA = 'fa-review-convergence-1'
|
|
11
|
+
const phases = ['ideation', 'qe']
|
|
12
|
+
const severities = ['BLOCKER', 'CRITICAL', 'HIGH', 'WARNING', 'SUGGESTION']
|
|
13
|
+
const kinds = ['structural', 'wording']
|
|
14
|
+
const digest = value => createHash('sha256').update(value).digest('hex')
|
|
15
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
16
|
+
const text = value => typeof value === 'string' && value.trim().length > 0 && value.length <= 65536
|
|
17
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
|
|
18
|
+
const array = (value, limit = 512) => Array.isArray(value) && value.length <= limit
|
|
19
|
+
function dictionary(value, keys, required = keys) {
|
|
20
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key)) && required.every(key => Object.hasOwn(value, key))
|
|
21
|
+
}
|
|
22
|
+
function strings(value, nonempty = false) { return array(value) && (!nonempty || value.length > 0) && value.every(text) && new Set(value).size === value.length }
|
|
23
|
+
function condition(c, owners) {
|
|
24
|
+
return dictionary(c, ['id', 'owner', 'severity', 'classification', 'requirement', 'scope', 'evidence']) && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(c.id) && owners.includes(c.owner) && severities.includes(c.severity) && kinds.includes(c.classification) && text(c.requirement) && strings(c.scope, true) && text(c.evidence)
|
|
25
|
+
}
|
|
26
|
+
function risks(r, owners) { return dictionary(r, ['assessment', 'evidence', 'conditions']) && text(r.assessment) && text(r.evidence) && array(r.conditions) && r.conditions.every(c => condition(c, owners)) }
|
|
27
|
+
function checkpointProof(p) {
|
|
28
|
+
return dictionary(p, ['checkpointDigest', 'evidence', 'supported', 'route', 'findings', 'reportDigest']) && hash(p.checkpointDigest) && text(p.evidence) && p.supported === true && ['native', 'mode-b', 'fallback'].includes(p.route) && array(p.findings) && p.findings.every(v => dictionary(v, ['digest', 'conditionId']) && hash(v.digest) && text(v.conditionId)) && (p.reportDigest === null || hash(p.reportDigest))
|
|
29
|
+
}
|
|
30
|
+
function reviewerFamily(stage) { return stage.qeReviewerUsed === 'codex-fallback' ? 'codex' : stage.qeReviewerUsed }
|
|
31
|
+
function reviewerEntry(r, roster) {
|
|
32
|
+
const owners = roster.map(p => p.id)
|
|
33
|
+
return !(!dictionary(r, ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification', 'checkpointVerification'], ['reviewer', 'family', 'independent', 'verdict', 'conditions', 'verifications', 'newRisks', 'authorAssessmentChecked', 'deltaVerification']) || !owners.includes(r.reviewer) || roster.find(p => p.id === r.reviewer)?.family !== r.family || r.independent !== true || !['clean', 'conditional', 'no-go'].includes(r.verdict) || !array(r.conditions) || !r.conditions.every(c => condition(c, owners) && c.owner === r.reviewer) || !array(r.verifications) || !risks(r.newRisks, owners) || !r.newRisks.conditions.every(c => c.owner === r.reviewer) || typeof r.authorAssessmentChecked !== 'boolean')
|
|
34
|
+
}
|
|
35
|
+
function result(phase, verdict, revision, reasons, unresolved = []) { return { schema: SCHEMA, phase, verdict, revision, unresolved, reasons } }
|
|
36
|
+
|
|
37
|
+
export function evaluateReviewConvergence(host, receipt, currentManifest) {
|
|
38
|
+
const phase = host?.phase
|
|
39
|
+
const refuse = reason => result(phase || 'qe', 'not-established', host?.snapshot?.revision || null, [reason], (host?.conditions || []).map(c => c.id))
|
|
40
|
+
if (!dictionary(host, ['schema', 'phase', 'reviewers', 'snapshot', 'conditions', 'reviewSeen', 'rework', 'changedPaths']) || host.schema !== SCHEMA || !phases.includes(phase) || !array(host.reviewers, 16) || host.reviewers.length === 0) return refuse('host-lineage-invalid')
|
|
41
|
+
const owners = host.reviewers.map(r => r.id)
|
|
42
|
+
if (new Set(owners).size !== owners.length || !host.reviewers.every(r => dictionary(r, ['id', 'family']) && text(r.id) && ['codex', 'claude', 'owner-exception'].includes(r.family))) return refuse('host-reviewers-invalid')
|
|
43
|
+
if (!dictionary(host.snapshot, ['nonce', 'revision', 'manifest']) || !text(host.snapshot.nonce) || !hash(host.snapshot.revision) || !array(host.conditions) || !host.conditions.every(c => condition(c, owners)) || typeof host.reviewSeen !== 'boolean' || typeof host.rework !== 'boolean' || !strings(host.changedPaths)) return refuse('host-snapshot-invalid')
|
|
44
|
+
if (!same(host.snapshot.manifest, currentManifest) || digest(JSON.stringify(currentManifest)) !== host.snapshot.revision) return refuse('artifact-manifest-changed')
|
|
45
|
+
if (!dictionary(receipt, ['schema', 'phase', 'nonce', 'revision', 'reviews', 'author']) || receipt.schema !== SCHEMA || receipt.phase !== phase || receipt.nonce !== host.snapshot.nonce || receipt.revision !== host.snapshot.revision) return refuse('receipt-phase-or-revision-invalid')
|
|
46
|
+
if (!array(receipt.reviews, 16) || receipt.reviews.length !== owners.length || new Set(receipt.reviews.map(r => r?.reviewer)).size !== owners.length) return refuse('reviewer-set-invalid')
|
|
47
|
+
const all = new Map(host.conditions.map(c => [c.id, c]))
|
|
48
|
+
const verified = new Set()
|
|
49
|
+
const reasons = []
|
|
50
|
+
const needsAuthor = host.rework || (host.reviewSeen && host.conditions.length > 0)
|
|
51
|
+
if (needsAuthor && !receipt.author) return refuse('author-delta-missing')
|
|
52
|
+
if (receipt.author !== null) {
|
|
53
|
+
const a = receipt.author
|
|
54
|
+
if (!dictionary(a, ['addressed', 'changedScope', 'classification', 'delta', 'evidence', 'newRisks']) || !strings(a.addressed) || !strings(a.changedScope, true) || !kinds.includes(a.classification) || !text(a.delta) || !text(a.evidence) || !risks(a.newRisks, owners)) return refuse('author-delta-invalid')
|
|
55
|
+
if (host.changedPaths.some(path => !a.changedScope.includes(path))) return refuse('author-delta-omits-measured-changes')
|
|
56
|
+
if (a.addressed.some(id => !all.has(id))) return refuse('author-addressed-unknown-condition')
|
|
57
|
+
for (const c of a.newRisks.conditions) {
|
|
58
|
+
if (all.has(c.id) && !same(all.get(c.id), c)) return refuse('author-risk-condition-redefined')
|
|
59
|
+
all.set(c.id, c)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const r of receipt.reviews) {
|
|
63
|
+
if (!reviewerEntry(r, host.reviewers)) return refuse('reviewer-contract-invalid')
|
|
64
|
+
if (Object.hasOwn(r, 'checkpointVerification') && !checkpointProof(r.checkpointVerification)) return refuse('checkpoint-verification-contract-invalid')
|
|
65
|
+
if (receipt.author && !r.authorAssessmentChecked) return refuse('author-risk-assessment-not-reviewed')
|
|
66
|
+
if (receipt.author) {
|
|
67
|
+
const d = r.deltaVerification
|
|
68
|
+
if (!dictionary(d, ['classification', 'evidence', 'implementationVerified']) || !kinds.includes(d.classification) || !text(d.evidence) || typeof d.implementationVerified !== 'boolean') return refuse('independent-delta-classification-missing')
|
|
69
|
+
if ((receipt.author.classification === 'structural' || d.classification === 'structural') && (!d.implementationVerified || d.classification !== 'structural')) reasons.push('structural-delta-not-verified:' + r.reviewer)
|
|
70
|
+
} else if (r.deltaVerification !== null) return refuse('unexpected-delta-verification')
|
|
71
|
+
const own = r.conditions.concat(r.newRisks.conditions)
|
|
72
|
+
if (new Set(own.map(c => c.id)).size !== own.length) return refuse('duplicate-condition')
|
|
73
|
+
for (const c of host.conditions.filter(c => c.owner === r.reviewer)) if (!own.some(n => same(n, c))) return refuse('prior-condition-omitted-or-redefined:' + c.id)
|
|
74
|
+
for (const c of own) {
|
|
75
|
+
if (c.scope.some(path => !currentManifest.some(p => p.path === path))) return refuse('condition-scope-unmeasured:' + c.id)
|
|
76
|
+
if (all.has(c.id) && !same(all.get(c.id), c)) return refuse('condition-redefined:' + c.id)
|
|
77
|
+
all.set(c.id, c)
|
|
78
|
+
}
|
|
79
|
+
if (r.verdict === 'clean' && own.length > 0 && r.verifications.length === 0) return refuse('clean-verdict-contradicts-conditions')
|
|
80
|
+
if (r.verdict === 'conditional' && own.length === 0) return refuse('conditional-without-conditions')
|
|
81
|
+
if (r.verdict === 'no-go') reasons.push('reviewer-no-go:' + r.reviewer)
|
|
82
|
+
const seen = new Set()
|
|
83
|
+
for (const v of r.verifications) {
|
|
84
|
+
if (!dictionary(v, ['id', 'revision', 'classification', 'evidence', 'implementationVerified']) || seen.has(v.id) || !all.has(v.id) || all.get(v.id).owner !== r.reviewer || v.revision !== host.snapshot.revision || !kinds.includes(v.classification) || !text(v.evidence) || typeof v.implementationVerified !== 'boolean') return refuse('verification-invalid-or-foreign')
|
|
85
|
+
seen.add(v.id)
|
|
86
|
+
// A semantic change cannot become a weaker check by the author's label.
|
|
87
|
+
const structural = all.get(v.id).classification === 'structural' || receipt.author?.classification === 'structural' || v.classification === 'structural'
|
|
88
|
+
if (structural && (!v.implementationVerified || v.classification !== 'structural')) reasons.push('structural-verification-missing:' + v.id)
|
|
89
|
+
else verified.add(v.id)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const unresolved = [...all.keys()].filter(id => !verified.has(id))
|
|
93
|
+
for (const id of unresolved) reasons.push('unresolved:' + id)
|
|
94
|
+
if (receipt.author?.newRisks.conditions.some(c => !receipt.reviews.some(r => r.conditions.concat(r.newRisks.conditions).some(n => same(n, c))))) reasons.push('author-risk-not-adopted-by-reviewer')
|
|
95
|
+
return result(phase, reasons.length ? 'unresolved' : 'closed', host.snapshot.revision, reasons, unresolved)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safePath(root, rel, allowMissing = true) {
|
|
99
|
+
if (!text(rel) || rel.startsWith('/') || rel.includes('\\') || rel.split('/').some(p => !p || p === '.' || p === '..') || /[\x00-\x1f]/.test(rel)) throw Error('unsafe-artifact-path')
|
|
100
|
+
let path = root
|
|
101
|
+
for (const part of rel.split('/')) {
|
|
102
|
+
path = resolve(path, part)
|
|
103
|
+
if (!existsSync(path)) { if (allowMissing) return resolve(root, rel); throw Error('artifact-missing:' + rel) }
|
|
104
|
+
if (lstatSync(path).isSymbolicLink()) throw Error('symlink-artifact:' + rel)
|
|
105
|
+
}
|
|
106
|
+
return path
|
|
107
|
+
}
|
|
108
|
+
function readJson(path) { return JSON.parse(readFileSync(path, 'utf8')) }
|
|
109
|
+
function writeJson(path, value) {
|
|
110
|
+
if (existsSync(path) && (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())) throw Error('unsafe-checkpoint')
|
|
111
|
+
const temporary = path + '.' + randomUUID() + '.tmp'
|
|
112
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' })
|
|
113
|
+
renameSync(temporary, path)
|
|
114
|
+
}
|
|
115
|
+
function pathsFor(repo, feature, phase) {
|
|
116
|
+
const prefix = relative(repo, feature).split(sep).join('/')
|
|
117
|
+
if (!prefix || prefix.startsWith('../')) throw Error('feature-must-be-inside-repo')
|
|
118
|
+
safePath(repo, prefix, false)
|
|
119
|
+
const targets = ['00_complexity_assessment.md', '01_requirements.md', '02_research.md', '04_domain_model.md', '05_architecture.md']
|
|
120
|
+
if (phase === 'qe') targets.push('06_implementation_plan.md', '03.5_ideation_report.md')
|
|
121
|
+
const adr = safePath(repo, prefix + '/03_adr')
|
|
122
|
+
if (existsSync(adr) && !lstatSync(adr).isDirectory()) throw Error('adr-not-directory')
|
|
123
|
+
const decisions = existsSync(adr) ? readdirSync(adr).filter(p => /^[0-9]{3}-.+\.md$/.test(p)).sort() : []
|
|
124
|
+
if (phase === 'ideation' && !decisions.length) throw Error('adr-missing')
|
|
125
|
+
const paths = targets.filter(p => existsSync(resolve(feature, p))).concat(decisions.map(p => '03_adr/' + p)).map(p => prefix + '/' + p)
|
|
126
|
+
for (const required of phase === 'ideation' ? ['01_requirements.md', '05_architecture.md'] : ['01_requirements.md', '06_implementation_plan.md']) if (!paths.includes(prefix + '/' + required)) throw Error('design-input-missing:' + required)
|
|
127
|
+
if (phase === 'qe') {
|
|
128
|
+
const plan = readFileSync(safePath(repo, prefix + '/06_implementation_plan.md', false), 'utf8')
|
|
129
|
+
const block = plan.split(/^EXPECTED_CODE_TARGETS:\s*$/m)
|
|
130
|
+
if (block.length !== 2) throw Error('target-block-missing-or-duplicate')
|
|
131
|
+
const planned = block[1].split('\n').filter(line => line.trim()).map(line => {
|
|
132
|
+
const match = /^- (.+)$/.exec(line)
|
|
133
|
+
if (!match) throw Error('target-block-malformed')
|
|
134
|
+
return match[1]
|
|
135
|
+
})
|
|
136
|
+
if (!planned.length) throw Error('target-block-empty')
|
|
137
|
+
if (planned.some(p => /^(features|\.dz|\.agentic-qe|roam)\//.test(p))) throw Error('circular-review-target')
|
|
138
|
+
paths.push(...planned)
|
|
139
|
+
// Additions/deletions are discovered by the host, not the author delta request.
|
|
140
|
+
const baseFile = resolve(feature, '.fa-state/base-ref')
|
|
141
|
+
const base = existsSync(baseFile) ? readFileSync(baseFile, 'utf8').trim() : 'HEAD'
|
|
142
|
+
if (!/^[A-Za-z0-9_./:-]+$/.test(base) || base.startsWith('-')) throw Error('base-ref-invalid')
|
|
143
|
+
for (const command of [['diff', '--name-only', '-z', base, '--'], ['ls-files', '--others', '--exclude-standard', '-z']]) {
|
|
144
|
+
const names = execFileSync('git', command, { cwd: repo, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 }).split('\0').filter(Boolean)
|
|
145
|
+
paths.push(...names.filter(p => !p.startsWith('features/') && !p.startsWith('.dz/') && !p.startsWith('.agentic-qe/') && !p.startsWith('roam/') && p !== 'architecture/map.json'))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...new Set(paths)].sort()
|
|
149
|
+
}
|
|
150
|
+
export function measureManifest(repo, feature, phase) {
|
|
151
|
+
return pathsFor(repo, feature, phase).map(path => {
|
|
152
|
+
const absolute = safePath(repo, path)
|
|
153
|
+
if (!existsSync(absolute)) return { path, digest: null }
|
|
154
|
+
if (!lstatSync(absolute).isFile()) throw Error('non-file-artifact:' + path)
|
|
155
|
+
return { path, digest: digest(readFileSync(absolute)) }
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
function checkpointPaths(repo, feature, phase) {
|
|
159
|
+
const stateRel = relative(repo, feature).split(sep).join('/') + '/.fa-state'
|
|
160
|
+
const state = safePath(repo, stateRel)
|
|
161
|
+
mkdirSync(state, { recursive: true })
|
|
162
|
+
return { host: safePath(repo, stateRel + '/review-convergence-' + phase + '-host.json'), review: safePath(repo, stateRel + '/review-convergence-' + phase + '-review.json') }
|
|
163
|
+
}
|
|
164
|
+
function qeCheckpoint(repo, feature) {
|
|
165
|
+
const rel = relative(repo, feature).split(sep).join('/') + '/.fa-state/checkpoints.jsonl'
|
|
166
|
+
const file = safePath(repo, rel)
|
|
167
|
+
if (!existsSync(file)) return null
|
|
168
|
+
let checkpoint = null
|
|
169
|
+
for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
170
|
+
const item = JSON.parse(line)
|
|
171
|
+
if (item.stage === 'qe') checkpoint = item.result
|
|
172
|
+
}
|
|
173
|
+
return checkpoint ? { checkpointDigest: digest(JSON.stringify(checkpoint)), checkpoint } : null
|
|
174
|
+
}
|
|
175
|
+
function seriousFindings(source) {
|
|
176
|
+
return (array(source?.gaps) ? source.gaps : []).filter(gap => [gap.sev, gap.severity, gap.priority].some(v => /(?:^|[^A-Z0-9])(BLOCKER|CRITICAL|HIGH|P0|P1)(?:$|[^A-Z0-9])/.test(String(v || '').toUpperCase())))
|
|
177
|
+
}
|
|
178
|
+
function reconcileCheckpoint(host, receipt, measured, repo, feature) {
|
|
179
|
+
if (!measured) return null
|
|
180
|
+
const stage = measured.checkpoint
|
|
181
|
+
for (const [base, source, family, missing] of [
|
|
182
|
+
['qe-primary', stage.qe, reviewerFamily(stage), !stage.qe || Boolean(stage.qe?.convergenceUnavailable)],
|
|
183
|
+
['qe-precision', stage.qe2, 'claude', host.reviewers.some(r => r.id.startsWith('qe-precision')) && (!stage.qe2 || !stage.qe2.reportWritten)],
|
|
184
|
+
]) {
|
|
185
|
+
const findings = seriousFindings(source)
|
|
186
|
+
if (!missing && !findings.length) continue
|
|
187
|
+
const origin = host.reviewers.find(r => (r.id === base || r.id === base + ':' + family) && r.family === family)
|
|
188
|
+
const review = receipt.reviews.find(r => r.reviewer === origin?.id)
|
|
189
|
+
const proof = review?.checkpointVerification
|
|
190
|
+
if (!origin || !checkpointProof(proof) || proof.checkpointDigest !== measured.checkpointDigest || !text(proof.evidence) || proof.supported !== true || !['native', 'mode-b', 'fallback'].includes(proof.route) || !array(proof.findings) || proof.findings.length !== findings.length || !(proof.reportDigest === null || hash(proof.reportDigest))) return null
|
|
191
|
+
const mapped = new Set()
|
|
192
|
+
for (const gap of findings) {
|
|
193
|
+
const item = proof.findings.find(v => v.digest === digest(JSON.stringify(gap)))
|
|
194
|
+
if (!dictionary(item, ['digest', 'conditionId']) || mapped.has(item.conditionId) || !host.conditions.some(c => c.id === item.conditionId && c.owner === origin.id && ['BLOCKER', 'CRITICAL', 'HIGH'].includes(c.severity)) || !review.verifications.some(v => v.id === item.conditionId && v.revision === host.snapshot.revision && v.implementationVerified === true && v.classification === 'structural')) return null
|
|
195
|
+
mapped.add(item.conditionId)
|
|
196
|
+
}
|
|
197
|
+
if (base === 'qe-precision' && missing) {
|
|
198
|
+
const file = safePath(repo, relative(repo, feature).split(sep).join('/') + '/08_qe_report.md', false)
|
|
199
|
+
const report = readFileSync(file)
|
|
200
|
+
if (digest(report) !== proof.reportDigest || !/## Primary QE pass/.test(report.toString()) || !/## Precision QE pass/.test(report.toString()) || !/Combined Step-8 grade:/.test(report.toString())) return null
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ...measured, revision: host.snapshot.revision }
|
|
204
|
+
}
|
|
205
|
+
function retainPendingReceipt(host, path) {
|
|
206
|
+
if (!host || !existsSync(path)) return host
|
|
207
|
+
let receipt
|
|
208
|
+
try { receipt = readJson(path) } catch (_) { throw Error('pending-reviewer-receipt-needs-origin-review') }
|
|
209
|
+
if (!dictionary(receipt, ['schema', 'phase', 'nonce', 'revision', 'reviews', 'author']) || receipt.schema !== SCHEMA || receipt.phase !== host.phase || !text(receipt.nonce) || !hash(receipt.revision) || !array(receipt.reviews, 16) || !receipt.reviews.length || new Set(receipt.reviews.map(r => r?.reviewer)).size !== receipt.reviews.length) throw Error('pending-reviewer-receipt-needs-origin-review')
|
|
210
|
+
const owners = host.reviewers.map(r => r.id), preserved = new Map(host.conditions.map(c => [c.id, c]))
|
|
211
|
+
const fresh = receipt.nonce === host.snapshot.nonce && receipt.revision === host.snapshot.revision
|
|
212
|
+
let problem = false, witnessed = false
|
|
213
|
+
for (const r of receipt.reviews) {
|
|
214
|
+
if (!reviewerEntry(r, host.reviewers) || Object.hasOwn(r, 'checkpointVerification') && !checkpointProof(r.checkpointVerification)) { problem = true; continue }
|
|
215
|
+
const own = r.conditions.concat(r.newRisks.conditions)
|
|
216
|
+
if (new Set(own.map(c => c.id)).size !== own.length) { problem = true; continue }
|
|
217
|
+
witnessed = true
|
|
218
|
+
for (const c of own) {
|
|
219
|
+
// Stale evidence may preserve a known condition; it cannot originate new host authority.
|
|
220
|
+
if (preserved.has(c.id)) { if (!same(preserved.get(c.id), c)) problem = true }
|
|
221
|
+
else if (!fresh || c.scope.some(path => !host.snapshot.manifest.some(p => p.path === path))) problem = true
|
|
222
|
+
else if (condition(c, owners) && c.owner === r.reviewer) preserved.set(c.id, c)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const next = { ...host, conditions: [...preserved.values()], reviewSeen: host.reviewSeen || witnessed }
|
|
226
|
+
// This imports original independent findings only, never closure, author claims or a grade.
|
|
227
|
+
return { next, problem }
|
|
228
|
+
}
|
|
229
|
+
function bindReviewers(host, requested, measured) {
|
|
230
|
+
if (!host) return requested
|
|
231
|
+
const bound = host.reviewers.map(r => ({ ...r }))
|
|
232
|
+
for (const actual of requested) {
|
|
233
|
+
const prior = bound.find(r => r.id === actual.id)
|
|
234
|
+
if (!prior) throw Error('host-lineage-conflict')
|
|
235
|
+
if (prior.family === actual.family) continue
|
|
236
|
+
const originated = host.reviewSeen || host.conditions.some(c => c.owner === prior.id) || measured && (prior.id === 'qe-primary' && reviewerFamily(measured.checkpoint) === prior.family || prior.id === 'qe-precision' && measured.checkpoint.qe2)
|
|
237
|
+
if (!originated) prior.family = actual.family
|
|
238
|
+
else if (!bound.some(r => r.id === actual.id + ':' + actual.family)) bound.push({ id: actual.id + ':' + actual.family, family: actual.family })
|
|
239
|
+
}
|
|
240
|
+
if (host.reviewers.some(r => !r.id.includes(':') && !requested.some(n => n.id === r.id)) || bound.length > 16) throw Error('host-lineage-conflict')
|
|
241
|
+
return bound
|
|
242
|
+
}
|
|
243
|
+
export function runGate({ action, repo, feature, phase, reviewers }) {
|
|
244
|
+
if (!phases.includes(phase) || !['prepare', 'evaluate'].includes(action)) throw Error('invalid-command')
|
|
245
|
+
repo = realpathSync(repo); feature = resolve(repo, feature)
|
|
246
|
+
const current = measureManifest(repo, feature, phase)
|
|
247
|
+
const paths = checkpointPaths(repo, feature, phase)
|
|
248
|
+
let host = existsSync(paths.host) ? readJson(paths.host) : null
|
|
249
|
+
const measuredCheckpoint = phase === 'qe' ? qeCheckpoint(repo, feature) : null
|
|
250
|
+
if (action === 'prepare') {
|
|
251
|
+
if (!array(reviewers, 16) || !reviewers.length || !reviewers.every(r => dictionary(r, ['id', 'family']) && text(r.id) && ['codex', 'claude', 'owner-exception'].includes(r.family)) || new Set(reviewers.map(r => r.id)).size !== reviewers.length) throw Error('reviewers-invalid')
|
|
252
|
+
if (host && (host.phase !== phase || host.schema !== SCHEMA || !array(host.conditions) || typeof host.reviewSeen !== 'boolean')) throw Error('host-lineage-conflict')
|
|
253
|
+
if (host && existsSync(paths.review)) {
|
|
254
|
+
const pending = retainPendingReceipt(host, paths.review)
|
|
255
|
+
host = pending.next; writeJson(paths.host, host)
|
|
256
|
+
if (pending.problem) throw Error('pending-reviewer-receipt-needs-origin-review')
|
|
257
|
+
}
|
|
258
|
+
reviewers = bindReviewers(host, reviewers, measuredCheckpoint)
|
|
259
|
+
const revision = digest(JSON.stringify(current))
|
|
260
|
+
const snapshot = host?.snapshot?.revision === revision ? host.snapshot : { nonce: randomUUID(), revision, manifest: current }
|
|
261
|
+
const priorManifest = new Map((host?.snapshot?.manifest || []).map(p => [p.path, p.digest]))
|
|
262
|
+
const newManifest = new Map(current.map(p => [p.path, p.digest]))
|
|
263
|
+
const changedPaths = host?.snapshot?.revision === revision ? host.changedPaths : host ? [...new Set([...(host.changedPaths || []), ...[...new Set([...priorManifest.keys(), ...newManifest.keys()])].filter(path => priorManifest.get(path) !== newManifest.get(path))])].sort() : []
|
|
264
|
+
const next = { schema: SCHEMA, phase, reviewers, snapshot, conditions: host?.conditions || [], reviewSeen: host?.reviewSeen || false, rework: Boolean(host && (host.rework || host.reviewSeen && (host.snapshot.revision !== revision || host.conditions.length > 0))), changedPaths }
|
|
265
|
+
writeJson(paths.host, next)
|
|
266
|
+
return { ...result(phase, 'prepared', revision, []), nonce: snapshot.nonce, manifest: current, conditions: next.conditions, reviewers, checkpointDigest: measuredCheckpoint?.checkpointDigest || null }
|
|
267
|
+
}
|
|
268
|
+
if (!host) return result(phase, 'not-established', null, ['host-lineage-missing'])
|
|
269
|
+
if (!existsSync(paths.review)) return result(phase, 'not-established', host.snapshot.revision, ['reviewer-receipt-missing'], host.conditions.map(c => c.id))
|
|
270
|
+
const receipt = readJson(paths.review)
|
|
271
|
+
let verdict = evaluateReviewConvergence(host, receipt, current)
|
|
272
|
+
let closure = null
|
|
273
|
+
if (phase === 'qe' && verdict.verdict === 'closed' && measuredCheckpoint) {
|
|
274
|
+
closure = reconcileCheckpoint(host, receipt, measuredCheckpoint, repo, feature)
|
|
275
|
+
if (!closure) verdict = result(phase, 'not-established', host.snapshot.revision, ['historical-checkpoint-own-verification-missing'])
|
|
276
|
+
}
|
|
277
|
+
// Preserve independently owned findings even on refusal. Never import author/scribe claims.
|
|
278
|
+
// Invalid ownership/meaning refuses without overwriting the prior host authority.
|
|
279
|
+
const owners = host.reviewers.map(r => r.id)
|
|
280
|
+
const preserved = new Map(host.conditions.map(c => [c.id, c]))
|
|
281
|
+
if (receipt?.schema === SCHEMA && receipt.phase === phase && array(receipt.reviews)) {
|
|
282
|
+
for (const r of receipt.reviews) if (owners.includes(r?.reviewer) && host.reviewers.find(p => p.id === r.reviewer)?.family === r.family && r.independent === true) {
|
|
283
|
+
for (const c of [...(array(r.conditions) ? r.conditions : []), ...(array(r.newRisks?.conditions) ? r.newRisks.conditions : [])]) {
|
|
284
|
+
if (condition(c, owners) && c.owner === r.reviewer && !preserved.has(c.id)) preserved.set(c.id, c)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (receipt?.phase === phase && receipt.nonce === host.snapshot.nonce && receipt.revision === host.snapshot.revision && risks(receipt.author?.newRisks, owners)) {
|
|
289
|
+
for (const c of receipt.author.newRisks.conditions) if (!preserved.has(c.id)) preserved.set(c.id, c)
|
|
290
|
+
}
|
|
291
|
+
writeJson(paths.host, { ...host, conditions: [...preserved.values()], reviewSeen: true, changedPaths: verdict.verdict === 'closed' ? [] : host.changedPaths })
|
|
292
|
+
if (closure && verdict.verdict === 'closed') return { ...verdict, checkpointClosure: closure }
|
|
293
|
+
return verdict
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
297
|
+
let phase = 'qe'
|
|
298
|
+
try {
|
|
299
|
+
const args = process.argv.slice(2)
|
|
300
|
+
const action = args.shift()
|
|
301
|
+
const options = {}
|
|
302
|
+
while (args.length) {
|
|
303
|
+
const key = args.shift()
|
|
304
|
+
if (!['--repo', '--feature', '--phase', '--reviewers'].includes(key) || !args.length || Object.hasOwn(options, key)) throw Error('invalid-arguments')
|
|
305
|
+
options[key] = args.shift()
|
|
306
|
+
}
|
|
307
|
+
phase = options['--phase']
|
|
308
|
+
if (!options['--repo'] || !options['--feature'] || !phases.includes(phase)) throw Error('invalid-arguments')
|
|
309
|
+
const output = runGate({ action, repo: options['--repo'], feature: options['--feature'], phase, reviewers: options['--reviewers'] ? JSON.parse(options['--reviewers']) : undefined })
|
|
310
|
+
process.stdout.write(JSON.stringify(output) + '\n')
|
|
311
|
+
process.exitCode = ['closed', 'prepared'].includes(output.verdict) ? 0 : output.verdict === 'unresolved' ? 1 : 3
|
|
312
|
+
} catch (error) {
|
|
313
|
+
process.stdout.write(JSON.stringify(result(phases.includes(phase) ? phase : 'qe', 'not-established', null, [String(error.message)])) + '\n')
|
|
314
|
+
process.exitCode = 3
|
|
315
|
+
}
|
|
316
|
+
}
|