@planu/cli 5.7.5 → 5.7.6
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 +75 -0
- package/README.md +3 -3
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/package-handoff.js +30 -5
- package/dist/config/compliance-profiles.json +36 -3
- package/dist/config/environment-schema.json +21 -0
- package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
- package/dist/config/official-sdd-tools.d.ts +1 -1
- package/dist/config/official-sdd-tools.js +11 -24
- package/dist/config/registries/hosts/codex.json +14 -30
- package/dist/config/registries/hosts/opencode.json +1 -6
- package/dist/config/server-instructions.js +0 -21
- package/dist/config/skill-templates/planu-context-assets.md +3 -6
- package/dist/config/skill-templates/planu-implement.md +2 -2
- package/dist/config/skill-templates/planu-multi-teammate-review.md +1 -1
- package/dist/config/skill-templates/planu-release.md +2 -7
- package/dist/config/skill-templates/planu-validate.md +3 -3
- package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
- package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
- package/dist/config/spec-templates/stripe-payments/template.json +5 -1
- package/dist/config/spec-templates/webhook-system/template.json +5 -1
- package/dist/config/subagent-templates/planu-challenger.md +4 -9
- package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
- package/dist/config/subagent-templates/planu-validator.md +5 -8
- package/dist/config/tool-groups.json +6 -144
- package/dist/engine/ambiguity-scorer.js +40 -1
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/code-impact-analyzer.js +2 -6
- package/dist/engine/context-orchestrator/index.js +2 -2
- package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
- package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
- package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
- package/dist/engine/evidence-gates/artifact-reader.js +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
- package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
- package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
- package/dist/engine/evidence-index/index-builder.js +21 -4
- package/dist/engine/execution/job-runtime.js +1 -1
- package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
- package/dist/engine/execution/package-handoff-job-executor.js +490 -0
- package/dist/engine/execution/validate-job-executor.js +20 -8
- package/dist/engine/git/exec-git.d.ts +4 -0
- package/dist/engine/git/exec-git.js +14 -0
- package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
- package/dist/engine/handoff-artifacts/schemas.js +1 -0
- package/dist/engine/handoff-format.d.ts +36 -0
- package/dist/engine/handoff-format.js +314 -0
- package/dist/engine/handoff-packager.d.ts +5 -3
- package/dist/engine/handoff-packager.js +89 -32
- package/dist/engine/host-tool-filter.js +20 -49
- package/dist/engine/human-summary.js +14 -3
- package/dist/engine/implementation-contract/common.d.ts +8 -3
- package/dist/engine/implementation-contract/common.js +9 -6
- package/dist/engine/implementation-contract/evaluator.js +4 -6
- package/dist/engine/implementation-contract/renderer.js +18 -23
- package/dist/engine/project-health-checker.js +2 -2
- package/dist/engine/readiness-checker.js +27 -0
- package/dist/engine/reconcile/apply-changes.js +4 -0
- package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
- package/dist/engine/reconcile/propagate-mirrors.js +311 -0
- package/dist/engine/self-healing/healer.js +12 -5
- package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
- package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
- package/dist/engine/self-healing/strategies/test-fix.js +32 -14
- package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
- package/dist/engine/session/checkpoint-writer.js +8 -4
- package/dist/engine/session/session-tracker.d.ts +5 -16
- package/dist/engine/session/session-tracker.js +29 -26
- package/dist/engine/session-state/writer.js +3 -11
- package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
- package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
- package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
- package/dist/engine/spec-format/bdd-parser.js +40 -11
- package/dist/engine/spec-format/lean-spec-generator.js +30 -10
- package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
- package/dist/engine/spec-format/retired-scaffold.js +5 -0
- package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
- package/dist/engine/spec-format/technical-md-populator.js +27 -0
- package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
- package/dist/engine/spec-format/unified-spec-builder.js +30 -20
- package/dist/engine/spec-grounding/contract.js +115 -1
- package/dist/engine/spec-quality/generic-output-gate.js +37 -3
- package/dist/engine/technical-enricher/index.js +4 -1
- package/dist/engine/type-safety-gate.js +3 -11
- package/dist/engine/universal-rules/catalog.js +0 -2
- package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
- package/dist/engine/validation/durable-validation.d.ts +3 -2
- package/dist/engine/validation/durable-validation.js +106 -40
- package/dist/engine/validation/validation-freshness.d.ts +3 -2
- package/dist/engine/validation/validation-freshness.js +22 -16
- package/dist/engine/validation/validation-worktree.js +34 -0
- package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
- package/dist/index.js +3 -17
- package/dist/resources/process.js +38 -65
- package/dist/storage/session-state-store.js +2 -0
- package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
- package/dist/tools/configure-checkpoint-policy.js +3 -3
- package/dist/tools/create-spec-helpers.js +2 -5
- package/dist/tools/create-spec.js +21 -4
- package/dist/tools/facilitate.js +3 -6
- package/dist/tools/git/branch-ops.js +16 -4
- package/dist/tools/github-release-handler.js +1 -20
- package/dist/tools/init-project/agents-md-writer.js +6 -6
- package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
- package/dist/tools/init-project/claude-md-generator.js +0 -35
- package/dist/tools/init-project/handler.js +7 -5
- package/dist/tools/init-project/per-client-files-writer.js +5 -5
- package/dist/tools/init-project/planu-workflow-generator.js +2 -13
- package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
- package/dist/tools/init-project/portable-index-reconciler.js +7 -2
- package/dist/tools/init-project/rules-generator.d.ts +0 -11
- package/dist/tools/init-project/rules-generator.js +4 -85
- package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
- package/dist/tools/init-project/scaffold-writer.js +1 -12
- package/dist/tools/jobs/handlers.d.ts +0 -6
- package/dist/tools/jobs/handlers.js +13 -40
- package/dist/tools/list-specs.js +6 -21
- package/dist/tools/package-handoff.d.ts +3 -2
- package/dist/tools/package-handoff.js +153 -427
- package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
- package/dist/tools/register-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/core-spec-tools.js +18 -27
- package/dist/tools/schemas/index.d.ts +1 -1
- package/dist/tools/schemas/index.js +1 -1
- package/dist/tools/schemas/output-schemas.d.ts +1 -18
- package/dist/tools/schemas/output-schemas.js +1 -11
- package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
- package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
- package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
- package/dist/tools/schemas/validate-output-schema.js +3 -2
- package/dist/tools/session-checkpoint.js +2 -2
- package/dist/tools/skill-registry/index.d.ts +0 -1
- package/dist/tools/skill-registry/index.js +0 -1
- package/dist/tools/sync-spec-state-handler.d.ts +2 -2
- package/dist/tools/sync-spec-state-handler.js +23 -3
- package/dist/tools/tool-registry/core-tools.js +1 -140
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +9 -1
- package/dist/tools/update-status/batch.js +17 -0
- package/dist/tools/update-status/dod-gates.js +14 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
- package/dist/tools/update-status/done-receipt-verifier.js +56 -26
- package/dist/tools/update-status/evidence-gate.js +10 -21
- package/dist/tools/update-status/file-sync.d.ts +1 -0
- package/dist/tools/update-status/file-sync.js +6 -0
- package/dist/tools/update-status/index.d.ts +6 -0
- package/dist/tools/update-status/index.js +39 -44
- package/dist/tools/update-status-actions.js +2 -7
- package/dist/tools/validate.js +34 -28
- package/dist/transports/oauth-validator.js +12 -1
- package/dist/transports/transport-factory.d.ts +2 -1
- package/dist/transports/transport-factory.js +19 -0
- package/dist/types/common/primitives.d.ts +2 -0
- package/dist/types/durable-job.d.ts +45 -0
- package/dist/types/durable-validation.d.ts +1 -1
- package/dist/types/evidence-autofill.d.ts +13 -1
- package/dist/types/handoff-artifacts.d.ts +2 -1
- package/dist/types/readiness.d.ts +8 -1
- package/dist/types/reconcile.d.ts +6 -0
- package/dist/types/skill-registry.d.ts +26 -1
- package/dist/types/spec/core.d.ts +5 -0
- package/dist/types/spec/inputs.d.ts +2 -2
- package/dist/types/spec-format.d.ts +11 -1
- package/dist/types/spec-grounding.d.ts +12 -0
- package/dist/types/transport.d.ts +1 -0
- package/dist/types/validation-receipt.d.ts +27 -0
- package/package.json +3 -2
- package/planu-plugin.json +13 -26
- package/src/i18n/messages/en.json +1 -1
- package/src/i18n/messages/es.json +1 -1
- package/src/i18n/messages/pt.json +1 -1
- package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
- package/dist/engine/skill-generator/conventions-hasher.js +0 -24
- package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
- package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
- package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
- package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
- package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
- package/dist/tools/clarify-requirements/questions-context.js +0 -74
- package/dist/tools/clarify-requirements/questions.d.ts +0 -16
- package/dist/tools/clarify-requirements/questions.js +0 -96
- package/dist/tools/clarify-requirements.d.ts +0 -4
- package/dist/tools/clarify-requirements.js +0 -314
- package/dist/tools/code-graph-handler.d.ts +0 -6
- package/dist/tools/code-graph-handler.js +0 -72
- package/dist/tools/create-rule.d.ts +0 -4
- package/dist/tools/create-rule.js +0 -107
- package/dist/tools/feedback-handler.d.ts +0 -7
- package/dist/tools/feedback-handler.js +0 -150
- package/dist/tools/semantic-search-handler.d.ts +0 -7
- package/dist/tools/semantic-search-handler.js +0 -71
- package/dist/tools/skill-registry/search.d.ts +0 -11
- package/dist/tools/skill-registry/search.js +0 -159
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// two-file spec.md + technical.md pair only to be merged on the next list_specs run.
|
|
4
4
|
import { stripFrontmatter } from '../frontmatter-parser.js';
|
|
5
5
|
import { extractAcceptanceCriteriaTexts } from './acceptance-criteria.js';
|
|
6
|
+
import { isRetiredScaffoldText } from './retired-scaffold.js';
|
|
6
7
|
import { escapeRegex } from '../../core/shared/strings.js';
|
|
7
8
|
const REQUIRED_SECTIONS = [
|
|
8
9
|
'Problem',
|
|
@@ -24,7 +25,7 @@ export function buildCanonicalUnifiedSpecContent(input) {
|
|
|
24
25
|
const goal = firstNonEmpty(explicitGoal, input.criteria[0]?.text, 'Needs decision: define the goal this spec must achieve.');
|
|
25
26
|
const explicitTechnical = extractTopLevelSectionBody(source, 'Technical');
|
|
26
27
|
const generatedNotes = extractTopLevelSectionBody(input.technicalBody, 'Implementation Notes');
|
|
27
|
-
const technical = firstNonEmpty(explicitTechnical, generatedNotes,
|
|
28
|
+
const technical = firstNonEmpty(explicitTechnical, generatedNotes, groundedTechnicalFallback(input.files, input.criteria));
|
|
28
29
|
const acceptance = extractExecutableCriteria(source, input.criteria);
|
|
29
30
|
const explicitOutOfScope = extractTopLevelSectionBody(source, 'Out of scope');
|
|
30
31
|
const outOfScope = input.outOfScope.length > 0
|
|
@@ -224,8 +225,12 @@ export function validateUnifiedSpecCandidate(candidate, options = {}) {
|
|
|
224
225
|
});
|
|
225
226
|
}
|
|
226
227
|
const acceptanceSection = extractTopLevelSectionBody(scannable, 'Acceptance Criteria');
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
const acceptanceSectionForBddCheck = acceptanceSection
|
|
229
|
+
?.split('\n')
|
|
230
|
+
.filter((line) => !isRetiredScaffoldText(line))
|
|
231
|
+
.join('\n');
|
|
232
|
+
if (!acceptanceSectionForBddCheck ||
|
|
233
|
+
!/\bGIVEN\b[\s\S]*\bWHEN\b[\s\S]*\bTHEN\b/i.test(acceptanceSectionForBddCheck)) {
|
|
229
234
|
issues.push({
|
|
230
235
|
code: 'MISSING_EXECUTABLE_CRITERIA',
|
|
231
236
|
section: 'Acceptance Criteria',
|
|
@@ -248,7 +253,7 @@ export function validateUnifiedSpecCandidate(candidate, options = {}) {
|
|
|
248
253
|
}
|
|
249
254
|
return { valid: issues.length === 0, issues };
|
|
250
255
|
}
|
|
251
|
-
function maskFencedAndQuotedText(body) {
|
|
256
|
+
export function maskFencedAndQuotedText(body) {
|
|
252
257
|
const lines = body.split('\n');
|
|
253
258
|
let fence = null;
|
|
254
259
|
return lines
|
|
@@ -305,27 +310,18 @@ function firstNonEmpty(...values) {
|
|
|
305
310
|
return (values.find((value) => value !== undefined && value !== null && value.length > 0) ?? '');
|
|
306
311
|
}
|
|
307
312
|
function fallbackCriteriaBullets(criteria) {
|
|
308
|
-
return criteria
|
|
309
|
-
.map((criterion) => `- GIVEN the requested change is implemented WHEN the behavior is exercised THEN ${criterion.text}`)
|
|
310
|
-
.join('\n');
|
|
313
|
+
return criteria.map((criterion) => `- ${criterion.text}`).join('\n');
|
|
311
314
|
}
|
|
312
|
-
// Same BDD step vocabulary `acceptance-criteria.ts` recognizes (English + Spanish).
|
|
313
|
-
// A criterion already starting with one of these is a genuine BDD fragment
|
|
314
|
-
// (a full GIVEN/WHEN/THEN block, or an AND continuation of one) — leave it as-is.
|
|
315
|
-
const BDD_STEP_START = /^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i;
|
|
316
315
|
/**
|
|
317
|
-
* Render one real criterion as
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
316
|
+
* Render one real criterion as a bullet. Criteria already written in BDD form
|
|
317
|
+
* pass through untouched; plain-text criteria (e.g. checkbox items) render as
|
|
318
|
+
* plain bullets — SPEC-1435 retires the constant GIVEN/WHEN/THEN scaffold that
|
|
319
|
+
* used to wrap them, since it satisfied `MISSING_EXECUTABLE_CRITERIA` without
|
|
320
|
+
* adding any spec-specific content.
|
|
322
321
|
*/
|
|
323
322
|
function toExecutableBullet(text) {
|
|
324
323
|
const cleaned = text.trim().replace(/[.\s]+$/, '.');
|
|
325
|
-
|
|
326
|
-
return `- ${cleaned}`;
|
|
327
|
-
}
|
|
328
|
-
return `- GIVEN the requested change is implemented WHEN the behavior is exercised THEN ${cleaned}`;
|
|
324
|
+
return `- ${cleaned}`;
|
|
329
325
|
}
|
|
330
326
|
/**
|
|
331
327
|
* Criteria resolve through the canonical parser in `acceptance-criteria.ts`
|
|
@@ -339,6 +335,20 @@ function extractExecutableCriteria(source, criteria) {
|
|
|
339
335
|
}
|
|
340
336
|
return parsed.map(toExecutableBullet).join('\n');
|
|
341
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* SPEC-1435: replaces the constant Technical fallback sentence. When neither an
|
|
340
|
+
* author-written `## Technical` body nor generated implementation notes exist, compose
|
|
341
|
+
* the section from the spec's own grounded material instead of a fixed sentence that
|
|
342
|
+
* never varies with the spec: the grounded file records when present, otherwise the
|
|
343
|
+
* criteria texts.
|
|
344
|
+
*/
|
|
345
|
+
function groundedTechnicalFallback(files, criteria) {
|
|
346
|
+
const groundedFiles = [...files.create, ...files.modify, ...files.test];
|
|
347
|
+
if (groundedFiles.length > 0) {
|
|
348
|
+
return groundedFiles.map((entry) => `- \`${entry.path}\``).join('\n');
|
|
349
|
+
}
|
|
350
|
+
return criteria.map((criterion) => `- ${criterion.text}`).join('\n');
|
|
351
|
+
}
|
|
342
352
|
function renderOwnedFiles(files) {
|
|
343
353
|
const sections = [];
|
|
344
354
|
for (const [title, entries] of [
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
1
2
|
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
3
|
+
const GROUNDING_SOURCES = [
|
|
4
|
+
'user_input',
|
|
5
|
+
'project_evidence',
|
|
6
|
+
'documented_assumption',
|
|
7
|
+
'ungrounded_advisory',
|
|
8
|
+
];
|
|
9
|
+
const GROUNDING_CONFIDENCES = ['low', 'medium', 'high'];
|
|
10
|
+
const TECHNICAL_REFERENCE_SECTIONS = [
|
|
11
|
+
'create',
|
|
12
|
+
'modify',
|
|
13
|
+
'test',
|
|
14
|
+
];
|
|
2
15
|
export { normalizeCriterionText } from '../criterion-identity.js';
|
|
3
16
|
const GENERIC_CRITERION_PATTERNS = [
|
|
4
17
|
/\bimplementation complete\b/i,
|
|
@@ -135,6 +148,107 @@ export function checkGroundedSpecContract(spec, content) {
|
|
|
135
148
|
};
|
|
136
149
|
}
|
|
137
150
|
export function parseCriterionGroundingRecords(frontmatter) {
|
|
151
|
+
const outcome = parseFrontmatterYaml(frontmatter);
|
|
152
|
+
if (!outcome.parsed) {
|
|
153
|
+
return parseCriterionGroundingRecordsLegacy(frontmatter);
|
|
154
|
+
}
|
|
155
|
+
const grounding = readGroundingSection(outcome.document);
|
|
156
|
+
const entries = Array.isArray(grounding?.criteria) ? grounding.criteria : [];
|
|
157
|
+
const records = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const record = toCriterionGroundingRecord(entry);
|
|
160
|
+
if (record) {
|
|
161
|
+
records.push(record);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return records;
|
|
165
|
+
}
|
|
166
|
+
export function parseTechnicalReferenceGroundingRecords(frontmatter) {
|
|
167
|
+
const outcome = parseFrontmatterYaml(frontmatter);
|
|
168
|
+
if (!outcome.parsed) {
|
|
169
|
+
return parseTechnicalReferenceGroundingRecordsLegacy(frontmatter);
|
|
170
|
+
}
|
|
171
|
+
const grounding = readGroundingSection(outcome.document);
|
|
172
|
+
const entries = grounding
|
|
173
|
+
? Array.isArray(grounding.technicalReferences)
|
|
174
|
+
? grounding.technicalReferences
|
|
175
|
+
: []
|
|
176
|
+
: Array.isArray(outcome.document?.technicalReferences)
|
|
177
|
+
? outcome.document.technicalReferences
|
|
178
|
+
: [];
|
|
179
|
+
const records = [];
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const record = toTechnicalReferenceGroundingRecord(entry);
|
|
182
|
+
if (record) {
|
|
183
|
+
records.push(record);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return records;
|
|
187
|
+
}
|
|
188
|
+
function parseFrontmatterYaml(frontmatter) {
|
|
189
|
+
let value;
|
|
190
|
+
try {
|
|
191
|
+
value = YAML.parse(frontmatter);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { parsed: false };
|
|
195
|
+
}
|
|
196
|
+
return { parsed: true, document: isRecord(value) ? value : undefined };
|
|
197
|
+
}
|
|
198
|
+
function readGroundingSection(document) {
|
|
199
|
+
const grounding = document?.grounding;
|
|
200
|
+
return isRecord(grounding) ? grounding : undefined;
|
|
201
|
+
}
|
|
202
|
+
function toCriterionGroundingRecord(entry) {
|
|
203
|
+
if (!isRecord(entry)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const text = typeof entry.text === 'string' && entry.text.length > 0 ? entry.text : null;
|
|
207
|
+
const source = toGroundingSource(entry.source);
|
|
208
|
+
const confidence = toGroundingConfidence(entry.confidence);
|
|
209
|
+
if (!text || !source || !confidence) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return { text, source, evidence: toStringArray(entry.evidence), confidence };
|
|
213
|
+
}
|
|
214
|
+
function toTechnicalReferenceGroundingRecord(entry) {
|
|
215
|
+
if (!isRecord(entry)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const path = typeof entry.path === 'string' && entry.path.length > 0 ? entry.path : null;
|
|
219
|
+
const section = toTechnicalReferenceSection(entry.section);
|
|
220
|
+
const source = toGroundingSource(entry.source);
|
|
221
|
+
const confidence = toGroundingConfidence(entry.confidence);
|
|
222
|
+
if (!path || !section || !source || !confidence) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return { path, section, source, evidence: toStringArray(entry.evidence), confidence };
|
|
226
|
+
}
|
|
227
|
+
function toGroundingSource(value) {
|
|
228
|
+
return typeof value === 'string' && GROUNDING_SOURCES.includes(value)
|
|
229
|
+
? value
|
|
230
|
+
: null;
|
|
231
|
+
}
|
|
232
|
+
function toGroundingConfidence(value) {
|
|
233
|
+
return typeof value === 'string' && GROUNDING_CONFIDENCES.includes(value)
|
|
234
|
+
? value
|
|
235
|
+
: null;
|
|
236
|
+
}
|
|
237
|
+
function toTechnicalReferenceSection(value) {
|
|
238
|
+
return typeof value === 'string' &&
|
|
239
|
+
TECHNICAL_REFERENCE_SECTIONS.includes(value)
|
|
240
|
+
? value
|
|
241
|
+
: null;
|
|
242
|
+
}
|
|
243
|
+
function toStringArray(value) {
|
|
244
|
+
return Array.isArray(value)
|
|
245
|
+
? value.filter((item) => typeof item === 'string')
|
|
246
|
+
: [];
|
|
247
|
+
}
|
|
248
|
+
function isRecord(value) {
|
|
249
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
250
|
+
}
|
|
251
|
+
function parseCriterionGroundingRecordsLegacy(frontmatter) {
|
|
138
252
|
const records = [];
|
|
139
253
|
const chunks = frontmatter.split(/\n\s{4}- text:\s*/).slice(1);
|
|
140
254
|
for (const chunk of chunks) {
|
|
@@ -154,7 +268,7 @@ export function parseCriterionGroundingRecords(frontmatter) {
|
|
|
154
268
|
}
|
|
155
269
|
return records;
|
|
156
270
|
}
|
|
157
|
-
|
|
271
|
+
function parseTechnicalReferenceGroundingRecordsLegacy(frontmatter) {
|
|
158
272
|
const records = [];
|
|
159
273
|
const chunks = frontmatter.split(/\n\s{4}- path:\s*/).slice(1);
|
|
160
274
|
for (const chunk of chunks) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { maskFencedAndQuotedText } from '../spec-format/unified-spec-builder.js';
|
|
2
|
+
import { RETIRED_SCAFFOLD_PREFIX } from '../spec-format/retired-scaffold.js';
|
|
1
3
|
/** Bounds the search for criteria to the frontmatter block only (between the two `---`
|
|
2
4
|
* delimiters), so criterion-shaped lines never leak in from the rendered body below it. */
|
|
3
5
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
@@ -21,7 +23,8 @@ const CRITERION_TEXT_RE = /^\s+- text:\s*"?(.+?)"?\s*$/gm;
|
|
|
21
23
|
* the criterion-equivalent for `scenarios:`-format specs, which have no `- text:` entries. */
|
|
22
24
|
const SCENARIO_TITLE_RE = /^\s+- title:\s*"?(.+?)"?\s*$/gm;
|
|
23
25
|
const FILE_LINE_RE = /^-\s+(.+?)\s+\((pending|done)\)\s*$/gm;
|
|
24
|
-
const
|
|
26
|
+
const SECTION_HEADING_RE = /^#{1,3}[ \t]+(.+?)[ \t]*$/gm;
|
|
27
|
+
const H3_HEADING_PREFIX_RE = /^###[ \t]/;
|
|
25
28
|
const BULLET_LINE_RE = /^[-*][ \t]+(.+)$/;
|
|
26
29
|
/** A bullet that, once a leading `Label:`, wrapping backticks, and a trailing `(status)`
|
|
27
30
|
* marker are stripped, is just a file path: contains a `/` and ends in a short extension.
|
|
@@ -98,9 +101,40 @@ export function checkGenericSpecOutput(content) {
|
|
|
98
101
|
...checkCriteria(criteria),
|
|
99
102
|
...checkTechnicalReferences(content),
|
|
100
103
|
...checkInterpolatedSections(content, criteria),
|
|
104
|
+
...checkRetiredFillerVocabulary(content),
|
|
101
105
|
];
|
|
102
106
|
return { passed: issues.length === 0, issues };
|
|
103
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* SPEC-1435: dedicated scan for the constant filler sentences the renderer and the
|
|
110
|
+
* unified spec builder used to emit whenever nothing was grounded. Those sentences
|
|
111
|
+
* satisfied structural checks (a non-empty subsection, an executable-criteria match)
|
|
112
|
+
* without carrying any spec-specific content, and no existing rule matched their exact
|
|
113
|
+
* wording. Scans the whole body — prose and bullets, any heading level — with fenced
|
|
114
|
+
* code blocks and quoted text masked the same way `unified-spec-builder.ts` masks them,
|
|
115
|
+
* so a legitimate example inside a fence or blockquote is never flagged.
|
|
116
|
+
*/
|
|
117
|
+
const RETIRED_FILLER_SENTENCES = [
|
|
118
|
+
'Implement only the grounded ownership and observable behavior declared by this contract.',
|
|
119
|
+
'No repository-supported verification command was grounded for this candidate.',
|
|
120
|
+
'No file ownership was grounded for this behavior.',
|
|
121
|
+
'No repository-supported verification evidence was grounded for these criteria.',
|
|
122
|
+
RETIRED_SCAFFOLD_PREFIX,
|
|
123
|
+
];
|
|
124
|
+
function checkRetiredFillerVocabulary(content) {
|
|
125
|
+
const scannable = maskFencedAndQuotedText(content);
|
|
126
|
+
const issues = [];
|
|
127
|
+
for (const sentence of RETIRED_FILLER_SENTENCES) {
|
|
128
|
+
if (scannable.includes(sentence)) {
|
|
129
|
+
issues.push({
|
|
130
|
+
kind: 'generic-technical-reference',
|
|
131
|
+
phrase: sentence,
|
|
132
|
+
reason: `retired filler text must not appear in a persisted spec: "${sentence}"`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return issues;
|
|
137
|
+
}
|
|
104
138
|
/**
|
|
105
139
|
* SPEC-1406 (DEFECT 2, round 2 — detector rebuilt around CRITERION RESTATEMENT): the
|
|
106
140
|
* previous prefix/suffix-length rule flagged `### File-Level Work Plan` (whose bullets
|
|
@@ -115,10 +149,10 @@ export function checkGenericSpecOutput(content) {
|
|
|
115
149
|
*/
|
|
116
150
|
function checkInterpolatedSections(content, criteria) {
|
|
117
151
|
const issues = [];
|
|
118
|
-
const headingMatches = [...content.matchAll(
|
|
152
|
+
const headingMatches = [...content.matchAll(SECTION_HEADING_RE)];
|
|
119
153
|
for (let i = 0; i < headingMatches.length; i += 1) {
|
|
120
154
|
const heading = headingMatches[i];
|
|
121
|
-
if (!heading?.[1]) {
|
|
155
|
+
if (!heading?.[1] || !H3_HEADING_PREFIX_RE.test(heading[0])) {
|
|
122
156
|
continue;
|
|
123
157
|
}
|
|
124
158
|
const start = heading.index + heading[0].length;
|
|
@@ -65,7 +65,10 @@ async function runEnricherFromSpecSection(opts) {
|
|
|
65
65
|
if (result.enriched && result.enrichedContent) {
|
|
66
66
|
const updated = `${specRaw.slice(0, section.contentStart)}${result.enrichedContent}${specRaw.slice(section.end)}`;
|
|
67
67
|
assertEnrichmentPreservesContract(specRaw, updated, 'review-enricher');
|
|
68
|
-
|
|
68
|
+
const relativeProposalPath = join('planu', '.runtime', 'enrichment-proposals', `${opts.specId}.md`);
|
|
69
|
+
const proposalPath = join(opts.projectPath, relativeProposalPath);
|
|
70
|
+
await atomicWriteFile(proposalPath, updated);
|
|
71
|
+
console.error(`Review enrichment proposal written to ${relativeProposalPath}`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// engine/type-safety-gate.ts — SPEC-968: Deterministic TypeScript anti-pattern scanner
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
2
|
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
4
|
+
import { execGitCapture } from './git/exec-git.js';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Helpers
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -9,11 +9,7 @@ function getChangedTsFiles(projectPath) {
|
|
|
9
9
|
let names;
|
|
10
10
|
// Try origin/main...HEAD (typical feature branch)
|
|
11
11
|
try {
|
|
12
|
-
names =
|
|
13
|
-
cwd: projectPath,
|
|
14
|
-
encoding: 'utf-8',
|
|
15
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
16
|
-
})
|
|
12
|
+
names = execGitCapture('git diff --name-only --diff-filter=ACM origin/main...HEAD', projectPath)
|
|
17
13
|
.trim()
|
|
18
14
|
.split('\n')
|
|
19
15
|
.filter(Boolean);
|
|
@@ -21,11 +17,7 @@ function getChangedTsFiles(projectPath) {
|
|
|
21
17
|
catch {
|
|
22
18
|
// Fallback: last commit
|
|
23
19
|
try {
|
|
24
|
-
names =
|
|
25
|
-
cwd: projectPath,
|
|
26
|
-
encoding: 'utf-8',
|
|
27
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
-
})
|
|
20
|
+
names = execGitCapture('git diff --name-only --diff-filter=ACM HEAD~1..HEAD', projectPath)
|
|
29
21
|
.trim()
|
|
30
22
|
.split('\n')
|
|
31
23
|
.filter(Boolean);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { planuDogfoodBugsRule } from './rules/planu-dogfood-bugs.js';
|
|
4
4
|
import { planuWorkflowRule } from './rules/planu-workflow.js';
|
|
5
5
|
import { planuModesRule } from './rules/planu-modes.js';
|
|
6
|
-
import { agentTeamsRule } from './rules/agent-teams.js';
|
|
7
6
|
import { planuEnglishSpecsRule } from './rules/planu-english-specs.js';
|
|
8
7
|
import { planuBddCriteriaRule } from './rules/planu-bdd-criteria.js';
|
|
9
8
|
import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
|
|
@@ -26,7 +25,6 @@ export const UNIVERSAL_RULES = [
|
|
|
26
25
|
planuMinimalChangeRule,
|
|
27
26
|
planuReleasePolicyRule,
|
|
28
27
|
planuModesRule,
|
|
29
|
-
agentTeamsRule,
|
|
30
28
|
planuDogfoodBugsRule,
|
|
31
29
|
planuDebateReviewRule,
|
|
32
30
|
planuRevertProofTestsRule,
|
|
@@ -48,7 +48,7 @@ Any new feature, bug fix, refactor, integration, or task > 5 min.
|
|
|
48
48
|
|------|------|
|
|
49
49
|
| New feature | \`facilitate("what to build")\` |
|
|
50
50
|
| Project status | \`planu_status\` |
|
|
51
|
-
| Find related specs | \`
|
|
51
|
+
| Find related specs | \`list_specs\` and scan titles |
|
|
52
52
|
| List approved work | \`list_specs\` with \`status: approved\` |
|
|
53
53
|
| Mark done | \`update_status\` → \`done\` |
|
|
54
54
|
| Verify | \`validate\` |
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ValidationArtifactDigests, ValidationReceiptBindings } from '../../types/validation-receipt.js';
|
|
1
|
+
import type { ValidationArtifactDigests, ValidationArtifactProvenanceWriteResult, ValidationReceiptBindings } from '../../types/validation-receipt.js';
|
|
2
2
|
import type { DurableValidationBindings, ValidationPhase, ValidationPhaseGraphResult, ValidationPhaseProgress } from '../../types/durable-validation.js';
|
|
3
3
|
export type { DurableValidationBindings } from '../../types/durable-validation.js';
|
|
4
4
|
export { gitSourceTreeDigest } from './validation-source-digest.js';
|
|
5
5
|
export { createPendingValidationSubmission, createValidationSubmission, validationOperationId, validationSubmissionOperationId, } from './validation-submission.js';
|
|
6
|
+
export declare function absentArtifactDigest(kind: string): string;
|
|
6
7
|
export declare function toValidationReceiptBindings(bindings: DurableValidationBindings, artifactDigests: ValidationArtifactDigests): ValidationReceiptBindings;
|
|
7
8
|
/**
|
|
8
9
|
* Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
|
|
@@ -28,7 +29,7 @@ export declare function writeValidationArtifactProvenance(input: {
|
|
|
28
29
|
readonly receiptId: string;
|
|
29
30
|
readonly issuerId: string;
|
|
30
31
|
readonly artifactDigests: ValidationArtifactDigests;
|
|
31
|
-
}): Promise<
|
|
32
|
+
}): Promise<ValidationArtifactProvenanceWriteResult>;
|
|
32
33
|
/** Redact progress before it crosses the durable checkpoint boundary. */
|
|
33
34
|
export declare function redactValidationProgress(progress: ValidationPhaseProgress): ValidationPhaseProgress;
|
|
34
35
|
/** Execute independent validation gates with one shared deadline and cancellation tree. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { technologyValue } from '../technology-registry.js';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { runAbortableProcess } from '../abortable-process-runner.js';
|
|
6
6
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
@@ -15,6 +15,13 @@ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
|
15
15
|
const DEFAULT_MAX_CONCURRENCY = 2;
|
|
16
16
|
const MAX_CONCURRENCY = 8;
|
|
17
17
|
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
const APPROVED_SPEC_MISSING_LABEL = 'approved-spec';
|
|
19
|
+
export function absentArtifactDigest(kind) {
|
|
20
|
+
return evidenceArtifactDigest({ missing: kind });
|
|
21
|
+
}
|
|
22
|
+
function missingFileDigest(label) {
|
|
23
|
+
return digest(`missing:${label}`);
|
|
24
|
+
}
|
|
18
25
|
function assertBindings(bindings) {
|
|
19
26
|
if (!CANONICAL_PROJECT_ID.test(bindings.projectId)) {
|
|
20
27
|
throw new Error('[Planu] Durable validation requires a canonical projectId');
|
|
@@ -47,6 +54,18 @@ export function toValidationReceiptBindings(bindings, artifactDigests) {
|
|
|
47
54
|
artifactDigests,
|
|
48
55
|
};
|
|
49
56
|
}
|
|
57
|
+
function codeBindingsMatch(observed, stored) {
|
|
58
|
+
return (observed.projectId === stored.projectId &&
|
|
59
|
+
observed.specId === stored.specId &&
|
|
60
|
+
observed.sourceDigest === stored.sourceDigest &&
|
|
61
|
+
observed.specDigest === stored.specDigest &&
|
|
62
|
+
observed.policyDigest === stored.policyDigest &&
|
|
63
|
+
observed.toolchainDigest === stored.toolchainDigest &&
|
|
64
|
+
observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec);
|
|
65
|
+
}
|
|
66
|
+
function digestRebindsToSentinel(observedDigest, storedDigest, kind) {
|
|
67
|
+
return observedDigest !== storedDigest && observedDigest === absentArtifactDigest(kind);
|
|
68
|
+
}
|
|
50
69
|
/**
|
|
51
70
|
* Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
|
|
52
71
|
* be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
|
|
@@ -57,20 +76,22 @@ export function doneReceiptBindingsDifferOnlyInEvidence(observed, stored) {
|
|
|
57
76
|
if (!observed?.artifactDigests || !stored?.artifactDigests) {
|
|
58
77
|
return false;
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
observed.specId === stored.specId &&
|
|
62
|
-
observed.sourceDigest === stored.sourceDigest &&
|
|
63
|
-
observed.specDigest === stored.specDigest &&
|
|
64
|
-
observed.policyDigest === stored.policyDigest &&
|
|
65
|
-
observed.toolchainDigest === stored.toolchainDigest &&
|
|
66
|
-
observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec;
|
|
67
|
-
if (!codeBindingsMatch) {
|
|
79
|
+
if (!codeBindingsMatch(observed, stored)) {
|
|
68
80
|
return false;
|
|
69
81
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
const observedArtifacts = observed.artifactDigests;
|
|
83
|
+
const storedArtifacts = stored.artifactDigests;
|
|
84
|
+
const evidenceDiffers = observedArtifacts.discovery !== storedArtifacts.discovery ||
|
|
85
|
+
observedArtifacts.taskPlan !== storedArtifacts.taskPlan ||
|
|
86
|
+
observedArtifacts.traceability !== storedArtifacts.traceability ||
|
|
87
|
+
observedArtifacts.contractValidation !== storedArtifacts.contractValidation;
|
|
88
|
+
if (!evidenceDiffers) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const rebindsToAbsentEvidence = digestRebindsToSentinel(observedArtifacts.discovery, storedArtifacts.discovery, 'discovery') ||
|
|
92
|
+
digestRebindsToSentinel(observedArtifacts.taskPlan, storedArtifacts.taskPlan, 'task-plan') ||
|
|
93
|
+
digestRebindsToSentinel(observedArtifacts.traceability, storedArtifacts.traceability, 'traceability');
|
|
94
|
+
return !rebindsToAbsentEvidence;
|
|
74
95
|
}
|
|
75
96
|
async function digestFile(path, missingLabel) {
|
|
76
97
|
try {
|
|
@@ -78,7 +99,7 @@ async function digestFile(path, missingLabel) {
|
|
|
78
99
|
}
|
|
79
100
|
catch (error) {
|
|
80
101
|
if (error.code === 'ENOENT') {
|
|
81
|
-
return
|
|
102
|
+
return missingFileDigest(missingLabel);
|
|
82
103
|
}
|
|
83
104
|
throw error;
|
|
84
105
|
}
|
|
@@ -101,7 +122,7 @@ async function readEvidenceFile(path) {
|
|
|
101
122
|
/** Compute immutable validation and lifecycle-artifact bindings without trusting spec metadata. */
|
|
102
123
|
export async function computeDurableValidationBindings(input) {
|
|
103
124
|
const handoff = join(projectDataDir(input.canonicalProjectId), 'handoffs', input.specId);
|
|
104
|
-
const specHash = await digestFile(input.specPath,
|
|
125
|
+
const specHash = await digestFile(input.specPath, APPROVED_SPEC_MISSING_LABEL);
|
|
105
126
|
const [discovery, taskPlan, traceability, ...contracts] = await Promise.all([
|
|
106
127
|
readEvidenceFile(join(handoff, 'discovery.json')),
|
|
107
128
|
readEvidenceFile(join(handoff, 'task-plan.json')),
|
|
@@ -136,19 +157,76 @@ export async function computeDurableValidationBindings(input) {
|
|
|
136
157
|
},
|
|
137
158
|
artifactDigests: {
|
|
138
159
|
approvedSpec: specHash,
|
|
139
|
-
discovery: discovery
|
|
140
|
-
|
|
141
|
-
: evidenceArtifactDigest({ missing: 'discovery' }),
|
|
142
|
-
taskPlan: taskPlan
|
|
143
|
-
? evidenceArtifactDigest(taskPlan)
|
|
144
|
-
: evidenceArtifactDigest({ missing: 'task-plan' }),
|
|
160
|
+
discovery: discovery ? evidenceArtifactDigest(discovery) : absentArtifactDigest('discovery'),
|
|
161
|
+
taskPlan: taskPlan ? evidenceArtifactDigest(taskPlan) : absentArtifactDigest('task-plan'),
|
|
145
162
|
traceability: traceability
|
|
146
163
|
? evidenceArtifactDigest(traceability)
|
|
147
|
-
:
|
|
164
|
+
: absentArtifactDigest('traceability'),
|
|
148
165
|
contractValidation: evidenceArtifactCollectionDigest(contractArtifacts),
|
|
149
166
|
},
|
|
150
167
|
};
|
|
151
168
|
}
|
|
169
|
+
async function payloadFileExists(path) {
|
|
170
|
+
try {
|
|
171
|
+
await stat(path);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error.code === 'ENOENT') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function perFileArtifacts(artifactDigests) {
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
name: 'approved-spec',
|
|
185
|
+
digestValue: artifactDigests.approvedSpec,
|
|
186
|
+
sentinel: missingFileDigest(APPROVED_SPEC_MISSING_LABEL),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'discovery',
|
|
190
|
+
digestValue: artifactDigests.discovery,
|
|
191
|
+
sentinel: absentArtifactDigest('discovery'),
|
|
192
|
+
payloadFile: 'discovery.json',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'task-plan',
|
|
196
|
+
digestValue: artifactDigests.taskPlan,
|
|
197
|
+
sentinel: absentArtifactDigest('task-plan'),
|
|
198
|
+
payloadFile: 'task-plan.json',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'traceability-matrix',
|
|
202
|
+
digestValue: artifactDigests.traceability,
|
|
203
|
+
sentinel: absentArtifactDigest('traceability'),
|
|
204
|
+
payloadFile: 'traceability-matrix.json',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
async function writeProvenanceSidecars(directory, entries, identity) {
|
|
209
|
+
const absentArtifacts = [];
|
|
210
|
+
const entriesToWrite = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.digestValue === entry.sentinel) {
|
|
213
|
+
absentArtifacts.push(entry.name);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (entry.payloadFile !== undefined &&
|
|
217
|
+
!(await payloadFileExists(join(directory, entry.payloadFile)))) {
|
|
218
|
+
throw new Error(`[Planu] Validation artifact provenance digest for ${entry.name} references a payload that does not exist`);
|
|
219
|
+
}
|
|
220
|
+
entriesToWrite.push(entry);
|
|
221
|
+
}
|
|
222
|
+
const writtenPaths = [];
|
|
223
|
+
for (const entry of entriesToWrite) {
|
|
224
|
+
const path = join(directory, `${entry.name}.provenance.json`);
|
|
225
|
+
await atomicWriteFile(path, `${JSON.stringify({ ...identity, artifactDigest: entry.digestValue }, null, 2)}\n`);
|
|
226
|
+
writtenPaths.push(path);
|
|
227
|
+
}
|
|
228
|
+
return { writtenPaths, absentArtifacts };
|
|
229
|
+
}
|
|
152
230
|
/** Persist content-addressed receipt links beside machine-local lifecycle artifacts. */
|
|
153
231
|
export async function writeValidationArtifactProvenance(input) {
|
|
154
232
|
assertBindings({
|
|
@@ -163,24 +241,12 @@ export async function writeValidationArtifactProvenance(input) {
|
|
|
163
241
|
throw new Error('[Planu] Invalid validation artifact provenance identity');
|
|
164
242
|
}
|
|
165
243
|
const directory = join(projectDataDir(input.canonicalProjectId), 'handoffs', input.specId);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
];
|
|
173
|
-
const sidecars = await Promise.all(entries.map(async ([name, artifactDigest]) => {
|
|
174
|
-
const path = join(directory, `${name}.provenance.json`);
|
|
175
|
-
await atomicWriteFile(path, `${JSON.stringify({
|
|
176
|
-
receiptId: input.receiptId,
|
|
177
|
-
artifactDigest,
|
|
178
|
-
specId: input.specId,
|
|
179
|
-
issuerId: input.issuerId,
|
|
180
|
-
}, null, 2)}\n`);
|
|
181
|
-
return path;
|
|
182
|
-
}));
|
|
183
|
-
return sidecars;
|
|
244
|
+
const identity = { receiptId: input.receiptId, specId: input.specId, issuerId: input.issuerId };
|
|
245
|
+
const { writtenPaths, absentArtifacts } = await writeProvenanceSidecars(directory, perFileArtifacts(input.artifactDigests), identity);
|
|
246
|
+
const contractSidecarPath = join(directory, 'contract-validation.provenance.json');
|
|
247
|
+
await atomicWriteFile(contractSidecarPath, `${JSON.stringify({ ...identity, artifactDigest: input.artifactDigests.contractValidation }, null, 2)}\n`);
|
|
248
|
+
writtenPaths.push(contractSidecarPath);
|
|
249
|
+
return { writtenPaths, absentArtifacts };
|
|
184
250
|
}
|
|
185
251
|
/** Redact progress before it crosses the durable checkpoint boundary. */
|
|
186
252
|
export function redactValidationProgress(progress) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ValidationFreshnessLease, ValidationReceiptBindings, ValidationWatchEventClassification } from '../../types/validation-receipt.js';
|
|
1
|
+
import type { ValidationFreshnessBarrierResult, ValidationFreshnessLease, ValidationFreshnessRebaselineResult, ValidationReceiptBindings, ValidationWatchEventClassification } from '../../types/validation-receipt.js';
|
|
2
2
|
export type { ValidationFreshnessLease } from '../../types/validation-receipt.js';
|
|
3
3
|
/**
|
|
4
4
|
* Invalidate the active validation lease for a spec.
|
|
@@ -49,6 +49,7 @@ export declare function closeValidationFreshnessLease(lease: Pick<ValidationFres
|
|
|
49
49
|
/** Bind the post-watch snapshot; an event before this call makes the lease unusable. */
|
|
50
50
|
export declare function bindValidationFreshnessLease(lease: ValidationFreshnessLease, bindings: ValidationReceiptBindings): Promise<boolean>;
|
|
51
51
|
/** Drain invalidation work and verify epoch, generation, health, expiry and bound inputs. */
|
|
52
|
+
export declare function evaluateValidationFreshnessBarrier(lease: ValidationFreshnessLease, bindings?: ValidationReceiptBindings): Promise<ValidationFreshnessBarrierResult>;
|
|
52
53
|
export declare function assertValidationFreshnessBarrier(lease: ValidationFreshnessLease, bindings?: ValidationReceiptBindings): Promise<boolean>;
|
|
53
54
|
export declare function captureValidationFreshnessLease(identity: {
|
|
54
55
|
projectId: string;
|
|
@@ -59,7 +60,7 @@ export declare function rebaselineValidationFreshnessLeaseAfterRecompute(input:
|
|
|
59
60
|
lease: ValidationFreshnessLease;
|
|
60
61
|
expectedBindings: ValidationReceiptBindings;
|
|
61
62
|
recompute: () => Promise<ValidationReceiptBindings>;
|
|
62
|
-
}): Promise<
|
|
63
|
+
}): Promise<ValidationFreshnessRebaselineResult>;
|
|
63
64
|
export declare function resetValidationFreshnessForTests(): Promise<void>;
|
|
64
65
|
/** Count-only process diagnostic; intentionally absent from the package entrypoint. */
|
|
65
66
|
export declare function getValidationFreshnessDiagnosticsForTests(): {
|