@cleocode/contracts 2026.5.111 → 2026.5.113
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/dist/__tests__/cli-meta-suggested-next.test.d.ts +21 -0
- package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts +16 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.js +394 -0
- package/dist/__tests__/evidence-atom-schema.test.js.map +1 -0
- package/dist/config/__tests__/manifest.test.d.ts +13 -0
- package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/config/__tests__/manifest.test.js +143 -0
- package/dist/config/__tests__/manifest.test.js.map +1 -0
- package/dist/config/manifest.d.ts +158 -0
- package/dist/config/manifest.d.ts.map +1 -0
- package/dist/config/manifest.js +123 -0
- package/dist/config/manifest.js.map +1 -0
- package/dist/docs/__tests__/provenance.test.d.ts +14 -0
- package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
- package/dist/docs/__tests__/provenance.test.js +579 -0
- package/dist/docs/__tests__/provenance.test.js.map +1 -0
- package/dist/docs/provenance.d.ts +731 -0
- package/dist/docs/provenance.d.ts.map +1 -0
- package/dist/docs/provenance.js +258 -0
- package/dist/docs/provenance.js.map +1 -0
- package/dist/docs-taxonomy.d.ts +24 -0
- package/dist/docs-taxonomy.d.ts.map +1 -1
- package/dist/docs-taxonomy.js +27 -0
- package/dist/docs-taxonomy.js.map +1 -1
- package/dist/doctor.d.ts +101 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +407 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -0
- package/dist/evidence-atom-schema.js +505 -0
- package/dist/evidence-atom-schema.js.map +1 -0
- package/dist/exit-codes.d.ts +37 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +37 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
- package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
- package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
- package/dist/invariants/__tests__/registry.test.d.ts +14 -0
- package/dist/invariants/__tests__/registry.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/registry.test.js +113 -0
- package/dist/invariants/__tests__/registry.test.js.map +1 -0
- package/dist/invariants/adr-056-release.d.ts +42 -0
- package/dist/invariants/adr-056-release.d.ts.map +1 -0
- package/dist/invariants/adr-056-release.js +143 -0
- package/dist/invariants/adr-056-release.js.map +1 -0
- package/dist/invariants/adr-070-orchestration.d.ts +73 -0
- package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
- package/dist/invariants/adr-070-orchestration.js +290 -0
- package/dist/invariants/adr-070-orchestration.js.map +1 -0
- package/dist/invariants/adr-073-saga.d.ts +32 -0
- package/dist/invariants/adr-073-saga.d.ts.map +1 -0
- package/dist/invariants/adr-073-saga.js +155 -0
- package/dist/invariants/adr-073-saga.js.map +1 -0
- package/dist/invariants/index.d.ts +162 -0
- package/dist/invariants/index.d.ts.map +1 -0
- package/dist/invariants/index.js +118 -0
- package/dist/invariants/index.js.map +1 -0
- package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
- package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/input-contract.test.js +158 -0
- package/dist/operations/__tests__/input-contract.test.js.map +1 -0
- package/dist/operations/docs.d.ts +16 -0
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/input-contract.d.ts +267 -0
- package/dist/operations/input-contract.d.ts.map +1 -0
- package/dist/operations/input-contract.js +29 -0
- package/dist/operations/input-contract.js.map +1 -0
- package/dist/templates/__tests__/manifest.test.d.ts +18 -0
- package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/templates/__tests__/manifest.test.js +123 -0
- package/dist/templates/__tests__/manifest.test.js.map +1 -0
- package/dist/templates/manifest.d.ts +186 -0
- package/dist/templates/manifest.d.ts.map +1 -0
- package/dist/templates/manifest.js +165 -0
- package/dist/templates/manifest.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
- package/src/__tests__/evidence-atom-schema.test.ts +499 -0
- package/src/config/__tests__/manifest.test.ts +176 -0
- package/src/config/manifest.ts +218 -0
- package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
- package/src/docs/__tests__/provenance.test.ts +648 -0
- package/src/docs/provenance.ts +601 -0
- package/src/docs-taxonomy.ts +55 -0
- package/src/doctor.ts +111 -0
- package/src/errors.ts +4 -0
- package/src/evidence-atom-schema.ts +628 -0
- package/src/exit-codes.ts +39 -0
- package/src/index.ts +129 -0
- package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
- package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
- package/src/invariants/__tests__/registry.test.ts +136 -0
- package/src/invariants/adr-056-release.ts +154 -0
- package/src/invariants/adr-070-orchestration.ts +317 -0
- package/src/invariants/adr-073-saga.ts +167 -0
- package/src/invariants/index.ts +222 -0
- package/src/operations/__tests__/input-contract.test.ts +188 -0
- package/src/operations/docs.ts +16 -0
- package/src/operations/input-contract.ts +301 -0
- package/src/templates/__tests__/manifest.test.ts +140 -0
- package/src/templates/manifest.ts +208 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema + gate-to-atom mapping for the ADR-051 evidence atom grammar.
|
|
3
|
+
*
|
|
4
|
+
* `cleo verify <task-id> --evidence "<atoms>"` accepts a semicolon-delimited
|
|
5
|
+
* list of evidence atoms. Each atom carries a kind prefix and a payload. The
|
|
6
|
+
* grammar lives in three places today:
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link EvidenceAtomSchema} — Zod discriminated union covering every
|
|
9
|
+
* parseable atom shape (this file).
|
|
10
|
+
* 2. {@link GATE_EVIDENCE_REQUIREMENTS} — gate → satisfying atom-combination
|
|
11
|
+
* map (this file).
|
|
12
|
+
* 3. `packages/core/src/tasks/evidence.ts` — runtime parser + filesystem /
|
|
13
|
+
* git / tool validators. The runtime parser delegates to
|
|
14
|
+
* {@link parseEvidenceString} below for the syntactic split.
|
|
15
|
+
*
|
|
16
|
+
* The schema is exposed for LLM agents and IDE tooling that want to validate
|
|
17
|
+
* an evidence string client-side BEFORE invoking `cleo verify`. The CLI
|
|
18
|
+
* runtime continues to perform the full filesystem-aware validation pass
|
|
19
|
+
* because Zod cannot check that a commit SHA is reachable or that a file
|
|
20
|
+
* exists on disk.
|
|
21
|
+
*
|
|
22
|
+
* The atom shapes here mirror the `ParsedAtom` union in
|
|
23
|
+
* `packages/core/src/tasks/evidence.ts` exactly. Behavior parity with the
|
|
24
|
+
* pre-T10337 ad-hoc parser is required — see the T10337 test suite for the
|
|
25
|
+
* full parity matrix.
|
|
26
|
+
*
|
|
27
|
+
* @task T10337
|
|
28
|
+
* @saga T10326
|
|
29
|
+
* @adr ADR-051
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { z } from 'zod';
|
|
33
|
+
|
|
34
|
+
import type { VerificationGate } from './task.js';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Per-atom Zod schemas
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `commit:<sha>` atom — references a git commit by full or short SHA.
|
|
42
|
+
*
|
|
43
|
+
* Format: `commit:<7-40 hex chars>`. The schema accepts any 7-40-character
|
|
44
|
+
* lowercase hex SHA; the runtime validator additionally checks reachability
|
|
45
|
+
* from the task branch and the AC-file content-intersect rule (T9245).
|
|
46
|
+
*
|
|
47
|
+
* @task T832
|
|
48
|
+
*/
|
|
49
|
+
export const commitAtomSchema = z.object({
|
|
50
|
+
kind: z.literal('commit'),
|
|
51
|
+
sha: z.string().regex(/^[0-9a-f]{7,40}$/i, 'commit sha must be 7-40 hex characters'),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `files:<p1,p2,...>` atom — list of file paths that the task touched.
|
|
56
|
+
*
|
|
57
|
+
* Format: `files:<comma-separated paths>`. The runtime validator additionally
|
|
58
|
+
* stats each path and records its sha256.
|
|
59
|
+
*
|
|
60
|
+
* @task T832
|
|
61
|
+
*/
|
|
62
|
+
export const filesAtomSchema = z.object({
|
|
63
|
+
kind: z.literal('files'),
|
|
64
|
+
paths: z.array(z.string().min(1)).min(1, 'files atom requires at least one path'),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `test-run:<path>` atom — path to a structured test runner JSON output
|
|
69
|
+
* (vitest, jest, pytest, cargo-nextest, etc.).
|
|
70
|
+
*
|
|
71
|
+
* Format: `test-run:<absolute or project-relative path>`. The runtime validator
|
|
72
|
+
* loads the file, parses the JSON, and rejects when failedTests > 0 or
|
|
73
|
+
* totalTests === 0.
|
|
74
|
+
*
|
|
75
|
+
* @task T832
|
|
76
|
+
*/
|
|
77
|
+
export const testRunAtomSchema = z.object({
|
|
78
|
+
kind: z.literal('test-run'),
|
|
79
|
+
path: z.string().min(1, 'test-run atom requires a non-empty path'),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `tool:<name>` atom — runs a project-resolved canonical tool and accepts
|
|
84
|
+
* exit-code 0.
|
|
85
|
+
*
|
|
86
|
+
* Format: `tool:<canonical name or legacy alias>`. Canonical names: `test`,
|
|
87
|
+
* `build`, `lint`, `typecheck`, `audit`, `security-scan`. Legacy aliases
|
|
88
|
+
* (`pnpm-test`, `tsc`, `biome`, `cargo-test`, `pytest`, …) still resolve via
|
|
89
|
+
* the runtime resolver (ADR-061 / T1534).
|
|
90
|
+
*
|
|
91
|
+
* @task T832
|
|
92
|
+
* @task T1534
|
|
93
|
+
*/
|
|
94
|
+
export const toolAtomSchema = z.object({
|
|
95
|
+
kind: z.literal('tool'),
|
|
96
|
+
tool: z.string().min(1, 'tool atom requires a non-empty tool name'),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `url:<href>` atom — soft evidence pointing to an external artifact (docs,
|
|
101
|
+
* dashboard, etc.).
|
|
102
|
+
*
|
|
103
|
+
* Format: `url:<http(s)://...>`. The runtime validator requires the
|
|
104
|
+
* `http://` or `https://` scheme.
|
|
105
|
+
*
|
|
106
|
+
* @task T832
|
|
107
|
+
*/
|
|
108
|
+
export const urlAtomSchema = z.object({
|
|
109
|
+
kind: z.literal('url'),
|
|
110
|
+
url: z
|
|
111
|
+
.string()
|
|
112
|
+
.min(1)
|
|
113
|
+
.regex(/^https?:\/\//, 'url atom must start with http:// or https://'),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `note:<text>` atom — free-form note used for soft evidence and waivers.
|
|
118
|
+
*
|
|
119
|
+
* Format: `note:<1-512 chars>`. The runtime validator caps the note at 512
|
|
120
|
+
* characters to keep the gate-evidence record bounded.
|
|
121
|
+
*
|
|
122
|
+
* @task T832
|
|
123
|
+
*/
|
|
124
|
+
export const noteAtomSchema = z.object({
|
|
125
|
+
kind: z.literal('note'),
|
|
126
|
+
note: z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1, 'note atom must be non-empty')
|
|
129
|
+
.max(512, 'note atom is too long (max 512 chars)'),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `decision:<id>` atom — references a `brain_decisions` row that IS the
|
|
134
|
+
* canonical artifact for a decision-only task. Satisfies the `implemented`
|
|
135
|
+
* gate when combined with `files:` pointing to a research note or with
|
|
136
|
+
* `note:` (T1875).
|
|
137
|
+
*
|
|
138
|
+
* Format: `decision:<id>` (e.g. `decision:D-arch-001`). The runtime validator
|
|
139
|
+
* looks up the row and requires `confirmation_state` ∈ {`accepted`,
|
|
140
|
+
* `proposed`}.
|
|
141
|
+
*
|
|
142
|
+
* @task T1875
|
|
143
|
+
*/
|
|
144
|
+
export const decisionAtomSchema = z.object({
|
|
145
|
+
kind: z.literal('decision'),
|
|
146
|
+
decisionId: z.string().min(1, 'decision atom requires a non-empty decision ID'),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `pr:<number>` atom — references a GitHub PR by number. Satisfies BOTH
|
|
151
|
+
* `testsPassed` and `qaPassed` simultaneously when the PR is MERGED and every
|
|
152
|
+
* required-workflow check is green (T9764). Extended in T9838 to satisfy
|
|
153
|
+
* `implemented` because the merge commit IS the landing artifact.
|
|
154
|
+
*
|
|
155
|
+
* Format: `pr:<positive integer>` (e.g. `pr:357`).
|
|
156
|
+
*
|
|
157
|
+
* @task T9764
|
|
158
|
+
* @task T9838
|
|
159
|
+
*/
|
|
160
|
+
export const prAtomSchema = z.object({
|
|
161
|
+
kind: z.literal('pr'),
|
|
162
|
+
prNumber: z.number().int().positive('pr atom requires a positive integer PR number'),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `loc-drop:<fromLines>:<toLines>` atom — proves that a migrated engine shed
|
|
167
|
+
* lines. Required for the `implemented` gate when the task carries the
|
|
168
|
+
* `engine-migration` label (T1604).
|
|
169
|
+
*
|
|
170
|
+
* Format: `loc-drop:<non-negative integer>:<non-negative integer>`.
|
|
171
|
+
* The schema admits any non-negative pair; the runtime validator additionally
|
|
172
|
+
* requires `fromLines > 0` and `toLines <= fromLines`.
|
|
173
|
+
*
|
|
174
|
+
* @task T1604
|
|
175
|
+
*/
|
|
176
|
+
export const locDropAtomSchema = z.object({
|
|
177
|
+
kind: z.literal('loc-drop'),
|
|
178
|
+
fromLines: z.number().int().nonnegative('loc-drop fromLines must be ≥ 0'),
|
|
179
|
+
toLines: z.number().int().nonnegative('loc-drop toLines must be ≥ 0'),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `callsite-coverage:<symbol>:<sourcePath>` atom — proves that an exported
|
|
184
|
+
* symbol has ≥1 production callsite outside its own source file, test files,
|
|
185
|
+
* and dist directories. Required for the `implemented` gate when the task
|
|
186
|
+
* carries the `callsite-coverage` label (T1605).
|
|
187
|
+
*
|
|
188
|
+
* @task T1605
|
|
189
|
+
*/
|
|
190
|
+
export const callsiteCoverageAtomSchema = z.object({
|
|
191
|
+
kind: z.literal('callsite-coverage'),
|
|
192
|
+
symbolName: z.string().min(1, 'callsite-coverage atom requires a non-empty symbolName'),
|
|
193
|
+
relativeSourcePath: z
|
|
194
|
+
.string()
|
|
195
|
+
.min(1, 'callsite-coverage atom requires a non-empty relativeSourcePath'),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Discriminated union
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Zod discriminated union covering every parseable evidence atom prefix.
|
|
204
|
+
*
|
|
205
|
+
* Use `EvidenceAtomSchema.safeParse(atom)` to validate a single atom client-
|
|
206
|
+
* side BEFORE invoking `cleo verify`. The runtime parser in
|
|
207
|
+
* `packages/core/src/tasks/evidence.ts` performs filesystem/git/tool checks
|
|
208
|
+
* on top of this syntactic schema.
|
|
209
|
+
*
|
|
210
|
+
* Atom kinds:
|
|
211
|
+
* - `commit` — git commit SHA (T832)
|
|
212
|
+
* - `files` — touched file paths (T832)
|
|
213
|
+
* - `test-run` — structured test runner JSON (T832)
|
|
214
|
+
* - `tool` — project-resolved tool exit code (T832 / T1534)
|
|
215
|
+
* - `url` — soft external pointer (T832)
|
|
216
|
+
* - `note` — free-form note (T832)
|
|
217
|
+
* - `decision` — brain_decisions row reference (T1875)
|
|
218
|
+
* - `pr` — merged-PR retroactive proof (T9764 / T9838)
|
|
219
|
+
* - `loc-drop` — engine-migration LOC reduction (T1604)
|
|
220
|
+
* - `callsite-coverage` — exported-symbol production callsite (T1605)
|
|
221
|
+
*
|
|
222
|
+
* @task T10337
|
|
223
|
+
* @adr ADR-051
|
|
224
|
+
*/
|
|
225
|
+
export const EvidenceAtomSchema = z.discriminatedUnion('kind', [
|
|
226
|
+
commitAtomSchema,
|
|
227
|
+
filesAtomSchema,
|
|
228
|
+
testRunAtomSchema,
|
|
229
|
+
toolAtomSchema,
|
|
230
|
+
urlAtomSchema,
|
|
231
|
+
noteAtomSchema,
|
|
232
|
+
decisionAtomSchema,
|
|
233
|
+
prAtomSchema,
|
|
234
|
+
locDropAtomSchema,
|
|
235
|
+
callsiteCoverageAtomSchema,
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Inferred TypeScript type for a parsed evidence atom.
|
|
240
|
+
*
|
|
241
|
+
* This is the pre-validation shape emitted by {@link parseEvidenceString}
|
|
242
|
+
* — distinct from the post-validation `EvidenceAtom` in `./task.ts` which
|
|
243
|
+
* carries fields populated by the runtime validator (sha256, exitCode, etc.).
|
|
244
|
+
*
|
|
245
|
+
* @task T10337
|
|
246
|
+
*/
|
|
247
|
+
export type EvidenceAtom = z.infer<typeof EvidenceAtomSchema>;
|
|
248
|
+
|
|
249
|
+
/** Discriminant union of every atom kind accepted by {@link EvidenceAtomSchema}. */
|
|
250
|
+
export type EvidenceAtomKind = EvidenceAtom['kind'];
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Gate-to-atom requirements map
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Per-gate evidence requirement spec.
|
|
258
|
+
*
|
|
259
|
+
* Each entry of `oneOf` is a satisfying atom-combination — the gate is
|
|
260
|
+
* satisfied IFF EVERY kind listed in at least ONE of the combinations is
|
|
261
|
+
* present among the supplied atoms.
|
|
262
|
+
*
|
|
263
|
+
* A single-element combination `['pr']` means "one `pr:` atom alone
|
|
264
|
+
* satisfies the gate". A two-element combination `['commit', 'files']` means
|
|
265
|
+
* "both a `commit:` atom AND a `files:` atom must be present".
|
|
266
|
+
*
|
|
267
|
+
* @task T10337
|
|
268
|
+
* @adr ADR-051 §2.3
|
|
269
|
+
*/
|
|
270
|
+
export interface GateEvidenceRequirement {
|
|
271
|
+
/** Alternative satisfying combinations — ANY one is sufficient. */
|
|
272
|
+
oneOf: ReadonlyArray<ReadonlyArray<EvidenceAtomKind>>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Mapping from each {@link VerificationGate} to its satisfying atom
|
|
277
|
+
* combinations. Mirrors the legacy `GATE_EVIDENCE_MINIMUMS` table in
|
|
278
|
+
* `packages/core/src/tasks/evidence.ts` exactly — behavior parity required
|
|
279
|
+
* by T10337.
|
|
280
|
+
*
|
|
281
|
+
* ## Why two alternatives for `implemented`?
|
|
282
|
+
*
|
|
283
|
+
* The `implemented` gate accepts six different evidence shapes:
|
|
284
|
+
*
|
|
285
|
+
* - `[commit, files]` — standard: commit SHA + touched file list.
|
|
286
|
+
* - `[commit, note]` — deletion-safe: commit SHA + descriptive note
|
|
287
|
+
* (when the implementation deleted files there
|
|
288
|
+
* are none left to anchor `files:`).
|
|
289
|
+
* - `[decision, files]` — decision-only task: brain_decisions row +
|
|
290
|
+
* research-note file (T1875).
|
|
291
|
+
* - `[decision, note]` — decision-only task without research note.
|
|
292
|
+
* - `[pr]` — merged-PR retroactive proof (T9838 extension
|
|
293
|
+
* of T9764).
|
|
294
|
+
*
|
|
295
|
+
* ## Why is `qaPassed` listed as `['tool']` not `['tool', 'tool']`?
|
|
296
|
+
*
|
|
297
|
+
* The pre-T10337 runtime enforced "at least one atom of kind `tool` is
|
|
298
|
+
* present" — it did NOT count distinct tool names. The convention `tool:lint`
|
|
299
|
+
* + `tool:typecheck` is documented in AGENTS.md as best-practice but the
|
|
300
|
+
* gate is satisfied by a single tool atom (e.g. `tool:test` when test
|
|
301
|
+
* already runs lint + typecheck pre-flight). Behavior parity requires that
|
|
302
|
+
* single-tool atoms continue to pass.
|
|
303
|
+
*
|
|
304
|
+
* @task T10337
|
|
305
|
+
* @adr ADR-051 §2.3
|
|
306
|
+
*/
|
|
307
|
+
export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
|
|
308
|
+
Record<VerificationGate, GateEvidenceRequirement>
|
|
309
|
+
> = Object.freeze({
|
|
310
|
+
implemented: {
|
|
311
|
+
oneOf: [
|
|
312
|
+
['commit', 'files'],
|
|
313
|
+
['commit', 'note'],
|
|
314
|
+
['decision', 'files'],
|
|
315
|
+
['decision', 'note'],
|
|
316
|
+
['pr'],
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
testsPassed: { oneOf: [['test-run'], ['tool'], ['pr']] },
|
|
320
|
+
qaPassed: { oneOf: [['tool'], ['pr']] },
|
|
321
|
+
documented: { oneOf: [['files'], ['url']] },
|
|
322
|
+
securityPassed: { oneOf: [['tool'], ['note']] },
|
|
323
|
+
cleanupDone: { oneOf: [['note']] },
|
|
324
|
+
nexusImpact: { oneOf: [['tool'], ['note']] },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Helpers
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Result of {@link validateEvidenceForGate}.
|
|
333
|
+
*
|
|
334
|
+
* Success carries no extra payload (the caller already holds the atom list).
|
|
335
|
+
* Failure carries a human-readable message in the legacy
|
|
336
|
+
* `Gate '<gate>' requires evidence: [<combo>] OR [<combo>]` format so error
|
|
337
|
+
* surfaces (E_EVIDENCE_INSUFFICIENT) read identically across the CLI and any
|
|
338
|
+
* client-side validator built on top of this schema.
|
|
339
|
+
*/
|
|
340
|
+
export type EvidenceValidationResult =
|
|
341
|
+
| { readonly ok: true }
|
|
342
|
+
| { readonly ok: false; readonly message: string };
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Format the satisfying-combinations spec for a gate as the legacy
|
|
346
|
+
* `[commit AND files] OR [pr]` string used in CLI error messages.
|
|
347
|
+
*
|
|
348
|
+
* Exposed so consumers can render the same hint text without re-computing it.
|
|
349
|
+
*
|
|
350
|
+
* @task T10337
|
|
351
|
+
*/
|
|
352
|
+
export function formatGateRequirement(gate: VerificationGate): string {
|
|
353
|
+
const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
|
|
354
|
+
return requirement.oneOf.map((set) => `[${set.join(' AND ')}]`).join(' OR ');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check whether a set of parsed atoms satisfies the requirement spec for
|
|
359
|
+
* `gate`.
|
|
360
|
+
*
|
|
361
|
+
* Returns `{ ok: true }` when ANY combination in `GATE_EVIDENCE_REQUIREMENTS[gate].oneOf`
|
|
362
|
+
* is fully present in `atoms` (matched by kind). Returns
|
|
363
|
+
* `{ ok: false, message }` otherwise — the message matches the legacy
|
|
364
|
+
* `checkGateEvidenceMinimum()` format byte-for-byte so error surfaces do not
|
|
365
|
+
* regress.
|
|
366
|
+
*
|
|
367
|
+
* Atoms must already be parsed (via {@link parseEvidenceString} or
|
|
368
|
+
* {@link EvidenceAtomSchema}); the helper does NOT re-validate the atom
|
|
369
|
+
* payload. Use {@link EvidenceAtomSchema.safeParse} first if the input came
|
|
370
|
+
* from an untrusted source.
|
|
371
|
+
*
|
|
372
|
+
* @param gate - Verification gate being checked.
|
|
373
|
+
* @param atoms - Already-parsed evidence atoms.
|
|
374
|
+
* @returns Pass / fail with a human-readable failure message.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* const atoms = parseEvidenceString('commit:abc1234;files:src/foo.ts');
|
|
379
|
+
* validateEvidenceForGate('implemented', atoms); // { ok: true }
|
|
380
|
+
* validateEvidenceForGate('testsPassed', atoms);
|
|
381
|
+
* // { ok: false, message: "Gate 'testsPassed' requires evidence: [test-run] OR [tool] OR [pr]" }
|
|
382
|
+
* ```
|
|
383
|
+
*
|
|
384
|
+
* @task T10337
|
|
385
|
+
* @adr ADR-051 §2.3
|
|
386
|
+
*/
|
|
387
|
+
export function validateEvidenceForGate(
|
|
388
|
+
gate: VerificationGate,
|
|
389
|
+
atoms: ReadonlyArray<{ kind: EvidenceAtomKind }>,
|
|
390
|
+
): EvidenceValidationResult {
|
|
391
|
+
const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
|
|
392
|
+
if (!requirement) {
|
|
393
|
+
// Unknown gate — defensive default: accept (preserves legacy "no minimum"
|
|
394
|
+
// behavior from checkGateEvidenceMinimum when the gate had no entry).
|
|
395
|
+
return { ok: true };
|
|
396
|
+
}
|
|
397
|
+
for (const combination of requirement.oneOf) {
|
|
398
|
+
const satisfied = combination.every((kind) => atoms.some((a) => a.kind === kind));
|
|
399
|
+
if (satisfied) return { ok: true };
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
message: `Gate '${gate}' requires evidence: ${formatGateRequirement(gate)}`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Error class thrown by {@link parseEvidenceString} when the input is
|
|
409
|
+
* malformed. Carries a {@link reason} suitable for the CLI `fix:` hint.
|
|
410
|
+
*
|
|
411
|
+
* Pure data — no dependency on `@cleocode/contracts/errors` (which would
|
|
412
|
+
* pull the CleoError import surface into every consumer that just wants
|
|
413
|
+
* syntactic validation).
|
|
414
|
+
*
|
|
415
|
+
* @task T10337
|
|
416
|
+
*/
|
|
417
|
+
export class EvidenceParseError extends Error {
|
|
418
|
+
/** Hint text suitable for an `fix:` message in the CLI surface. */
|
|
419
|
+
public readonly fix: string;
|
|
420
|
+
|
|
421
|
+
constructor(message: string, fix: string) {
|
|
422
|
+
super(message);
|
|
423
|
+
this.name = 'EvidenceParseError';
|
|
424
|
+
this.fix = fix;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Parse the raw CLI `--evidence` string into structured atoms.
|
|
430
|
+
*
|
|
431
|
+
* Syntax:
|
|
432
|
+
* ```
|
|
433
|
+
* evidence-list := atom (';' atom)*
|
|
434
|
+
* atom := kind ':' payload
|
|
435
|
+
* ```
|
|
436
|
+
*
|
|
437
|
+
* The payload format depends on `kind`:
|
|
438
|
+
*
|
|
439
|
+
* - `files` — comma-separated paths
|
|
440
|
+
* - `loc-drop` — `<fromLines>:<toLines>` (two integers)
|
|
441
|
+
* - `callsite-coverage` — `<symbolName>:<relativeSourcePath>`
|
|
442
|
+
* - `pr` — positive integer
|
|
443
|
+
* - everything else — opaque payload string until the next `;`
|
|
444
|
+
*
|
|
445
|
+
* A `state:MERGED` modifier may follow a `pr:<num>` atom and is consumed
|
|
446
|
+
* in-place (no separate atom is emitted) — see T9838. Any other usage of
|
|
447
|
+
* `state:` is rejected.
|
|
448
|
+
*
|
|
449
|
+
* Whitespace surrounding the `;` separators and around `kind`/`payload` is
|
|
450
|
+
* trimmed. Empty chunks are skipped (consecutive `;;` is tolerated).
|
|
451
|
+
*
|
|
452
|
+
* @param raw - Raw `--evidence` string.
|
|
453
|
+
* @returns Parsed atoms ready for {@link validateEvidenceForGate} or for
|
|
454
|
+
* the runtime validator in `packages/core/src/tasks/evidence.ts`.
|
|
455
|
+
* @throws {@link EvidenceParseError} when the input is malformed.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```ts
|
|
459
|
+
* parseEvidenceString('commit:abc1234;files:src/foo.ts,src/bar.ts;tool:lint');
|
|
460
|
+
* // [
|
|
461
|
+
* // { kind: 'commit', sha: 'abc1234' },
|
|
462
|
+
* // { kind: 'files', paths: ['src/foo.ts', 'src/bar.ts'] },
|
|
463
|
+
* // { kind: 'tool', tool: 'lint' },
|
|
464
|
+
* // ]
|
|
465
|
+
* ```
|
|
466
|
+
*
|
|
467
|
+
* @task T10337
|
|
468
|
+
* @adr ADR-051
|
|
469
|
+
*/
|
|
470
|
+
export function parseEvidenceString(raw: string): EvidenceAtom[] {
|
|
471
|
+
if (!raw || typeof raw !== 'string') {
|
|
472
|
+
throw new EvidenceParseError(
|
|
473
|
+
'Evidence string is empty',
|
|
474
|
+
"Pass evidence like '--evidence commit:<sha>;files:<path>;...'",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const chunks = raw
|
|
478
|
+
.split(';')
|
|
479
|
+
.map((s) => s.trim())
|
|
480
|
+
.filter(Boolean);
|
|
481
|
+
if (chunks.length === 0) {
|
|
482
|
+
throw new EvidenceParseError(
|
|
483
|
+
'Evidence string contained no atoms',
|
|
484
|
+
"Pass evidence like '--evidence commit:<sha>;files:<path>;...'",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const atoms: EvidenceAtom[] = [];
|
|
489
|
+
for (const chunk of chunks) {
|
|
490
|
+
const colon = chunk.indexOf(':');
|
|
491
|
+
if (colon < 1 || colon === chunk.length - 1) {
|
|
492
|
+
throw new EvidenceParseError(
|
|
493
|
+
`Malformed evidence atom: "${chunk}" (expected <kind>:<payload>)`,
|
|
494
|
+
'Each atom must be of form "<kind>:<payload>" separated by ";".',
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
const kind = chunk.slice(0, colon).trim();
|
|
498
|
+
const payload = chunk.slice(colon + 1).trim();
|
|
499
|
+
switch (kind) {
|
|
500
|
+
case 'commit':
|
|
501
|
+
atoms.push({ kind: 'commit', sha: payload });
|
|
502
|
+
break;
|
|
503
|
+
case 'files':
|
|
504
|
+
atoms.push({
|
|
505
|
+
kind: 'files',
|
|
506
|
+
paths: payload
|
|
507
|
+
.split(',')
|
|
508
|
+
.map((p) => p.trim())
|
|
509
|
+
.filter(Boolean),
|
|
510
|
+
});
|
|
511
|
+
break;
|
|
512
|
+
case 'test-run':
|
|
513
|
+
atoms.push({ kind: 'test-run', path: payload });
|
|
514
|
+
break;
|
|
515
|
+
case 'tool':
|
|
516
|
+
atoms.push({ kind: 'tool', tool: payload });
|
|
517
|
+
break;
|
|
518
|
+
case 'url':
|
|
519
|
+
atoms.push({ kind: 'url', url: payload });
|
|
520
|
+
break;
|
|
521
|
+
case 'note':
|
|
522
|
+
atoms.push({ kind: 'note', note: payload });
|
|
523
|
+
break;
|
|
524
|
+
case 'decision': {
|
|
525
|
+
if (!payload) {
|
|
526
|
+
throw new EvidenceParseError(
|
|
527
|
+
`decision atom requires a non-empty decision ID in "${chunk}"`,
|
|
528
|
+
'Use format: decision:<decisionId> e.g. decision:D-arch-001',
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
atoms.push({ kind: 'decision', decisionId: payload });
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'pr': {
|
|
535
|
+
const prNumber = Number(payload);
|
|
536
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
537
|
+
throw new EvidenceParseError(
|
|
538
|
+
`pr atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
|
|
539
|
+
'Use format: pr:<number> e.g. pr:357',
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
atoms.push({ kind: 'pr', prNumber });
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'loc-drop': {
|
|
546
|
+
const firstColon = payload.indexOf(':');
|
|
547
|
+
if (firstColon < 1 || firstColon === payload.length - 1) {
|
|
548
|
+
throw new EvidenceParseError(
|
|
549
|
+
`Malformed loc-drop atom: "${chunk}" (expected loc-drop:<fromLines>:<toLines>)`,
|
|
550
|
+
'Use format: loc-drop:<fromLines>:<toLines> e.g. loc-drop:1200:800',
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const fromRaw = payload.slice(0, firstColon).trim();
|
|
554
|
+
const toRaw = payload.slice(firstColon + 1).trim();
|
|
555
|
+
const fromLines = Number(fromRaw);
|
|
556
|
+
const toLines = Number(toRaw);
|
|
557
|
+
if (!Number.isInteger(fromLines) || fromLines < 0) {
|
|
558
|
+
throw new EvidenceParseError(
|
|
559
|
+
`loc-drop: fromLines must be a non-negative integer, got "${fromRaw}"`,
|
|
560
|
+
'Use format: loc-drop:<fromLines>:<toLines> e.g. loc-drop:1200:800',
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (!Number.isInteger(toLines) || toLines < 0) {
|
|
564
|
+
throw new EvidenceParseError(
|
|
565
|
+
`loc-drop: toLines must be a non-negative integer, got "${toRaw}"`,
|
|
566
|
+
'Use format: loc-drop:<fromLines>:<toLines> e.g. loc-drop:1200:800',
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
atoms.push({ kind: 'loc-drop', fromLines, toLines });
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case 'callsite-coverage': {
|
|
573
|
+
const colonIdx = payload.indexOf(':');
|
|
574
|
+
if (colonIdx < 1 || colonIdx === payload.length - 1) {
|
|
575
|
+
throw new EvidenceParseError(
|
|
576
|
+
`Malformed callsite-coverage atom: "${chunk}" (expected callsite-coverage:<symbolName>:<relativeSourcePath>)`,
|
|
577
|
+
'Use format: callsite-coverage:<symbolName>:<relativeSourcePath> e.g. callsite-coverage:myFn:packages/core/src/myFn.ts',
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const symbolName = payload.slice(0, colonIdx).trim();
|
|
581
|
+
const relativeSourcePath = payload.slice(colonIdx + 1).trim();
|
|
582
|
+
if (!symbolName) {
|
|
583
|
+
throw new EvidenceParseError(
|
|
584
|
+
`callsite-coverage: symbolName must not be empty in "${chunk}"`,
|
|
585
|
+
'Use format: callsite-coverage:<symbolName>:<relativeSourcePath>',
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (!relativeSourcePath) {
|
|
589
|
+
throw new EvidenceParseError(
|
|
590
|
+
`callsite-coverage: relativeSourcePath must not be empty in "${chunk}"`,
|
|
591
|
+
'Use format: callsite-coverage:<symbolName>:<relativeSourcePath>',
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
atoms.push({ kind: 'callsite-coverage', symbolName, relativeSourcePath });
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case 'state': {
|
|
598
|
+
// T9838: explicit-form modifier for the preceding pr: atom.
|
|
599
|
+
// Format: pr:<num>;state:MERGED. The resolver always requires
|
|
600
|
+
// state === 'MERGED' so the modifier is intent-documenting — it
|
|
601
|
+
// asserts the caller knows the contract.
|
|
602
|
+
if (payload !== 'MERGED') {
|
|
603
|
+
throw new EvidenceParseError(
|
|
604
|
+
`state atom only accepts "MERGED", got "${payload}" in "${chunk}"`,
|
|
605
|
+
'Use format: pr:<num>;state:MERGED (state is only meaningful paired with a pr: atom)',
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const lastAtom = atoms[atoms.length - 1];
|
|
609
|
+
if (!lastAtom || lastAtom.kind !== 'pr') {
|
|
610
|
+
throw new EvidenceParseError(
|
|
611
|
+
`state:MERGED must immediately follow a pr:<num> atom in the same evidence string ` +
|
|
612
|
+
`(got "${chunk}" with no preceding pr: atom)`,
|
|
613
|
+
'Use format: --evidence "pr:357;state:MERGED" (state modifier requires a pr: predecessor)',
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
// Modifier consumed — no new atom emitted.
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
default:
|
|
620
|
+
throw new EvidenceParseError(
|
|
621
|
+
`Unknown evidence kind: "${kind}" in atom "${chunk}"`,
|
|
622
|
+
'Valid kinds: commit, files, test-run, tool, url, note, loc-drop, callsite-coverage, decision, pr, state',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return atoms;
|
|
628
|
+
}
|
package/src/exit-codes.ts
CHANGED
|
@@ -323,6 +323,45 @@ export const E_WORKFLOW_NOT_FOUND = 'E_WORKFLOW_NOT_FOUND' as const;
|
|
|
323
323
|
*/
|
|
324
324
|
export const E_WRITE_CONTENTION = 'E_WRITE_CONTENTION' as const;
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* E_INVALID_SEVERITY_VALUE — `--severity` was passed a value that is not one
|
|
328
|
+
* of the canonical {@link TASK_SEVERITIES} members (`P0|P1|P2|P3`).
|
|
329
|
+
*
|
|
330
|
+
* Returned by the `cleo add` and `cleo update` CLI commands at the dispatch
|
|
331
|
+
* boundary BEFORE the request reaches the database. Replaces the previous
|
|
332
|
+
* late-stage failure mode where SQLite surfaced a cryptic
|
|
333
|
+
* `CHECK constraint failed: severity` error after the write attempt.
|
|
334
|
+
*
|
|
335
|
+
* Maps to {@link ExitCode.VALIDATION_ERROR} (6) at the CLI boundary.
|
|
336
|
+
*
|
|
337
|
+
* @task T10341
|
|
338
|
+
* @epic T10327
|
|
339
|
+
* @saga T10326
|
|
340
|
+
*/
|
|
341
|
+
export const E_INVALID_SEVERITY_VALUE = 'E_INVALID_SEVERITY_VALUE' as const;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* E_INVALID_PIPELINE_STAGE — `--pipeline-stage` was passed a value that
|
|
345
|
+
* either (a) is not a recognised stage name in the canonical RCASD-IVTR+C
|
|
346
|
+
* pipeline (`research|consensus|architecture_decision|specification|`
|
|
347
|
+
* `decomposition|implementation|validation|testing|release|contribution|`
|
|
348
|
+
* `cancelled`), or (b) would move the task BACKWARD relative to its
|
|
349
|
+
* current stage. Pipeline stages are forward-only.
|
|
350
|
+
*
|
|
351
|
+
* Returned by the `cleo update` CLI command at the dispatch boundary BEFORE
|
|
352
|
+
* the request reaches the database. Replaces the previous late-stage
|
|
353
|
+
* failure mode where SQLite surfaced a cryptic
|
|
354
|
+
* `CHECK constraint failed: pipeline_stage` error or the core engine threw
|
|
355
|
+
* an opaque {@link CleoError} after a partial round-trip.
|
|
356
|
+
*
|
|
357
|
+
* Maps to {@link ExitCode.VALIDATION_ERROR} (6) at the CLI boundary.
|
|
358
|
+
*
|
|
359
|
+
* @task T10341
|
|
360
|
+
* @epic T10327
|
|
361
|
+
* @saga T10326
|
|
362
|
+
*/
|
|
363
|
+
export const E_INVALID_PIPELINE_STAGE = 'E_INVALID_PIPELINE_STAGE' as const;
|
|
364
|
+
|
|
326
365
|
/** Check if an exit code represents an error (1-99). */
|
|
327
366
|
export function isErrorCode(code: ExitCode): boolean {
|
|
328
367
|
return code >= 1 && code < 100;
|