peaks-loop 4.0.53 → 4.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/codegraph-commands.js +164 -5
- package/dist/cli/commands/core/memory-command.js +35 -2
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/prd-commands.js +15 -0
- package/dist/cli/commands/sub-agent-shared.d.ts +23 -0
- package/dist/cli/commands/sub-agent-shared.js +39 -0
- package/dist/services/artifacts/artifact-prerequisites.js +34 -0
- package/dist/services/audit/enforcer-liveness.js +1 -1
- package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +44 -0
- package/dist/services/codegraph/codegraph-config-repair-writer.js +112 -13
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +23 -1
- package/dist/services/codegraph/codegraph-exclude-repair.js +6 -0
- package/dist/services/evidence/evidence-generator.js +11 -3
- package/dist/services/memory/project-memory-service/index.d.ts +1 -1
- package/dist/services/memory/project-memory-service/index.js +1 -1
- package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +59 -0
- package/dist/services/memory/project-memory-service/store/atomic-write.js +167 -7
- package/dist/services/prd/gate-evidence-derivation.d.ts +160 -0
- package/dist/services/prd/gate-evidence-derivation.js +270 -0
- package/dist/services/prd/handoff-auto-regen.js +13 -1
- package/dist/services/prd/handoff-frontmatter.d.ts +1 -1
- package/dist/services/prd/handoff-frontmatter.js +62 -0
- package/dist/services/prd/handoff-gate-evidence.d.ts +95 -1
- package/dist/services/prd/handoff-gate-evidence.js +125 -57
- package/dist/services/prd/handoff-service.d.ts +21 -1
- package/dist/services/prd/handoff-service.js +45 -2
- package/dist/services/prd/handoff-types.d.ts +40 -0
- package/dist/services/prd/handoff-types.js +28 -1
- package/dist/services/prd/project-scan-reader.d.ts +7 -0
- package/dist/services/prd/project-scan-reader.js +7 -1
- package/dist/services/rd/reviewer-dispatch-policy.d.ts +36 -8
- package/dist/services/rd/reviewer-dispatch-policy.js +36 -8
- package/package.json +6 -6
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +26 -10
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +1 -1
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +7 -5
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- /package/{docs → contracts}/test-style-contract.md +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `gateEvidence` comes FROM, and where its promise is checked.
|
|
3
|
+
*
|
|
4
|
+
* B2 (merged with the B1 repair), rid `rid-b2-gate-evidence-wiring`.
|
|
5
|
+
*
|
|
6
|
+
* B1 made the field real at the SERVICE layer and QA found the hole that
|
|
7
|
+
* mattered (F1 of `rid-b1-qa`): all three frontmatter producers passed
|
|
8
|
+
* nothing, so no capsule on disk ever carried the field — the dead surface
|
|
9
|
+
* had moved one layer out, from "no producer function" to "no production
|
|
10
|
+
* caller". This module closes that by DERIVING the map instead of accepting
|
|
11
|
+
* it from a caller.
|
|
12
|
+
*
|
|
13
|
+
* WHY DERIVED, NOT PASSED IN. Every path in the map is a function of the
|
|
14
|
+
* session id, the request id and the REQUEST TYPE, all of which the producer
|
|
15
|
+
* already has. Asking the caller to supply the map would put a second source
|
|
16
|
+
* next to the derivation, and a CLI flag (`--gate-evidence <json>`) would
|
|
17
|
+
* additionally require the user to hand-author JSON, which the project's
|
|
18
|
+
* Human-NL-Choice-Only rule forbids. So: one function, computed, no flag.
|
|
19
|
+
*
|
|
20
|
+
* THE TYPE-AWARE TENSION, AND HOW IT IS RESOLVED. The schema doc
|
|
21
|
+
* (`skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:53`) calls
|
|
22
|
+
* the five keys a schema and says "Missing keys → Gate C failure". Taken
|
|
23
|
+
* literally that demands all five paths in every capsule — and then a `docs`
|
|
24
|
+
* slice, which Gate C requires NO evidence from, would fail the gate for
|
|
25
|
+
* "declaring" `audit/perf-<rid>.md` and `audit/security-<rid>.md`, two files
|
|
26
|
+
* its type is not supposed to produce. Declaring evidence that does not and
|
|
27
|
+
* should not exist is a false claim, and a gate that fails on a false claim
|
|
28
|
+
* the writer invented is worse than useless.
|
|
29
|
+
*
|
|
30
|
+
* Resolution, and the reason it is not a special case: the map declares
|
|
31
|
+
* EXACTLY what this request type's transition gate requires, read from
|
|
32
|
+
* `PREREQUISITES_BY_TYPE` — the same table `checkPrerequisites` enforces at
|
|
33
|
+
* `rd:qa-handoff`. So:
|
|
34
|
+
*
|
|
35
|
+
* - feature / refactor → projectScan, prdHandoff, codeReview,
|
|
36
|
+
* securityReview, perfBaseline (all five)
|
|
37
|
+
* - bugfix → the same five
|
|
38
|
+
* - config → projectScan, securityReview (TWO keys, verified
|
|
39
|
+
* against the table rather than assumed — `CONFIG_TABLE['rd:qa-handoff']`
|
|
40
|
+
* is `[SECURITY_REVIEW]` alone, so there is no `prdHandoff`: config has no
|
|
41
|
+
* `AUDIT_REQUIRES_HANDOFF` row. An earlier revision of this comment
|
|
42
|
+
* claimed three, and QA caught prose that disagreed with the code, which
|
|
43
|
+
* is the defect this module exists to end. The security path declared is
|
|
44
|
+
* the config-specific `rd/security-review.md` — genuinely ridless, per its
|
|
45
|
+
* own row — not the documented `audit/security-<rid>.md`, which no config
|
|
46
|
+
* slice writes)
|
|
47
|
+
* - docs / chore → NOTHING. No `rd:qa-handoff` row ⇒ nothing for Gate C
|
|
48
|
+
* to validate ⇒ nothing declared (see below)
|
|
49
|
+
*
|
|
50
|
+
* WHY `projectScan` IS DECLARED, AND WHY ONLY FOR GATED TYPES. It is not read
|
|
51
|
+
* from the table — it is Gate A's artifact, and Gate A is type-independent
|
|
52
|
+
* ("After workspace init + project scan", step 0.6 of every workflow). But a
|
|
53
|
+
* declaration is only worth making when something VERIFIES it, and the only
|
|
54
|
+
* verifier is `checkDeclaredGateEvidence`, which runs on `rd:qa-handoff`. For a
|
|
55
|
+
* type with a gate row, Gate C therefore does check this path like any other.
|
|
56
|
+
* For `docs`/`chore` nothing ever reads it, so it is not declared: keeping an
|
|
57
|
+
* inert statement in every docs capsule would recreate, in miniature, the exact
|
|
58
|
+
* defect this whole line of work exists to remove — a claim no one reads, which
|
|
59
|
+
* is how the field was dead in the first place (F1 of `rid-b2-qa`). Scope, in
|
|
60
|
+
* one sentence: **this map declares the evidence paths Gate C will check for
|
|
61
|
+
* this slice**, so an empty declaration is the honest one when there is no such
|
|
62
|
+
* gate.
|
|
63
|
+
*
|
|
64
|
+
* THE REJECTED ALTERNATIVE, named so it is not silently revisited: declaring
|
|
65
|
+
* all five unconditionally and teaching Gate C to check only the
|
|
66
|
+
* type-required subset. Rejected because the capsule would then carry
|
|
67
|
+
* permanent false statements about files that must never exist, and every
|
|
68
|
+
* reader of the frontmatter (peaks-qa's own checklist among them) would need
|
|
69
|
+
* the same type-conditioned filter to avoid mis-reading them.
|
|
70
|
+
*
|
|
71
|
+
* NO DERIVATION → NO DECLARATION. When there is no request artifact to read
|
|
72
|
+
* the type off, this returns `undefined` and the producers write no
|
|
73
|
+
* `gateEvidence` block at all. Never a partial map: a half-derived declaration
|
|
74
|
+
* is a false statement about the missing half's keys, and the pre-B2 bytes are
|
|
75
|
+
* the honest fallback. An id the artifact service REFUSES is not this case and
|
|
76
|
+
* is not reported as `undefined` — see `deriveGateEvidenceForRequest`.
|
|
77
|
+
*/
|
|
78
|
+
import { type RequestType } from '../artifacts/artifact-prerequisites.js';
|
|
79
|
+
import { type GateEvidence } from './handoff-types.js';
|
|
80
|
+
/**
|
|
81
|
+
* The `gateEvidence` map for a slice of `requestType` — the evidence paths
|
|
82
|
+
* Gate C will check for it. Pure: no disk access, so a caller can compute it
|
|
83
|
+
* without side effects. Empty (`{}`) — never a partial map — when the type has
|
|
84
|
+
* no `rd:qa-handoff` row, because then there is no gate to declare anything to.
|
|
85
|
+
*/
|
|
86
|
+
export declare function deriveGateEvidence(opts: {
|
|
87
|
+
readonly sessionId: string;
|
|
88
|
+
readonly requestId: string;
|
|
89
|
+
readonly requestType: RequestType;
|
|
90
|
+
}): GateEvidence;
|
|
91
|
+
/**
|
|
92
|
+
* Derive for a request by reading its PRD artifact — the only place the
|
|
93
|
+
* request TYPE is recorded (`- type: <t>`, written by `peaks request init`
|
|
94
|
+
* and read back by `request-artifact-service.extractMetadata`).
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` when there is NO artifact to read — the one case the
|
|
97
|
+
* producers turn into "write no block" (see the header). That is now the ONLY
|
|
98
|
+
* `undefined`: a `showRequestArtifact` that THROWS is deliberately not caught,
|
|
99
|
+
* so a caller can tell "this slice has nothing to declare" apart from "the
|
|
100
|
+
* artifact could not be read".
|
|
101
|
+
*
|
|
102
|
+
* F4 (`rid-f4-ceiling-breach`). This used to be `catch { return undefined }`,
|
|
103
|
+
* which folded those two into one value — and the repository's own ratchet
|
|
104
|
+
* caught it (`capability-guard-runner/contracts/J03.ts`, rule
|
|
105
|
+
* `catch-return-null`; the ceiling was written for exactly this shape). Nothing
|
|
106
|
+
* is lost by letting the throw through. The refusals `showRequestArtifact` can
|
|
107
|
+
* raise are the rid/sid ones, and BOTH callers reach a byte-identical message
|
|
108
|
+
* on the next statement anyway — `assertSafeHandoffIds` (`handoff-service.ts`)
|
|
109
|
+
* for `prd handoff init`, `generateEvidence`'s own guard for the evidence
|
|
110
|
+
* generator — so the user-visible failure is unchanged; only its origin moves
|
|
111
|
+
* two lines earlier. What the catch DID cover in practice was an I/O failure
|
|
112
|
+
* while reading the artifact file, and swallowing that writes a capsule with no
|
|
113
|
+
* declaration at all, which Gate C reads as "nothing declared, nothing to
|
|
114
|
+
* check" (`field-absent` is not this check's business, below). A silent hole is
|
|
115
|
+
* worse than a loud, already-duplicated one.
|
|
116
|
+
*/
|
|
117
|
+
export declare function deriveGateEvidenceForRequest(opts: {
|
|
118
|
+
readonly projectRoot: string;
|
|
119
|
+
readonly sessionId: string;
|
|
120
|
+
readonly requestId: string;
|
|
121
|
+
}): Promise<GateEvidence | undefined>;
|
|
122
|
+
/** `missing` / `warnings` in the same shape `checkPrerequisites` reports. */
|
|
123
|
+
export interface GateEvidenceDeclarationCheck {
|
|
124
|
+
readonly ok: boolean;
|
|
125
|
+
readonly missing: ReadonlyArray<{
|
|
126
|
+
path: string;
|
|
127
|
+
description: string;
|
|
128
|
+
}>;
|
|
129
|
+
readonly warnings: ReadonlyArray<{
|
|
130
|
+
path: string;
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
}>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* GATE C — is the capsule's OWN declaration true?
|
|
137
|
+
*
|
|
138
|
+
* The division of labour, stated so the two checks cannot drift: the
|
|
139
|
+
* prerequisite table says which artifacts this type MUST produce (it owns the
|
|
140
|
+
* requirement and its file existence); this function says the capsule must
|
|
141
|
+
* not CLAIM evidence it does not have (it owns the declaration). Neither
|
|
142
|
+
* re-implements the other, and both read their paths from wherever the path
|
|
143
|
+
* really comes from.
|
|
144
|
+
*
|
|
145
|
+
* A missing declared path FAILS and names the key, because a declaration the
|
|
146
|
+
* gate cannot verify is exactly the "prose pretending to be data" state this
|
|
147
|
+
* field was created to end. Outcomes that are not failures, each for its own
|
|
148
|
+
* reason:
|
|
149
|
+
* - no capsule → not this check's business (`AUDIT_REQUIRES_HANDOFF` owns it)
|
|
150
|
+
* - `field-absent` → the capsule declares nothing; every pre-B1 capsule,
|
|
151
|
+
* and every slice whose producer could not derive a type
|
|
152
|
+
* - an unknown key → a WARNING: the five-key vocabulary is a typo trap, and
|
|
153
|
+
* the type's real requirement is still enforced by the table, so this
|
|
154
|
+
* informs without blocking
|
|
155
|
+
*/
|
|
156
|
+
export declare function checkDeclaredGateEvidence(opts: {
|
|
157
|
+
readonly projectRoot: string;
|
|
158
|
+
readonly sessionId: string;
|
|
159
|
+
readonly requestId: string;
|
|
160
|
+
}): Promise<GateEvidenceDeclarationCheck>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `gateEvidence` comes FROM, and where its promise is checked.
|
|
3
|
+
*
|
|
4
|
+
* B2 (merged with the B1 repair), rid `rid-b2-gate-evidence-wiring`.
|
|
5
|
+
*
|
|
6
|
+
* B1 made the field real at the SERVICE layer and QA found the hole that
|
|
7
|
+
* mattered (F1 of `rid-b1-qa`): all three frontmatter producers passed
|
|
8
|
+
* nothing, so no capsule on disk ever carried the field — the dead surface
|
|
9
|
+
* had moved one layer out, from "no producer function" to "no production
|
|
10
|
+
* caller". This module closes that by DERIVING the map instead of accepting
|
|
11
|
+
* it from a caller.
|
|
12
|
+
*
|
|
13
|
+
* WHY DERIVED, NOT PASSED IN. Every path in the map is a function of the
|
|
14
|
+
* session id, the request id and the REQUEST TYPE, all of which the producer
|
|
15
|
+
* already has. Asking the caller to supply the map would put a second source
|
|
16
|
+
* next to the derivation, and a CLI flag (`--gate-evidence <json>`) would
|
|
17
|
+
* additionally require the user to hand-author JSON, which the project's
|
|
18
|
+
* Human-NL-Choice-Only rule forbids. So: one function, computed, no flag.
|
|
19
|
+
*
|
|
20
|
+
* THE TYPE-AWARE TENSION, AND HOW IT IS RESOLVED. The schema doc
|
|
21
|
+
* (`skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:53`) calls
|
|
22
|
+
* the five keys a schema and says "Missing keys → Gate C failure". Taken
|
|
23
|
+
* literally that demands all five paths in every capsule — and then a `docs`
|
|
24
|
+
* slice, which Gate C requires NO evidence from, would fail the gate for
|
|
25
|
+
* "declaring" `audit/perf-<rid>.md` and `audit/security-<rid>.md`, two files
|
|
26
|
+
* its type is not supposed to produce. Declaring evidence that does not and
|
|
27
|
+
* should not exist is a false claim, and a gate that fails on a false claim
|
|
28
|
+
* the writer invented is worse than useless.
|
|
29
|
+
*
|
|
30
|
+
* Resolution, and the reason it is not a special case: the map declares
|
|
31
|
+
* EXACTLY what this request type's transition gate requires, read from
|
|
32
|
+
* `PREREQUISITES_BY_TYPE` — the same table `checkPrerequisites` enforces at
|
|
33
|
+
* `rd:qa-handoff`. So:
|
|
34
|
+
*
|
|
35
|
+
* - feature / refactor → projectScan, prdHandoff, codeReview,
|
|
36
|
+
* securityReview, perfBaseline (all five)
|
|
37
|
+
* - bugfix → the same five
|
|
38
|
+
* - config → projectScan, securityReview (TWO keys, verified
|
|
39
|
+
* against the table rather than assumed — `CONFIG_TABLE['rd:qa-handoff']`
|
|
40
|
+
* is `[SECURITY_REVIEW]` alone, so there is no `prdHandoff`: config has no
|
|
41
|
+
* `AUDIT_REQUIRES_HANDOFF` row. An earlier revision of this comment
|
|
42
|
+
* claimed three, and QA caught prose that disagreed with the code, which
|
|
43
|
+
* is the defect this module exists to end. The security path declared is
|
|
44
|
+
* the config-specific `rd/security-review.md` — genuinely ridless, per its
|
|
45
|
+
* own row — not the documented `audit/security-<rid>.md`, which no config
|
|
46
|
+
* slice writes)
|
|
47
|
+
* - docs / chore → NOTHING. No `rd:qa-handoff` row ⇒ nothing for Gate C
|
|
48
|
+
* to validate ⇒ nothing declared (see below)
|
|
49
|
+
*
|
|
50
|
+
* WHY `projectScan` IS DECLARED, AND WHY ONLY FOR GATED TYPES. It is not read
|
|
51
|
+
* from the table — it is Gate A's artifact, and Gate A is type-independent
|
|
52
|
+
* ("After workspace init + project scan", step 0.6 of every workflow). But a
|
|
53
|
+
* declaration is only worth making when something VERIFIES it, and the only
|
|
54
|
+
* verifier is `checkDeclaredGateEvidence`, which runs on `rd:qa-handoff`. For a
|
|
55
|
+
* type with a gate row, Gate C therefore does check this path like any other.
|
|
56
|
+
* For `docs`/`chore` nothing ever reads it, so it is not declared: keeping an
|
|
57
|
+
* inert statement in every docs capsule would recreate, in miniature, the exact
|
|
58
|
+
* defect this whole line of work exists to remove — a claim no one reads, which
|
|
59
|
+
* is how the field was dead in the first place (F1 of `rid-b2-qa`). Scope, in
|
|
60
|
+
* one sentence: **this map declares the evidence paths Gate C will check for
|
|
61
|
+
* this slice**, so an empty declaration is the honest one when there is no such
|
|
62
|
+
* gate.
|
|
63
|
+
*
|
|
64
|
+
* THE REJECTED ALTERNATIVE, named so it is not silently revisited: declaring
|
|
65
|
+
* all five unconditionally and teaching Gate C to check only the
|
|
66
|
+
* type-required subset. Rejected because the capsule would then carry
|
|
67
|
+
* permanent false statements about files that must never exist, and every
|
|
68
|
+
* reader of the frontmatter (peaks-qa's own checklist among them) would need
|
|
69
|
+
* the same type-conditioned filter to avoid mis-reading them.
|
|
70
|
+
*
|
|
71
|
+
* NO DERIVATION → NO DECLARATION. When there is no request artifact to read
|
|
72
|
+
* the type off, this returns `undefined` and the producers write no
|
|
73
|
+
* `gateEvidence` block at all. Never a partial map: a half-derived declaration
|
|
74
|
+
* is a false statement about the missing half's keys, and the pre-B2 bytes are
|
|
75
|
+
* the honest fallback. An id the artifact service REFUSES is not this case and
|
|
76
|
+
* is not reported as `undefined` — see `deriveGateEvidenceForRequest`.
|
|
77
|
+
*/
|
|
78
|
+
import { join } from 'node:path';
|
|
79
|
+
import { pathExists } from 'peaks-loop-shared/fs';
|
|
80
|
+
import { getPrerequisitesFor } from '../artifacts/artifact-prerequisites.js';
|
|
81
|
+
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
82
|
+
import { readHandoffGateEvidence } from './handoff-gate-evidence.js';
|
|
83
|
+
import { resolveHandoffPath } from './handoff-service.js';
|
|
84
|
+
import { GATE_EVIDENCE_KEYS } from './handoff-types.js';
|
|
85
|
+
import { PROJECT_SCAN_RELATIVE } from './project-scan-reader.js';
|
|
86
|
+
/**
|
|
87
|
+
* Map ONE prerequisite of the `rd:qa-handoff` table onto the five-key
|
|
88
|
+
* vocabulary. The PATHS come from the table (single source); this function
|
|
89
|
+
* only names which key each required artifact answers to, so a change to a
|
|
90
|
+
* prerequisite's path moves the declared path with it.
|
|
91
|
+
*
|
|
92
|
+
* `mut/mut-report.json`, `rd/karpathy-review-<rid>.md`,
|
|
93
|
+
* `rd/third-party-review.md`, `qa/test-cases/<rid>.md` and the unit-test
|
|
94
|
+
* marker have no key — the vocabulary is five keys, and those artifacts are
|
|
95
|
+
* already enforced by the table itself, so they are deliberately not
|
|
96
|
+
* declared here rather than being forced into a key that does not mean them.
|
|
97
|
+
*/
|
|
98
|
+
function gateKeyOfPrerequisitePath(relativePath) {
|
|
99
|
+
if (relativePath.startsWith('prd/handoff'))
|
|
100
|
+
return 'prdHandoff';
|
|
101
|
+
if (relativePath.startsWith('rd/code-review'))
|
|
102
|
+
return 'codeReview';
|
|
103
|
+
if (relativePath.startsWith('audit/security') || relativePath.startsWith('rd/security-review')) {
|
|
104
|
+
return 'securityReview';
|
|
105
|
+
}
|
|
106
|
+
if (relativePath.startsWith('audit/perf'))
|
|
107
|
+
return 'perfBaseline';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The `gateEvidence` map for a slice of `requestType` — the evidence paths
|
|
112
|
+
* Gate C will check for it. Pure: no disk access, so a caller can compute it
|
|
113
|
+
* without side effects. Empty (`{}`) — never a partial map — when the type has
|
|
114
|
+
* no `rd:qa-handoff` row, because then there is no gate to declare anything to.
|
|
115
|
+
*/
|
|
116
|
+
export function deriveGateEvidence(opts) {
|
|
117
|
+
const requirements = getPrerequisitesFor('rd', 'qa-handoff', opts.requestType);
|
|
118
|
+
// docs / chore land here: `MINIMAL_TABLE` has no row at that transition, so
|
|
119
|
+
// nothing is required and nothing is declared. An empty map renders no block
|
|
120
|
+
// at all (`handoff-frontmatter.ts`), so such a capsule is byte-identical to a
|
|
121
|
+
// pre-B2 one and carries no statement that nothing verifies.
|
|
122
|
+
if (requirements.length === 0)
|
|
123
|
+
return {};
|
|
124
|
+
const sessionRoot = join('.peaks', '_runtime', opts.sessionId);
|
|
125
|
+
const evidence = {
|
|
126
|
+
projectScan: PROJECT_SCAN_RELATIVE
|
|
127
|
+
};
|
|
128
|
+
for (const prerequisite of requirements) {
|
|
129
|
+
const key = gateKeyOfPrerequisitePath(prerequisite.relativePath);
|
|
130
|
+
if (key === null)
|
|
131
|
+
continue;
|
|
132
|
+
// `<rid>` is the table's own placeholder, resolved the way the table's
|
|
133
|
+
// resolver resolves it, so the declaration names the file the gate looks
|
|
134
|
+
// for.
|
|
135
|
+
evidence[key] = join(sessionRoot, prerequisite.relativePath.replace('<rid>', opts.requestId));
|
|
136
|
+
}
|
|
137
|
+
return evidence;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Derive for a request by reading its PRD artifact — the only place the
|
|
141
|
+
* request TYPE is recorded (`- type: <t>`, written by `peaks request init`
|
|
142
|
+
* and read back by `request-artifact-service.extractMetadata`).
|
|
143
|
+
*
|
|
144
|
+
* Returns `undefined` when there is NO artifact to read — the one case the
|
|
145
|
+
* producers turn into "write no block" (see the header). That is now the ONLY
|
|
146
|
+
* `undefined`: a `showRequestArtifact` that THROWS is deliberately not caught,
|
|
147
|
+
* so a caller can tell "this slice has nothing to declare" apart from "the
|
|
148
|
+
* artifact could not be read".
|
|
149
|
+
*
|
|
150
|
+
* F4 (`rid-f4-ceiling-breach`). This used to be `catch { return undefined }`,
|
|
151
|
+
* which folded those two into one value — and the repository's own ratchet
|
|
152
|
+
* caught it (`capability-guard-runner/contracts/J03.ts`, rule
|
|
153
|
+
* `catch-return-null`; the ceiling was written for exactly this shape). Nothing
|
|
154
|
+
* is lost by letting the throw through. The refusals `showRequestArtifact` can
|
|
155
|
+
* raise are the rid/sid ones, and BOTH callers reach a byte-identical message
|
|
156
|
+
* on the next statement anyway — `assertSafeHandoffIds` (`handoff-service.ts`)
|
|
157
|
+
* for `prd handoff init`, `generateEvidence`'s own guard for the evidence
|
|
158
|
+
* generator — so the user-visible failure is unchanged; only its origin moves
|
|
159
|
+
* two lines earlier. What the catch DID cover in practice was an I/O failure
|
|
160
|
+
* while reading the artifact file, and swallowing that writes a capsule with no
|
|
161
|
+
* declaration at all, which Gate C reads as "nothing declared, nothing to
|
|
162
|
+
* check" (`field-absent` is not this check's business, below). A silent hole is
|
|
163
|
+
* worse than a loud, already-duplicated one.
|
|
164
|
+
*/
|
|
165
|
+
export async function deriveGateEvidenceForRequest(opts) {
|
|
166
|
+
// F4 (`rid-f4-ceiling-breach`). The throw from `showRequestArtifact` is
|
|
167
|
+
// propagated, so a caller can tell "this slice has nothing to declare"
|
|
168
|
+
// (artifact missing → `artifact === null` → `requestType === null` →
|
|
169
|
+
// `undefined`) apart from "the artifact could not be read" (an id the
|
|
170
|
+
// service refuses → rejected with `Invalid request id: ...`). The old
|
|
171
|
+
// `catch { return undefined }` collapsed both into the same value, and the
|
|
172
|
+
// `catch-return-null` ratchet caught it. The refusals `showRequestArtifact`
|
|
173
|
+
// raises are the rid / sid ones, and both callers reach a byte-identical
|
|
174
|
+
// guard a few lines up — `assertSafeHandoffIds` for `prd handoff init`,
|
|
175
|
+
// `generateEvidence`'s own guard for the evidence generator — so the
|
|
176
|
+
// user-visible failure is unchanged; only its origin moves earlier.
|
|
177
|
+
const artifact = await showRequestArtifact({
|
|
178
|
+
projectRoot: opts.projectRoot,
|
|
179
|
+
role: 'prd',
|
|
180
|
+
requestId: opts.requestId,
|
|
181
|
+
sessionId: opts.sessionId
|
|
182
|
+
});
|
|
183
|
+
const requestType = artifact?.requestType ?? null;
|
|
184
|
+
if (requestType === null)
|
|
185
|
+
return undefined;
|
|
186
|
+
return deriveGateEvidence({
|
|
187
|
+
sessionId: opts.sessionId,
|
|
188
|
+
requestId: opts.requestId,
|
|
189
|
+
requestType
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* GATE C — is the capsule's OWN declaration true?
|
|
194
|
+
*
|
|
195
|
+
* The division of labour, stated so the two checks cannot drift: the
|
|
196
|
+
* prerequisite table says which artifacts this type MUST produce (it owns the
|
|
197
|
+
* requirement and its file existence); this function says the capsule must
|
|
198
|
+
* not CLAIM evidence it does not have (it owns the declaration). Neither
|
|
199
|
+
* re-implements the other, and both read their paths from wherever the path
|
|
200
|
+
* really comes from.
|
|
201
|
+
*
|
|
202
|
+
* A missing declared path FAILS and names the key, because a declaration the
|
|
203
|
+
* gate cannot verify is exactly the "prose pretending to be data" state this
|
|
204
|
+
* field was created to end. Outcomes that are not failures, each for its own
|
|
205
|
+
* reason:
|
|
206
|
+
* - no capsule → not this check's business (`AUDIT_REQUIRES_HANDOFF` owns it)
|
|
207
|
+
* - `field-absent` → the capsule declares nothing; every pre-B1 capsule,
|
|
208
|
+
* and every slice whose producer could not derive a type
|
|
209
|
+
* - an unknown key → a WARNING: the five-key vocabulary is a typo trap, and
|
|
210
|
+
* the type's real requirement is still enforced by the table, so this
|
|
211
|
+
* informs without blocking
|
|
212
|
+
*/
|
|
213
|
+
export async function checkDeclaredGateEvidence(opts) {
|
|
214
|
+
const capsule = resolveHandoffPath({
|
|
215
|
+
projectRoot: opts.projectRoot,
|
|
216
|
+
sessionId: opts.sessionId,
|
|
217
|
+
requestId: opts.requestId
|
|
218
|
+
});
|
|
219
|
+
if (capsule === null)
|
|
220
|
+
return { ok: true, missing: [], warnings: [] };
|
|
221
|
+
const declared = await readHandoffGateEvidence(capsule);
|
|
222
|
+
if (declared.status === 'field-absent' || declared.status === 'file-missing') {
|
|
223
|
+
return { ok: true, missing: [], warnings: [] };
|
|
224
|
+
}
|
|
225
|
+
// A capsule with NO frontmatter block cannot declare anything, and
|
|
226
|
+
// `AUDIT_REQUIRES_HANDOFF` — the prereq that owns capsule validity — accepts
|
|
227
|
+
// that shape on a substring check (`schemaVersion: 2` + `sha256:` appearing
|
|
228
|
+
// anywhere). Failing it here would make Gate C refuse capsules the gate next
|
|
229
|
+
// to it accepts, breaking the "the checker and `request transition` agree"
|
|
230
|
+
// property that `pipeline-verify-contract-drift.test.ts` pins. So the line is
|
|
231
|
+
// drawn at the fence, not at readability: no fence ⇒ nothing declared;
|
|
232
|
+
// a fence whose YAML will not parse ⇒ a declaration that may be inside and
|
|
233
|
+
// unreadable, and THAT fails below.
|
|
234
|
+
if (declared.status === 'frontmatter-malformed' && declared.reason === 'no-frontmatter-fence') {
|
|
235
|
+
return { ok: true, missing: [], warnings: [] };
|
|
236
|
+
}
|
|
237
|
+
if (declared.status !== 'ok') {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
missing: [
|
|
241
|
+
{
|
|
242
|
+
path: `gateEvidence(${declared.status})`,
|
|
243
|
+
description: `the handoff's gateEvidence declaration is unusable (${declared.status}) — ` +
|
|
244
|
+
'a declaration that cannot be read is not the same as no declaration'
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
warnings: []
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const missing = [];
|
|
251
|
+
for (const key of GATE_EVIDENCE_KEYS) {
|
|
252
|
+
const declaredPath = declared.evidence[key];
|
|
253
|
+
if (declaredPath === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
if (await pathExists(join(opts.projectRoot, declaredPath)))
|
|
256
|
+
continue;
|
|
257
|
+
missing.push({
|
|
258
|
+
path: `gateEvidence.${key}`,
|
|
259
|
+
description: `gateEvidence.${key} declares "${declaredPath}", which does not exist`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const warnings = declared.unknownKeys.map((key) => ({
|
|
263
|
+
path: `gateEvidence.${key}`,
|
|
264
|
+
code: 'gate-evidence-unknown-key',
|
|
265
|
+
message: `gateEvidence declares "${key}", which is not one of the five keys ` +
|
|
266
|
+
`(${GATE_EVIDENCE_KEYS.join(', ')}) — a misspelled key is why a required gate ` +
|
|
267
|
+
'can read as undeclared'
|
|
268
|
+
}));
|
|
269
|
+
return { ok: missing.length === 0, missing, warnings };
|
|
270
|
+
}
|
|
@@ -21,6 +21,7 @@ import { dirname, join } from 'node:path';
|
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
22
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
23
23
|
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
24
|
+
import { deriveGateEvidence } from './gate-evidence-derivation.js';
|
|
24
25
|
import { handoffRelativePath, sha256OfBody } from './handoff-service.js';
|
|
25
26
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
26
27
|
/**
|
|
@@ -64,7 +65,18 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
64
65
|
goals: [],
|
|
65
66
|
acceptanceCriteria: [],
|
|
66
67
|
preservedBehavior: [],
|
|
67
|
-
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')
|
|
68
|
+
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, ''),
|
|
69
|
+
// B2: DERIVED from the request type of the artifact this producer just
|
|
70
|
+
// read. B1 gave this function an optional caller-supplied map instead;
|
|
71
|
+
// with a single-source derivation available that option was the wrong
|
|
72
|
+
// shape — it left the one production caller (`request-commands.ts`) able
|
|
73
|
+
// to pass nothing, which is exactly the F1 hole. There is no input to
|
|
74
|
+
// forget now.
|
|
75
|
+
gateEvidence: deriveGateEvidence({
|
|
76
|
+
sessionId: opts.sessionId,
|
|
77
|
+
requestId: opts.requestId,
|
|
78
|
+
requestType: artifact.requestType
|
|
79
|
+
})
|
|
68
80
|
};
|
|
69
81
|
const content = `${serializeHandoffFrontmatter(frontmatter)}${body}`;
|
|
70
82
|
mkdirSync(dirname(handoffPath), { recursive: true });
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
36
|
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
37
|
*/
|
|
38
|
-
import type
|
|
38
|
+
import { type HandoffFrontmatter } from './handoff-types.js';
|
|
39
39
|
/**
|
|
40
40
|
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
41
41
|
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
36
|
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
37
|
*/
|
|
38
|
+
import { GATE_EVIDENCE_KEYS, isGateEvidenceKey } from './handoff-types.js';
|
|
38
39
|
/** Render a string as a YAML double-quoted scalar. JSON string escapes are a
|
|
39
40
|
* subset of YAML 1.2's double-quoted escapes, so this is valid YAML and
|
|
40
41
|
* handles `\` (Windows paths), quotes, colons and newlines in one step. */
|
|
@@ -47,6 +48,61 @@ function blockSequence(key, values) {
|
|
|
47
48
|
return [`${key}: []`];
|
|
48
49
|
return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Render `gateEvidence` as a YAML map of quoted path scalars, or as NOTHING.
|
|
53
|
+
*
|
|
54
|
+
* Two shape decisions, both load-bearing:
|
|
55
|
+
*
|
|
56
|
+
* - **Omitted when absent or empty.** An absent map and an empty map both
|
|
57
|
+
* render zero lines, so a handoff that declares no evidence is
|
|
58
|
+
* byte-identical to a pre-B1 capsule. `gateEvidence: {}` would add a line
|
|
59
|
+
* to every handoff in the repo to say nothing — and the `each key exactly
|
|
60
|
+
* once` assertions in `handoff-auto-regen.test.ts` /
|
|
61
|
+
* `handoff-writer-gate-convergence.test.ts` pin the emitted key SET.
|
|
62
|
+
* - **Canonical key order** (`GATE_EVIDENCE_KEYS`), NOT insertion order of
|
|
63
|
+
* the caller's object. The frontmatter is sha256-adjacent and read by
|
|
64
|
+
* substring/regex consumers; a caller that built its map `{perfBaseline,
|
|
65
|
+
* projectScan}` must not produce different bytes from one that built it
|
|
66
|
+
* the other way round.
|
|
67
|
+
*
|
|
68
|
+
* Values go through `yamlScalar` because these are PATHS: on Windows they
|
|
69
|
+
* contain backslashes, which a plain YAML scalar would escape.
|
|
70
|
+
*/
|
|
71
|
+
function gateEvidenceBlock(evidence) {
|
|
72
|
+
if (evidence === undefined)
|
|
73
|
+
return [];
|
|
74
|
+
// F3 of `rid-b1-qa`: this function used to iterate only the five known keys,
|
|
75
|
+
// so anything else was dropped WITHOUT A TRACE — `initHandoff({gateEvidence:
|
|
76
|
+
// {projectScans: 'typo.md'}})` wrote no block at all and the capsule then
|
|
77
|
+
// read back as `field-absent`, i.e. as if nothing had ever been declared.
|
|
78
|
+
// The type system blocks literal typos, but not a value that arrived through
|
|
79
|
+
// `JSON.parse`, an `as` assertion, or a JS caller — and the derivation added
|
|
80
|
+
// in B2 is exactly such an adapter. So the check is runtime, and it is HERE
|
|
81
|
+
// because this serializer is the single funnel every producer passes
|
|
82
|
+
// through: one check covers all three writers and any future one.
|
|
83
|
+
const provided = Object.entries(evidence);
|
|
84
|
+
const unknownKeys = provided
|
|
85
|
+
.map(([key]) => key)
|
|
86
|
+
.filter((key) => !isGateEvidenceKey(key))
|
|
87
|
+
.sort();
|
|
88
|
+
if (unknownKeys.length > 0) {
|
|
89
|
+
throw new Error(`handoff: unknown gateEvidence key(s) [${unknownKeys.join(', ')}]; expected one of ` +
|
|
90
|
+
`${GATE_EVIDENCE_KEYS.join(', ')} — refusing to write a declaration that would be ` +
|
|
91
|
+
'dropped silently');
|
|
92
|
+
}
|
|
93
|
+
const nonStringKeys = provided
|
|
94
|
+
.filter(([, value]) => typeof value !== 'string')
|
|
95
|
+
.map(([key]) => key)
|
|
96
|
+
.sort();
|
|
97
|
+
if (nonStringKeys.length > 0) {
|
|
98
|
+
throw new Error(`handoff: gateEvidence value(s) for [${nonStringKeys.join(', ')}] must be strings (evidence paths)`);
|
|
99
|
+
}
|
|
100
|
+
const entries = GATE_EVIDENCE_KEYS.flatMap((key) => {
|
|
101
|
+
const value = evidence[key];
|
|
102
|
+
return value === undefined ? [] : [` ${key}: ${yamlScalar(value)}`];
|
|
103
|
+
});
|
|
104
|
+
return entries.length === 0 ? [] : ['gateEvidence:', ...entries];
|
|
105
|
+
}
|
|
50
106
|
/**
|
|
51
107
|
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
52
108
|
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
@@ -69,6 +125,12 @@ export function serializeHandoffFrontmatter(frontmatter) {
|
|
|
69
125
|
...blockSequence('acceptanceCriteria', frontmatter.acceptanceCriteria),
|
|
70
126
|
...blockSequence('preservedBehavior', frontmatter.preservedBehavior),
|
|
71
127
|
`handoffPath: ${yamlScalar(frontmatter.handoffPath)}`,
|
|
128
|
+
// LAST, after every anchored field: `schemaVersion` / `sha256` are
|
|
129
|
+
// matched as `^`-anchored lines by the gate and both audit loaders, so
|
|
130
|
+
// nothing new may be inserted before them. A nested map also renders
|
|
131
|
+
// indented lines only, leaving the top-level key set exactly as it was
|
|
132
|
+
// for every capsule that declares no evidence.
|
|
133
|
+
...gateEvidenceBlock(frontmatter.gateEvidence),
|
|
72
134
|
'---',
|
|
73
135
|
];
|
|
74
136
|
return `${lines.join('\n')}\n`;
|
|
@@ -1 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
import { type GateEvidence } from './handoff-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why a `gateEvidence` read produced what it produced.
|
|
4
|
+
*
|
|
5
|
+
* `ok` is the only success. `field-absent` is NOT a failure — a capsule with
|
|
6
|
+
* no declaration is a valid capsule (every pre-B1 handoff) — while
|
|
7
|
+
* `field-not-map` and `value-not-string` are declarations that exist and are
|
|
8
|
+
* broken, and `file-missing` / `read-error` / `frontmatter-malformed` mean
|
|
9
|
+
* nothing could be read at all. Collapsing any two of these is the defect
|
|
10
|
+
* this reader was rewritten to remove.
|
|
11
|
+
*/
|
|
12
|
+
export type HandoffGateEvidenceStatus = 'ok' | 'file-missing' | 'read-error' | 'frontmatter-malformed' | 'field-absent' | 'field-not-map' | 'value-not-string';
|
|
13
|
+
/**
|
|
14
|
+
* Discriminated result. `status` alone decides the branch, so a caller that
|
|
15
|
+
* only wants "is there a usable declaration?" checks `result.status === 'ok'`
|
|
16
|
+
* and reads `result.evidence` — and a caller that must refuse corrupt
|
|
17
|
+
* declarations gets `keys` / `actualType` / `reason` to name the problem in
|
|
18
|
+
* its failure message.
|
|
19
|
+
*/
|
|
20
|
+
export type HandoffGateEvidenceResult = {
|
|
21
|
+
readonly status: 'ok';
|
|
22
|
+
/** Only the five known keys. Absent key = not declared. An EMPTY map
|
|
23
|
+
* on disk yields an empty `evidence`, not an error: the field was
|
|
24
|
+
* written and declares nothing, which is a different fact from
|
|
25
|
+
* `field-absent` and is reported as such. */
|
|
26
|
+
readonly evidence: GateEvidence;
|
|
27
|
+
/** Keys present in the YAML that are not one of the five. Sorted.
|
|
28
|
+
* Reported rather than dropped: a typo'd key is why a required gate
|
|
29
|
+
* reads as "not declared", and this is the only place that can say so.
|
|
30
|
+
* The serializer cannot emit these, so they are hand-authored only. */
|
|
31
|
+
readonly unknownKeys: readonly string[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly status: 'file-missing';
|
|
34
|
+
} | {
|
|
35
|
+
readonly status: 'read-error';
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'frontmatter-malformed';
|
|
39
|
+
readonly reason: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly status: 'field-absent';
|
|
42
|
+
}
|
|
43
|
+
/** `actualType` is the YAML type found (`array`, `null`, `string`, …) —
|
|
44
|
+
* `array` is the pre-B1 shape, present in old test fixtures only. */
|
|
45
|
+
| {
|
|
46
|
+
readonly status: 'field-not-map';
|
|
47
|
+
readonly actualType: string;
|
|
48
|
+
}
|
|
49
|
+
/** At least one value was not a string; `keys` names every offender. The
|
|
50
|
+
* whole declaration is refused rather than filtered down to its good
|
|
51
|
+
* entries — dropping a malformed value is the collapse this replaced. */
|
|
52
|
+
| {
|
|
53
|
+
readonly status: 'value-not-string';
|
|
54
|
+
readonly keys: readonly string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* THE shape rule for a `gateEvidence` value — one home, two callers.
|
|
58
|
+
*
|
|
59
|
+
* F2 of `rid-b1-qa` found this rule written twice (`handoff-service.ts`'s
|
|
60
|
+
* `isGateEvidenceMap`, added by B1, and this module's own inline checks) with
|
|
61
|
+
* the SAME accept/reject boundary but DIFFERENT results: the same bytes
|
|
62
|
+
* produced a map WITH unknown keys through `readHandoff` and a map WITHOUT
|
|
63
|
+
* them through the reader, and `gateEvidence:` (null) threw on one path and
|
|
64
|
+
* reported a status on the other. That falsified the invariant
|
|
65
|
+
* `handoff-service.ts` stated about itself. Both paths now call this, and
|
|
66
|
+
* only the REFUSAL MECHANISM differs (a status vs a throw) — which is the
|
|
67
|
+
* pre-existing, documented policy split between the throwing `readHandoff`
|
|
68
|
+
* and the permissive reader, not a second opinion about the shape.
|
|
69
|
+
*
|
|
70
|
+
* `absent` is a first-class kind rather than an error: a capsule that
|
|
71
|
+
* declares nothing is valid, and (invariant 1 of B1) every pre-B1 handoff
|
|
72
|
+
* is one.
|
|
73
|
+
*/
|
|
74
|
+
export type GateEvidenceShape = {
|
|
75
|
+
readonly kind: 'absent';
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: 'not-map';
|
|
78
|
+
readonly actualType: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: 'value-not-string';
|
|
81
|
+
readonly keys: readonly string[];
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: 'ok';
|
|
84
|
+
readonly evidence: GateEvidence;
|
|
85
|
+
readonly unknownKeys: readonly string[];
|
|
86
|
+
};
|
|
87
|
+
/** Classify a `gateEvidence` VALUE (already located — see the reader for the
|
|
88
|
+
* fence/YAML/presence layers, which are file-level concerns, not shape). */
|
|
89
|
+
export declare function classifyGateEvidence(value: unknown): GateEvidenceShape;
|
|
90
|
+
/**
|
|
91
|
+
* Read + classify the `gateEvidence` map out of `filePath`. Never throws.
|
|
92
|
+
* See the header for the permissive contract and
|
|
93
|
+
* `HandoffGateEvidenceResult` for the outcomes.
|
|
94
|
+
*/
|
|
95
|
+
export declare function readHandoffGateEvidence(filePath: string): Promise<HandoffGateEvidenceResult>;
|