@planu/cli 5.0.0 → 5.1.1
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 -1
- package/dist/cli/commands/spec.js +10 -1
- package/dist/core/spec-validator.js +32 -18
- package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +59 -2
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
- package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
- package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
- package/dist/engine/execution/operation-journal.js +10 -4
- package/dist/engine/minimality/policy-loader.js +247 -6
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
- package/dist/engine/reverse-engineer/api-detector.js +2 -13
- package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
- package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
- package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
- package/dist/engine/spec-format/acceptance-criteria.js +13 -12
- package/dist/engine/spec-format/text-fences.js +20 -2
- package/dist/engine/spec-state-syncer.js +1 -1
- package/dist/engine/timing/budget.js +5 -1
- package/dist/server/routes/specs.js +7 -5
- package/dist/tools/challenge-spec-helpers.d.ts +10 -1
- package/dist/tools/challenge-spec-helpers.js +63 -22
- package/dist/tools/challenge-spec.js +18 -3
- package/dist/tools/check-readiness.js +37 -13
- package/dist/tools/create-spec/spec-builder.d.ts +7 -0
- package/dist/tools/create-spec/spec-builder.js +19 -4
- package/dist/tools/create-spec.js +156 -86
- package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
- package/dist/tools/sync-spec-state-handler.js +49 -1
- package/dist/tools/update-status/batch.d.ts +6 -2
- package/dist/tools/update-status/batch.js +58 -1
- package/dist/tools/update-status/dod-gates.d.ts +16 -1
- package/dist/tools/update-status/dod-gates.js +191 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
- package/dist/tools/update-status/done-receipt-verifier.js +37 -2
- package/dist/tools/update-status/evidence-gate.d.ts +4 -0
- package/dist/tools/update-status/evidence-gate.js +67 -2
- package/dist/tools/update-status/file-sync.d.ts +2 -2
- package/dist/tools/update-status/index.d.ts +23 -1
- package/dist/tools/update-status/index.js +201 -24
- package/dist/tools/update-status/transition-guard.js +13 -1
- package/dist/tools/workspace-dashboard-handler.js +38 -0
- package/dist/types/evidence-autofill.d.ts +34 -0
- package/dist/types/evidence-autofill.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +6 -0
- package/dist/types/spec/inputs.d.ts +7 -2
- package/dist/types/spec-format.d.ts +1 -1
- package/dist/types/transition-log.d.ts +1 -1
- package/dist/types/validation.d.ts +8 -2
- package/package.json +11 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
## [5.1.1] - 2026-08-04
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(release-harness): resolve published artifact from staged repack when publish has no tarball argv
|
|
5
|
+
- fix(release): stop CLI-guard stdout pollution, npm view array probe, and tarball-path publish leak
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [5.1.0] - 2026-08-04
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- feat(lifecycle): automate done evidence pipeline end to end
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(update-status): make typed failure explicit at dod-gates catch sites
|
|
15
|
+
- fix(deps): patch hono, fast-uri, ip-address, and undici advisories via overrides
|
|
16
|
+
- fix(evidence-gates): derive contract example kinds from filenames
|
|
17
|
+
- fix(reverse-engineer): share walk-ignore list and add fast release-gate test lane
|
|
18
|
+
- fix(challenge): suppress ungrounded concurrency boilerplate in challenge_spec
|
|
19
|
+
- fix(update-status): report per-spec failure reasons in batch results
|
|
20
|
+
- fix(create-spec): guarantee spec.md write before reporting persisted
|
|
21
|
+
- fix(create-spec): release idempotency claim when create_spec fails before commit
|
|
22
|
+
- fix(release): accept npm 12 pack metadata
|
|
23
|
+
- fix(deps): patch transitive security advisories
|
|
24
|
+
- fix(privacy): redact minimality policy locators
|
|
25
|
+
- fix(lifecycle): route review evidence by target
|
|
26
|
+
- fix(readiness): unify canonical validation evidence
|
|
27
|
+
|
|
28
|
+
### Chores
|
|
29
|
+
- chore(pnpm): disable modules purge confirmation for non-TTY automation
|
|
30
|
+
- chore(planu): session checkpoint before release
|
|
31
|
+
- chore(planu): close SPEC-1350 lifecycle state
|
|
32
|
+
- chore(planu): hand off freshness blocker
|
|
33
|
+
- chore(planu): checkpoint release handoff
|
|
34
|
+
- chore(planu): preserve native engine review state
|
|
35
|
+
- chore(planu): persist delayed challenge evidence
|
|
36
|
+
- chore(planu): recover delayed audit specs
|
|
37
|
+
- chore(planu): capture lifecycle dogfood failures
|
|
38
|
+
- chore(planu): persist release remediation handoffs
|
|
39
|
+
- chore(planu): approve final release blockers
|
|
40
|
+
- chore(planu): start dependency security remediation
|
|
41
|
+
- chore(planu): approve final security remediation
|
|
42
|
+
- chore(planu): checkpoint final release remediations
|
|
43
|
+
- chore(planu): start lifecycle routing implementation
|
|
44
|
+
- chore(planu): approve lifecycle routing remediation
|
|
45
|
+
- chore(planu): checkpoint release remediation specs
|
|
46
|
+
- chore(planu): track v5 release dogfood regressions
|
|
47
|
+
|
|
48
|
+
|
|
1
49
|
## [5.0.0] - 2026-07-31
|
|
2
50
|
|
|
3
51
|
### Breaking Changes
|
|
@@ -4508,4 +4556,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
4508
4556
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
4509
4557
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
4510
4558
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
4511
|
-
- 10,857 tests with ≥95% coverage
|
|
4559
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -30,13 +30,14 @@ function printSpecSubcommandHelp() {
|
|
|
30
30
|
` ${'list'.padEnd(10)} ${dim('List specs [--status draft|approved|done...]')}`,
|
|
31
31
|
` ${'show'.padEnd(10)} ${dim('Show spec details (SPEC-NNN)')}`,
|
|
32
32
|
` ${'status'.padEnd(10)} ${dim('Update spec status (SPEC-NNN <status>)')}`,
|
|
33
|
+
` ${''.padEnd(10)} ${dim('Batch targets: draft|review|approved|implementing|discarded')}`,
|
|
33
34
|
` ${'validate'.padEnd(10)} ${dim('Validate a spec against its codebase')}`,
|
|
34
35
|
'',
|
|
35
36
|
cyan('Examples:'),
|
|
36
37
|
` planu spec create "Add login flow"`,
|
|
37
38
|
` planu spec list --status approved`,
|
|
38
39
|
` planu spec show SPEC-001`,
|
|
39
|
-
` planu spec status SPEC-001 done
|
|
40
|
+
` planu spec status SPEC-001 done --implementation-review-digest sha256:<64-hex>`,
|
|
40
41
|
` planu spec validate SPEC-001`,
|
|
41
42
|
];
|
|
42
43
|
process.stdout.write(lines.join('\n') + '\n');
|
|
@@ -207,6 +208,7 @@ async function runShow(args, flags) {
|
|
|
207
208
|
// ---------------------------------------------------------------------------
|
|
208
209
|
// Subcommand: spec status
|
|
209
210
|
// ---------------------------------------------------------------------------
|
|
211
|
+
// eslint-disable-next-line complexity -- batch done requires an explicit pre-resolution fail-closed branch
|
|
210
212
|
async function runStatus(args, flags) {
|
|
211
213
|
const { positionals, values } = parseArgs({
|
|
212
214
|
args,
|
|
@@ -215,6 +217,7 @@ async function runStatus(args, flags) {
|
|
|
215
217
|
notes: { type: 'string', short: 'n' },
|
|
216
218
|
batch: { type: 'boolean' },
|
|
217
219
|
set: { type: 'string', short: 's' },
|
|
220
|
+
'implementation-review-digest': { type: 'string' },
|
|
218
221
|
},
|
|
219
222
|
strict: false,
|
|
220
223
|
allowPositionals: true,
|
|
@@ -232,6 +235,11 @@ async function runStatus(args, flags) {
|
|
|
232
235
|
process.exitCode = 1;
|
|
233
236
|
return;
|
|
234
237
|
}
|
|
238
|
+
if (batch && status === 'done') {
|
|
239
|
+
process.stderr.write(`${red('BATCH_DONE_UNSUPPORTED:')} Close specs individually with update_status(done) and full implementation review evidence.\n`);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
235
243
|
const projectId = values['project-id'] ?? detectProjectId();
|
|
236
244
|
if (batch) {
|
|
237
245
|
const result = await handleUpdateStatusBatch({
|
|
@@ -253,6 +261,7 @@ async function runStatus(args, flags) {
|
|
|
253
261
|
projectId,
|
|
254
262
|
status: status,
|
|
255
263
|
reviewNotes: values.notes ?? undefined,
|
|
264
|
+
implementationReviewDigest: values['implementation-review-digest'] ?? undefined,
|
|
256
265
|
});
|
|
257
266
|
if (result.isError) {
|
|
258
267
|
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
@@ -6,6 +6,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
6
6
|
import { checkSpecReadiness } from '../engine/readiness-checker.js';
|
|
7
7
|
import { scoreSpecQuality } from '../engine/spec-quality-scorer.js';
|
|
8
8
|
import { parseFrontmatter, stripFrontmatter } from '../engine/frontmatter-parser.js';
|
|
9
|
+
import { extractNormalizedAcceptanceCriteria } from '../engine/spec-format/acceptance-criteria.js';
|
|
10
|
+
import { parseFrontmatterScenarios } from '../engine/validator/spec-compliance-runner.js';
|
|
9
11
|
// ── Required frontmatter fields ──────────────────────────────────────────────
|
|
10
12
|
const REQUIRED_FRONTMATTER_FIELDS = ['id', 'title', 'status', 'type', 'target', 'scope'];
|
|
11
13
|
// ── Required body sections (SPEC-785 aligned with lean-spec-generator output) ─
|
|
@@ -133,6 +135,20 @@ function mapQualityToValidation(dimensions) {
|
|
|
133
135
|
}
|
|
134
136
|
return warnings;
|
|
135
137
|
}
|
|
138
|
+
function countCanonicalBddScenarios(raw) {
|
|
139
|
+
const criteria = extractNormalizedAcceptanceCriteria(raw, { allowLegacyBodyFallback: false });
|
|
140
|
+
const bodyCriteria = criteria.filter((criterion) => criterion.source === 'body');
|
|
141
|
+
const bddCandidates = bodyCriteria.length > 0
|
|
142
|
+
? bodyCriteria.map((criterion) => criterion.text)
|
|
143
|
+
: parseFrontmatterScenarios(raw).map((scenario) => scenario.steps?.map((step) => `${step.keyword} ${step.text}`).join(' ') ??
|
|
144
|
+
scenario.title);
|
|
145
|
+
return {
|
|
146
|
+
criteriaCount: criteria.length,
|
|
147
|
+
bddScenarioCount: bddCandidates.filter((criterion) => /\b(?:given|dado)\b/i.test(criterion) &&
|
|
148
|
+
/\b(?:when|cuando)\b/i.test(criterion) &&
|
|
149
|
+
/\b(?:then|entonces)\b/i.test(criterion)).length,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
136
152
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
137
153
|
/**
|
|
138
154
|
* Validate a spec's format against the unified spec.md schema (SPEC-630).
|
|
@@ -168,22 +184,16 @@ export async function validateSpecFormat(spec, opts) {
|
|
|
168
184
|
allWarnings.push(...unifiedResult.warnings);
|
|
169
185
|
// ── 4. Readiness check ───────────────────────────────────────────────────
|
|
170
186
|
let readinessScore = 0;
|
|
171
|
-
let
|
|
172
|
-
|
|
187
|
+
let readinessReport;
|
|
188
|
+
// The canonical metrics are independent from the readiness checker. Keep
|
|
189
|
+
// them truthful even when that best-effort checker throws.
|
|
190
|
+
const { criteriaCount, bddScenarioCount } = countCanonicalBddScenarios(raw);
|
|
173
191
|
try {
|
|
174
192
|
// Cast to Spec since checkSpecReadiness needs the full type but only reads
|
|
175
193
|
// the fields available on the Pick type we receive.
|
|
176
|
-
|
|
177
|
-
readinessScore =
|
|
178
|
-
|
|
179
|
-
bddScenarioCount = (raw.match(/^\s+-\s+title:/gm) ?? []).length;
|
|
180
|
-
// Count criteria lines (checkbox or GIVEN/WHEN/THEN)
|
|
181
|
-
const bodyLines = body.split('\n');
|
|
182
|
-
criteriaCount = bodyLines.filter((l) => /^- \[[ x]\]/i.test(l.trim()) || /^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(l.trim())).length;
|
|
183
|
-
if (criteriaCount === 0) {
|
|
184
|
-
criteriaCount = bddScenarioCount;
|
|
185
|
-
}
|
|
186
|
-
const mapped = mapReadinessToValidation(report.issues.blockers, report.issues.warnings);
|
|
194
|
+
readinessReport = await checkSpecReadiness(spec, readinessMode);
|
|
195
|
+
readinessScore = readinessReport.score;
|
|
196
|
+
const mapped = mapReadinessToValidation(readinessReport.issues.blockers, readinessReport.issues.warnings);
|
|
187
197
|
allErrors.push(...mapped.errors);
|
|
188
198
|
allWarnings.push(...mapped.warnings);
|
|
189
199
|
}
|
|
@@ -197,11 +207,12 @@ export async function validateSpecFormat(spec, opts) {
|
|
|
197
207
|
return { valid, errors: allErrors, warnings: allWarnings };
|
|
198
208
|
}
|
|
199
209
|
// ── 5. Quality scoring ───────────────────────────────────────────────────
|
|
200
|
-
let qualityScore
|
|
201
|
-
let qualityGrade
|
|
210
|
+
let qualityScore;
|
|
211
|
+
let qualityGrade;
|
|
212
|
+
let qualityReport;
|
|
202
213
|
if (!skipQuality) {
|
|
203
214
|
try {
|
|
204
|
-
|
|
215
|
+
qualityReport = await scoreSpecQuality(spec);
|
|
205
216
|
qualityScore = qualityReport.score.total;
|
|
206
217
|
qualityGrade = qualityReport.score.grade;
|
|
207
218
|
const qualityWarnings = mapQualityToValidation(qualityReport.dimensions);
|
|
@@ -226,11 +237,14 @@ export async function validateSpecFormat(spec, opts) {
|
|
|
226
237
|
warnings: allWarnings,
|
|
227
238
|
metrics: {
|
|
228
239
|
readinessScore,
|
|
229
|
-
qualityScore,
|
|
230
|
-
qualityGrade,
|
|
231
240
|
bddScenarioCount,
|
|
232
241
|
criteriaCount,
|
|
242
|
+
...(qualityScore === undefined || qualityGrade === undefined
|
|
243
|
+
? {}
|
|
244
|
+
: { qualityScore, qualityGrade }),
|
|
233
245
|
},
|
|
246
|
+
readinessReport,
|
|
247
|
+
qualityReport,
|
|
234
248
|
};
|
|
235
249
|
}
|
|
236
250
|
//# sourceMappingURL=spec-validator.js.map
|
|
@@ -15,6 +15,8 @@ export declare function traceabilityRowHasCurrentCommandEvidence(row: Traceabili
|
|
|
15
15
|
projectId: string;
|
|
16
16
|
projectPath: string;
|
|
17
17
|
}): boolean;
|
|
18
|
+
/** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
|
|
19
|
+
export declare function handoffEvidencePath(projectId: string, specId: string, filename: string): string;
|
|
18
20
|
export declare function readEvidenceArtifacts(args: {
|
|
19
21
|
spec: Spec;
|
|
20
22
|
projectId: string;
|
|
@@ -244,7 +244,58 @@ const ARTIFACT_HINTS = {
|
|
|
244
244
|
function artifactHint(label) {
|
|
245
245
|
return ARTIFACT_HINTS[label] ?? '';
|
|
246
246
|
}
|
|
247
|
-
|
|
247
|
+
/**
|
|
248
|
+
* SPEC-1356 / AC5: compact JSON literal per artifact kind, appended verbatim to
|
|
249
|
+
* schema-validation errors so a retrying caller sees the exact expected shape
|
|
250
|
+
* instead of re-deriving it from prose.
|
|
251
|
+
*/
|
|
252
|
+
const ARTIFACT_EXAMPLES = {
|
|
253
|
+
'Discovery evidence': JSON.stringify({
|
|
254
|
+
version: 1,
|
|
255
|
+
rules: ['Rule text'],
|
|
256
|
+
examples: [{ rule: 'Rule text', example: 'Example text' }],
|
|
257
|
+
openQuestions: [{ question: 'Open question?', status: 'open' }],
|
|
258
|
+
outOfScope: ['Not in scope item'],
|
|
259
|
+
glossary: [{ term: 'Term', meaning: 'Meaning' }],
|
|
260
|
+
}),
|
|
261
|
+
'Task plan evidence': JSON.stringify({
|
|
262
|
+
version: 1,
|
|
263
|
+
tasks: [{ id: 'T1', title: 'Task title', acceptanceCriteria: ['AC1'], status: 'pending' }],
|
|
264
|
+
}),
|
|
265
|
+
'Traceability matrix evidence': JSON.stringify({
|
|
266
|
+
version: 1,
|
|
267
|
+
rows: [
|
|
268
|
+
{
|
|
269
|
+
acceptanceCriterion: 'AC text',
|
|
270
|
+
testEvidence: ['tests/example.test.ts'],
|
|
271
|
+
changedFiles: ['src/example.ts'],
|
|
272
|
+
validationEvidence: 'validate passed with score 100',
|
|
273
|
+
reviewerEvidence: 'planu-implementation-reviewer approved',
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
}),
|
|
277
|
+
// Contract kinds are derived from the artifact filenames so no
|
|
278
|
+
// configurable technology literal is hardcoded here (audit:hardcodes).
|
|
279
|
+
...Object.fromEntries([
|
|
280
|
+
'contract-validation-api.json',
|
|
281
|
+
'contract-validation-graphql.json',
|
|
282
|
+
'contract-validation-event.json',
|
|
283
|
+
'contract-validation-ui.json',
|
|
284
|
+
'contract-validation-mcp.json',
|
|
285
|
+
].map((label) => [
|
|
286
|
+
label,
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
version: 1,
|
|
289
|
+
kind: label.slice('contract-validation-'.length, -'.json'.length),
|
|
290
|
+
passed: true,
|
|
291
|
+
}),
|
|
292
|
+
])),
|
|
293
|
+
};
|
|
294
|
+
function artifactExample(label) {
|
|
295
|
+
return ARTIFACT_EXAMPLES[label] ?? '';
|
|
296
|
+
}
|
|
297
|
+
/** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
|
|
298
|
+
export function handoffEvidencePath(projectId, specId, filename) {
|
|
248
299
|
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
249
300
|
}
|
|
250
301
|
async function readUnknown(paths) {
|
|
@@ -273,7 +324,13 @@ async function readOptional(args) {
|
|
|
273
324
|
}
|
|
274
325
|
const parsed = args.schema.safeParse(found.value);
|
|
275
326
|
if (!parsed.success) {
|
|
276
|
-
|
|
327
|
+
// SPEC-1356 / AC5: surface every Zod issue (field path + message), not just the
|
|
328
|
+
// first one, so a single retry can fix every constraint violation at once.
|
|
329
|
+
const allIssues = parsed.error.issues
|
|
330
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
|
|
331
|
+
.join('; ');
|
|
332
|
+
const example = artifactExample(args.label);
|
|
333
|
+
args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${allIssues} ${artifactHint(args.label)}${example ? ` Expected shape example: ${example}` : ''}`.trim());
|
|
277
334
|
return undefined;
|
|
278
335
|
}
|
|
279
336
|
return parsed.data;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AutofillTraceabilityMatrixArgs, AutofillTraceabilityMatrixResult } from '../../types/evidence-autofill.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate `traceability-matrix.json` in the handoff evidence store when it does
|
|
4
|
+
* not already exist. Never overwrites an existing file — if one is present this
|
|
5
|
+
* is a no-op that reports `already-exists`. Tolerates every failure by returning
|
|
6
|
+
* a typed non-throwing result; callers must never let this crash a lifecycle
|
|
7
|
+
* transition.
|
|
8
|
+
*/
|
|
9
|
+
export declare function autofillTraceabilityMatrix(args: AutofillTraceabilityMatrixArgs): Promise<AutofillTraceabilityMatrixResult>;
|
|
10
|
+
//# sourceMappingURL=evidence-autofill.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// engine/evidence-gates/evidence-autofill.ts — SPEC-1356: automated lifecycle evidence pipeline
|
|
2
|
+
//
|
|
3
|
+
// Generates traceability-matrix.json in the handoff evidence store when it is
|
|
4
|
+
// missing, so a `done` transition never fails purely because nobody hand-wrote
|
|
5
|
+
// the traceability artifact.
|
|
6
|
+
//
|
|
7
|
+
// ORDERING CAVEAT (documented per SPEC-1356 T-A3): writing traceability-matrix.json
|
|
8
|
+
// *after* `validate` has already computed and bound durable validation receipts
|
|
9
|
+
// would make those bindings stale relative to the newly-written evidence — the
|
|
10
|
+
// receipt would be certifying a repository state that does not include this file.
|
|
11
|
+
// This module does NOT attempt to re-run or re-bind validation. Instead, callers
|
|
12
|
+
// (see `src/tools/update-status/index.ts`) MUST treat a write from this module as
|
|
13
|
+
// "evidence created, not yet bound": when autofill actually writes the file, the
|
|
14
|
+
// caller must block the `done` transition with a message telling the operator to
|
|
15
|
+
// re-run `validate` once so the freshly-created evidence gets bound, then retry
|
|
16
|
+
// `done`. This is the simpler, safe choice given SPEC-1356 could not fully verify
|
|
17
|
+
// that skipping straight through to the done gates after autofill would keep
|
|
18
|
+
// receipt bindings correct in every code path (reconciliation, forced bypass,
|
|
19
|
+
// legacy harnesses, etc.).
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
import { promisify } from 'node:util';
|
|
24
|
+
import { extractAcceptanceCriteriaTexts } from '../spec-format/acceptance-criteria.js';
|
|
25
|
+
import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
|
|
26
|
+
import { extractCanonicalFileOwnership } from '../handoff-packager.js';
|
|
27
|
+
import { handoffEvidencePath } from './artifact-reader.js';
|
|
28
|
+
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
29
|
+
import { ValidationReportV1Schema } from '../handoff-artifacts/schemas.js';
|
|
30
|
+
import { projectDataDir } from '../../storage/base-store.js';
|
|
31
|
+
import { readFile } from 'node:fs/promises';
|
|
32
|
+
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
33
|
+
const execFileAsync = promisify(execFile);
|
|
34
|
+
const defaultExec = async (command, args, options) => {
|
|
35
|
+
const { stdout } = await execFileAsync(command, args, { cwd: options.cwd, encoding: 'utf-8' });
|
|
36
|
+
return stdout;
|
|
37
|
+
};
|
|
38
|
+
/** Best-effort `git diff --name-only <mergeBase>...HEAD`; never throws. */
|
|
39
|
+
async function computeChangedFilesFromGit(projectPath, exec, baseBranch) {
|
|
40
|
+
try {
|
|
41
|
+
const mergeBaseOut = await exec('git', ['merge-base', 'HEAD', baseBranch], {
|
|
42
|
+
cwd: projectPath,
|
|
43
|
+
});
|
|
44
|
+
const mergeBase = mergeBaseOut.trim();
|
|
45
|
+
if (!mergeBase) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const diffOut = await exec('git', ['diff', '--name-only', `${mergeBase}...HEAD`], {
|
|
49
|
+
cwd: projectPath,
|
|
50
|
+
});
|
|
51
|
+
return diffOut
|
|
52
|
+
.split('\n')
|
|
53
|
+
.map((line) => line.trim())
|
|
54
|
+
.filter((line) => line.length > 0);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
/* reliability-optional: EVIDENCE_AUTOFILL_GIT_DIFF — falls back to ## Files ownership */
|
|
58
|
+
reportClassifiedDegradation('EVIDENCE_AUTOFILL_GIT_DIFF', error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function filterExistingRepoFiles(projectPath, paths) {
|
|
63
|
+
return paths.filter((path) => {
|
|
64
|
+
if (isAbsolute(path)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return existsSync(resolve(projectPath, path));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Best-effort read + sha256 summary of the persisted validation report, for prose evidence. */
|
|
76
|
+
async function readValidationReportSummary(projectId, specId) {
|
|
77
|
+
const reportPath = join(projectDataDir(projectId), 'handoffs', specId, 'validation-report.json');
|
|
78
|
+
try {
|
|
79
|
+
const raw = await readFile(reportPath, 'utf-8');
|
|
80
|
+
const parsed = ValidationReportV1Schema.safeParse(JSON.parse(raw));
|
|
81
|
+
if (!parsed.success) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
const { score, reviewer } = parsed.data;
|
|
85
|
+
return {
|
|
86
|
+
validationEvidence: `Validated by handoffs/${specId}/validation-report.json (score=${score ?? 'n/a'}).`,
|
|
87
|
+
reviewerEvidence: `Reviewed by ${reviewer.agent} (${reviewer.kind}), verdict=${reviewer.verdict}.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate `traceability-matrix.json` in the handoff evidence store when it does
|
|
96
|
+
* not already exist. Never overwrites an existing file — if one is present this
|
|
97
|
+
* is a no-op that reports `already-exists`. Tolerates every failure by returning
|
|
98
|
+
* a typed non-throwing result; callers must never let this crash a lifecycle
|
|
99
|
+
* transition.
|
|
100
|
+
*/
|
|
101
|
+
export async function autofillTraceabilityMatrix(args) {
|
|
102
|
+
const targetPath = handoffEvidencePath(args.projectId, args.specId, 'traceability-matrix.json');
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(targetPath)) {
|
|
105
|
+
return { written: false, reason: 'already-exists' };
|
|
106
|
+
}
|
|
107
|
+
const criteria = extractAcceptanceCriteriaTexts(args.specBody);
|
|
108
|
+
if (criteria.length === 0) {
|
|
109
|
+
return { written: false, reason: 'no-criteria' };
|
|
110
|
+
}
|
|
111
|
+
const scenarios = parseFrontmatterScenarios(args.specBody);
|
|
112
|
+
const exec = args.exec ?? defaultExec;
|
|
113
|
+
const baseBranch = args.baseBranch ?? 'main';
|
|
114
|
+
const gitChangedFiles = args.projectPath
|
|
115
|
+
? await computeChangedFilesFromGit(args.projectPath, exec, baseBranch)
|
|
116
|
+
: [];
|
|
117
|
+
const ownership = extractCanonicalFileOwnership(args.specBody);
|
|
118
|
+
const ownershipFiles = [...ownership.toCreate, ...ownership.toModify, ...ownership.toTest];
|
|
119
|
+
const rawChangedFiles = gitChangedFiles.length > 0 ? gitChangedFiles : ownershipFiles;
|
|
120
|
+
const changedFiles = args.projectPath
|
|
121
|
+
? filterExistingRepoFiles(args.projectPath, rawChangedFiles)
|
|
122
|
+
: rawChangedFiles;
|
|
123
|
+
const { validationEvidence, reviewerEvidence } = await readValidationReportSummary(args.projectId, args.specId);
|
|
124
|
+
const rows = criteria.map((acceptanceCriterion, index) => {
|
|
125
|
+
const testPaths = scenarios[index]?.tests?.map((test) => test.path) ?? [];
|
|
126
|
+
return {
|
|
127
|
+
acceptanceCriterion,
|
|
128
|
+
...(testPaths.length > 0 ? { testEvidence: testPaths } : {}),
|
|
129
|
+
changedFiles,
|
|
130
|
+
...(validationEvidence ? { validationEvidence } : {}),
|
|
131
|
+
...(reviewerEvidence ? { reviewerEvidence } : {}),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
const payload = { version: 1, rows };
|
|
135
|
+
await atomicWriteFile(targetPath, JSON.stringify(payload, null, 2));
|
|
136
|
+
return { written: true, path: targetPath, rowCount: rows.length };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
/* reliability-optional: EVIDENCE_AUTOFILL_WRITE — caller proceeds without autofill */
|
|
140
|
+
reportClassifiedDegradation('EVIDENCE_AUTOFILL_WRITE', error);
|
|
141
|
+
return {
|
|
142
|
+
written: false,
|
|
143
|
+
reason: 'write-failed',
|
|
144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=evidence-autofill.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Spec } from '../../types/spec/core.js';
|
|
2
|
+
import type { DiscoveryEvidence, TaskPlanEvidence } from '../../types/evidence-gates.js';
|
|
3
|
+
/** Build a strict-schema discovery.json skeleton grounded in the spec's own grounding metadata. */
|
|
4
|
+
export declare function generateDiscoverySkeleton(args: {
|
|
5
|
+
spec: Spec;
|
|
6
|
+
body: string | null;
|
|
7
|
+
}): DiscoveryEvidence;
|
|
8
|
+
/** Build a strict-schema task-plan.json skeleton with one pending task per canonical criterion. */
|
|
9
|
+
export declare function generateTaskPlanSkeleton(args: {
|
|
10
|
+
criteria: string[];
|
|
11
|
+
}): TaskPlanEvidence;
|
|
12
|
+
/** Persist an auto-generated evidence skeleton to the external handoff store. */
|
|
13
|
+
export declare function writeEvidenceSkeleton(args: {
|
|
14
|
+
projectId: string;
|
|
15
|
+
specId: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
artifact: DiscoveryEvidence | TaskPlanEvidence;
|
|
18
|
+
}): Promise<string>;
|
|
19
|
+
//# sourceMappingURL=evidence-skeletons.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// evidence-skeletons.ts — SPEC-1356: auto-generate schema-valid discovery/task-plan skeletons
|
|
2
|
+
// when the underlying handoff evidence file is missing, so lifecycle gates self-heal instead
|
|
3
|
+
// of blocking on a manual authoring step. Skeletons still require review; they only unblock
|
|
4
|
+
// the transition, they do not fabricate confidence.
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { extractListItems, extractSection } from '../spec-format/markdown-sections.js';
|
|
7
|
+
import { parseCriterionGroundingRecords } from '../spec-grounding/contract.js';
|
|
8
|
+
import { projectDataDir } from '../../storage/base-store.js';
|
|
9
|
+
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
10
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
11
|
+
const DEFAULT_OUT_OF_SCOPE = 'None declared beyond the spec scope.';
|
|
12
|
+
const DEFAULT_RULE = 'Behavior must follow the approved spec Problem and Technical sections.';
|
|
13
|
+
function specFrontmatter(specBody) {
|
|
14
|
+
return FRONTMATTER_RE.exec(specBody)?.[1] ?? '';
|
|
15
|
+
}
|
|
16
|
+
function outOfScopeFromBody(specBody) {
|
|
17
|
+
if (!specBody) {
|
|
18
|
+
return [DEFAULT_OUT_OF_SCOPE];
|
|
19
|
+
}
|
|
20
|
+
const section = extractSection(specBody, 'out of scope');
|
|
21
|
+
if (!section) {
|
|
22
|
+
return [DEFAULT_OUT_OF_SCOPE];
|
|
23
|
+
}
|
|
24
|
+
const items = extractListItems(section);
|
|
25
|
+
return items.length > 0 ? items : [DEFAULT_OUT_OF_SCOPE];
|
|
26
|
+
}
|
|
27
|
+
/** Build a strict-schema discovery.json skeleton grounded in the spec's own grounding metadata. */
|
|
28
|
+
export function generateDiscoverySkeleton(args) {
|
|
29
|
+
const records = args.body ? parseCriterionGroundingRecords(specFrontmatter(args.body)) : [];
|
|
30
|
+
const rules = records.length > 0 ? [...new Set(records.map((record) => record.text))] : [DEFAULT_RULE];
|
|
31
|
+
const examples = records.length > 0
|
|
32
|
+
? records.map((record) => ({
|
|
33
|
+
rule: record.text,
|
|
34
|
+
example: record.evidence[0] ??
|
|
35
|
+
`Derived from ${args.spec.id} (${args.spec.title}) grounding evidence.`,
|
|
36
|
+
}))
|
|
37
|
+
: [{ rule: DEFAULT_RULE, example: `Derived from spec ${args.spec.id}: ${args.spec.title}.` }];
|
|
38
|
+
return {
|
|
39
|
+
version: 1,
|
|
40
|
+
rules,
|
|
41
|
+
examples,
|
|
42
|
+
openQuestions: [],
|
|
43
|
+
outOfScope: outOfScopeFromBody(args.body),
|
|
44
|
+
glossary: [{ term: args.spec.title, meaning: args.spec.feature ?? args.spec.title }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Build a strict-schema task-plan.json skeleton with one pending task per canonical criterion. */
|
|
48
|
+
export function generateTaskPlanSkeleton(args) {
|
|
49
|
+
const criteria = args.criteria.length > 0 ? args.criteria : ['Implementation satisfies the approved spec.'];
|
|
50
|
+
return {
|
|
51
|
+
version: 1,
|
|
52
|
+
tasks: criteria.map((criterion, index) => ({
|
|
53
|
+
id: `AC-${String(index + 1)}`,
|
|
54
|
+
title: criterion.length > 500 ? `${criterion.slice(0, 497)}...` : criterion,
|
|
55
|
+
acceptanceCriteria: [`AC${String(index + 1)}`],
|
|
56
|
+
status: 'pending',
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function handoffPath(projectId, specId, filename) {
|
|
61
|
+
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
62
|
+
}
|
|
63
|
+
/** Persist an auto-generated evidence skeleton to the external handoff store. */
|
|
64
|
+
export async function writeEvidenceSkeleton(args) {
|
|
65
|
+
const path = handoffPath(args.projectId, args.specId, args.filename);
|
|
66
|
+
await atomicWriteFile(path, `${JSON.stringify(args.artifact, null, 2)}\n`);
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=evidence-skeletons.js.map
|
|
@@ -54,9 +54,15 @@ export class OperationJournal {
|
|
|
54
54
|
const existing = this.get(operation, idempotencyKey);
|
|
55
55
|
if (existing) {
|
|
56
56
|
if (existing.requestDigest !== requestDigest) {
|
|
57
|
-
|
|
57
|
+
// SPEC-1348: a rolled-back attempt produced no durable effects, so the
|
|
58
|
+
// key is re-claimable even when the retry payload differs byte-wise.
|
|
59
|
+
if (existing.state !== 'rolled-back') {
|
|
60
|
+
throw new BoundaryFailure('Conflict', 'operation begin', 'Use a new idempotency key for a different request.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
return existing;
|
|
58
65
|
}
|
|
59
|
-
return existing;
|
|
60
66
|
}
|
|
61
67
|
const now = new Date().toISOString();
|
|
62
68
|
const entry = {
|
|
@@ -66,8 +72,8 @@ export class OperationJournal {
|
|
|
66
72
|
requestDigest,
|
|
67
73
|
state: 'intent',
|
|
68
74
|
...(recovery ?? {}),
|
|
69
|
-
recoveryAttempts: 0,
|
|
70
|
-
createdAt: now,
|
|
75
|
+
recoveryAttempts: existing?.recoveryAttempts ?? 0,
|
|
76
|
+
createdAt: existing?.createdAt ?? now,
|
|
71
77
|
updatedAt: now,
|
|
72
78
|
};
|
|
73
79
|
this.put(entry);
|