@planu/cli 5.7.5 → 5.7.7
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 +88 -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-review-panel-trigger.sh +2 -3
- 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 +8 -12
- 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/evidence-skeletons.js +3 -1
- 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 +43 -5
- 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 +32 -12
- 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.d.ts +3 -3
- package/dist/engine/spec-grounding/contract.js +65 -4
- package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
- package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
- 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/storage/spec-store.js +8 -1
- package/dist/tools/bump-spec-version.js +5 -5
- 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/init-project/skills-multi-teammate-review-writer.js +1 -1
- 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/multi-teammate-review.js +1 -14
- 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-platform-tools/design-stack-tools.js +3 -3
- package/dist/tools/register-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
- package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
- 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 +72 -164
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
- package/dist/tools/tool-registry-helpers.js +6 -2
- 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 +26 -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
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
- package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const IMPLEMENTATION_CONTRACT_SECTION = "Implementation Contract";
|
|
2
2
|
/** Subsections every rendered contract must include — omitting one is a defect. */
|
|
3
|
-
export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['User Outcome'
|
|
3
|
+
export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['User Outcome'];
|
|
4
4
|
/**
|
|
5
5
|
* SPEC-1406: subsections that carry no grounded content are omitted from the rendered
|
|
6
6
|
* body rather than filled with restated-criterion filler. Their absence is not an
|
|
@@ -8,10 +8,15 @@ export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['Us
|
|
|
8
8
|
* already-persisted specs that still carry these sections (with real, non-empty
|
|
9
9
|
* content) transitionable: `evaluateImplementationContract` only requires them when
|
|
10
10
|
* the rendered body chose to include the heading.
|
|
11
|
+
*
|
|
12
|
+
* SPEC-1435: extends the same omission precedent to File-Level Work Plan,
|
|
13
|
+
* Acceptance-To-Verification Map, and Verification Commands — the renderer used to
|
|
14
|
+
* fill these with a constant "no evidence was grounded" sentence that its own quality
|
|
15
|
+
* gates could not see. Now the renderer omits the subsection instead.
|
|
11
16
|
*/
|
|
12
|
-
export declare const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS: readonly ['Behavior Contract', 'Edge Cases And Failure Modes', 'Non-Goals And Forbidden Approaches'];
|
|
17
|
+
export declare const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS: readonly ['Behavior Contract', 'Edge Cases And Failure Modes', 'Non-Goals And Forbidden Approaches', 'File-Level Work Plan', 'Acceptance-To-Verification Map', 'Verification Commands'];
|
|
13
18
|
/** Union of required + optional subsections. Kept for callers that only need the full set. */
|
|
14
|
-
export declare const IMPLEMENTATION_CONTRACT_SUBSECTIONS: readonly ["User Outcome", "
|
|
19
|
+
export declare const IMPLEMENTATION_CONTRACT_SUBSECTIONS: readonly ["User Outcome", "Behavior Contract", "Edge Cases And Failure Modes", "Non-Goals And Forbidden Approaches", "File-Level Work Plan", "Acceptance-To-Verification Map", "Verification Commands"];
|
|
15
20
|
export declare function hasImplementationContract(specBody: string): boolean;
|
|
16
21
|
export declare function extractTopLevelSection(body: string, sectionName: string): string;
|
|
17
22
|
//# sourceMappingURL=common.d.ts.map
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { escapeRegex } from '../../core/shared/strings.js';
|
|
2
2
|
export const IMPLEMENTATION_CONTRACT_SECTION = 'Implementation Contract';
|
|
3
3
|
/** Subsections every rendered contract must include — omitting one is a defect. */
|
|
4
|
-
export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = [
|
|
5
|
-
'User Outcome',
|
|
6
|
-
'File-Level Work Plan',
|
|
7
|
-
'Acceptance-To-Verification Map',
|
|
8
|
-
'Verification Commands',
|
|
9
|
-
];
|
|
4
|
+
export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = ['User Outcome'];
|
|
10
5
|
/**
|
|
11
6
|
* SPEC-1406: subsections that carry no grounded content are omitted from the rendered
|
|
12
7
|
* body rather than filled with restated-criterion filler. Their absence is not an
|
|
@@ -14,11 +9,19 @@ export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = [
|
|
|
14
9
|
* already-persisted specs that still carry these sections (with real, non-empty
|
|
15
10
|
* content) transitionable: `evaluateImplementationContract` only requires them when
|
|
16
11
|
* the rendered body chose to include the heading.
|
|
12
|
+
*
|
|
13
|
+
* SPEC-1435: extends the same omission precedent to File-Level Work Plan,
|
|
14
|
+
* Acceptance-To-Verification Map, and Verification Commands — the renderer used to
|
|
15
|
+
* fill these with a constant "no evidence was grounded" sentence that its own quality
|
|
16
|
+
* gates could not see. Now the renderer omits the subsection instead.
|
|
17
17
|
*/
|
|
18
18
|
export const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS = [
|
|
19
19
|
'Behavior Contract',
|
|
20
20
|
'Edge Cases And Failure Modes',
|
|
21
21
|
'Non-Goals And Forbidden Approaches',
|
|
22
|
+
'File-Level Work Plan',
|
|
23
|
+
'Acceptance-To-Verification Map',
|
|
24
|
+
'Verification Commands',
|
|
22
25
|
];
|
|
23
26
|
/** Union of required + optional subsections. Kept for callers that only need the full set. */
|
|
24
27
|
export const IMPLEMENTATION_CONTRACT_SUBSECTIONS = [
|
|
@@ -83,12 +83,10 @@ function evaluateSubsection(section, content) {
|
|
|
83
83
|
}
|
|
84
84
|
const trimmed = content.trim();
|
|
85
85
|
if (trimmed.length === 0) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
}
|
|
86
|
+
issues.push({
|
|
87
|
+
code: 'contract_subsection_empty',
|
|
88
|
+
message: `Implementation Contract subsection "${section}" is empty.`,
|
|
89
|
+
});
|
|
92
90
|
return issues;
|
|
93
91
|
}
|
|
94
92
|
for (const pattern of FILLER_PATTERNS) {
|
|
@@ -3,32 +3,32 @@ export function buildImplementationContractSection(input) {
|
|
|
3
3
|
const criteria = input.criteria;
|
|
4
4
|
const testFiles = input.files.test.map((file) => file.path);
|
|
5
5
|
const verificationCommands = input.verificationCommands;
|
|
6
|
+
const filePlan = renderFilePlan(input.files);
|
|
6
7
|
const verificationRows = renderVerificationRows(criteria, testFiles, verificationCommands);
|
|
7
8
|
const failureModes = renderFailureModes(criteria);
|
|
8
9
|
const nonGoals = renderNonGoals(input.outOfScope);
|
|
10
|
+
const verificationCommandLines = verificationCommands.length > 0
|
|
11
|
+
? verificationCommands.map((command) => `- \`${command}\``)
|
|
12
|
+
: criteria.length > 0
|
|
13
|
+
? []
|
|
14
|
+
: ['- Needs decision: provide a repository-supported verification command.'];
|
|
9
15
|
const lines = [
|
|
10
16
|
`## ${IMPLEMENTATION_CONTRACT_SECTION}`,
|
|
11
17
|
'### User Outcome',
|
|
12
18
|
criteria[0]?.text ??
|
|
13
19
|
'Needs decision: define the exact observable outcome this spec must deliver.',
|
|
14
20
|
'',
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// SPEC-1406 (DEFECT 5): a subsection with no grounded content is omitted rather than
|
|
22
|
-
// filled with a restated-criterion placeholder — prefer omission over filler.
|
|
21
|
+
// SPEC-1406 (DEFECT 5) / SPEC-1435: a subsection with no grounded content is omitted
|
|
22
|
+
// rather than filled with a restated-criterion placeholder — prefer omission over filler.
|
|
23
|
+
...(filePlan.length > 0 ? ['### File-Level Work Plan', ...filePlan, ''] : []),
|
|
24
|
+
...(verificationRows.length > 0
|
|
25
|
+
? ['### Acceptance-To-Verification Map', ...verificationRows, '']
|
|
26
|
+
: []),
|
|
23
27
|
...(failureModes.length > 0 ? ['### Edge Cases And Failure Modes', ...failureModes, ''] : []),
|
|
24
28
|
...(nonGoals.length > 0 ? ['### Non-Goals And Forbidden Approaches', ...nonGoals, ''] : []),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
: criteria.length > 0
|
|
29
|
-
? ['- No repository-supported verification command was grounded for this candidate.']
|
|
30
|
-
: ['- Needs decision: provide a repository-supported verification command.']),
|
|
31
|
-
'',
|
|
29
|
+
...(verificationCommandLines.length > 0
|
|
30
|
+
? ['### Verification Commands', ...verificationCommandLines, '']
|
|
31
|
+
: []),
|
|
32
32
|
];
|
|
33
33
|
return lines.join('\n');
|
|
34
34
|
}
|
|
@@ -38,7 +38,7 @@ export function appendImplementationContractIfMissing(specBody, input) {
|
|
|
38
38
|
}
|
|
39
39
|
return `${specBody.trimEnd()}\n\n${buildImplementationContractSection(input)}`;
|
|
40
40
|
}
|
|
41
|
-
function renderFilePlan(files
|
|
41
|
+
function renderFilePlan(files) {
|
|
42
42
|
const lines = [];
|
|
43
43
|
for (const [label, entries] of [
|
|
44
44
|
['Create', files.create],
|
|
@@ -49,12 +49,7 @@ function renderFilePlan(files, hasGroundedBehavior) {
|
|
|
49
49
|
lines.push(`- ${label}: \`${entry.path}\` (${entry.status})`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
return lines;
|
|
54
|
-
}
|
|
55
|
-
return hasGroundedBehavior
|
|
56
|
-
? ['- No file ownership was grounded for this behavior.']
|
|
57
|
-
: ['- Needs decision: identify expected source and test files.'];
|
|
52
|
+
return lines;
|
|
58
53
|
}
|
|
59
54
|
/**
|
|
60
55
|
* SPEC-1406 (DEFECT 1, round 2): renders the Acceptance-To-Verification Map body.
|
|
@@ -80,7 +75,7 @@ function renderVerificationRows(criteria, testFiles, commands) {
|
|
|
80
75
|
return rows;
|
|
81
76
|
}
|
|
82
77
|
if (testFiles.length === 0) {
|
|
83
|
-
return [
|
|
78
|
+
return [];
|
|
84
79
|
}
|
|
85
80
|
return renderSharedVerificationRow(testFiles, commands);
|
|
86
81
|
}
|
|
@@ -2,15 +2,15 @@ import { technologyValue } from './technology-registry.js';
|
|
|
2
2
|
import { fileExists } from '../core/shared/fs.js';
|
|
3
3
|
// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
|
|
4
4
|
// engine/project-health-checker.ts — SPEC-180 + SPEC-192 + SPEC-177: Project health check with convention scanning
|
|
5
|
-
import { execSync } from 'node:child_process';
|
|
6
5
|
import { readFile } from 'node:fs/promises';
|
|
7
6
|
import { join, relative } from 'node:path';
|
|
8
7
|
import { parseConventions, scanConventions } from './convention-scanner/index.js';
|
|
9
8
|
import { fastFindPatterns } from './core-bridge.js';
|
|
9
|
+
import { execGitCapture } from './git/exec-git.js';
|
|
10
10
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
11
11
|
function execSafe(cmd, cwd) {
|
|
12
12
|
try {
|
|
13
|
-
return
|
|
13
|
+
return execGitCapture(cmd, cwd);
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
16
|
return '';
|
|
@@ -10,11 +10,39 @@ import { findScenariosWithoutTests, parseFrontmatterScenarios, } from './validat
|
|
|
10
10
|
import { normalizeExecutableEvidence } from './validator/executable-evidence.js';
|
|
11
11
|
import { evaluateImplementationContract } from './implementation-contract/index.js';
|
|
12
12
|
import { extractNormalizedAcceptanceCriteria } from './spec-format/acceptance-criteria.js';
|
|
13
|
+
import { isRetiredScaffoldText } from './spec-format/retired-scaffold.js';
|
|
13
14
|
import { detectCrossSpecPremiseContradictions } from './contradiction-detector.js';
|
|
15
|
+
import { parseCriterionGroundingRecords, parseTechnicalReferenceGroundingRecords, } from './spec-grounding/contract.js';
|
|
14
16
|
// ── SPEC-784: Technical section quality constants ─────────────────────────────
|
|
15
17
|
const TECHNICAL_MIN_CHARS = 500;
|
|
18
|
+
const GROUNDING_FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
16
19
|
// Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
|
|
17
20
|
const TECHNICAL_PLACEHOLDER_REGEX = /\bSee\s+(?:`?[^`\n]+`?\s+)?(?:technical|spec)\.md/i;
|
|
21
|
+
const BDD_CRITERION_RE = /\bGIVEN\b[\s\S]*\bWHEN\b[\s\S]*\bTHEN\b/i;
|
|
22
|
+
function isExecutableBddLine(line) {
|
|
23
|
+
return !isRetiredScaffoldText(line) && BDD_CRITERION_RE.test(line);
|
|
24
|
+
}
|
|
25
|
+
function groundingParseDiagnosticFindings(huRaw) {
|
|
26
|
+
const frontmatter = GROUNDING_FRONTMATTER_RE.exec(huRaw)?.[1] ?? '';
|
|
27
|
+
const criteriaDiagnostics = parseCriterionGroundingRecords(frontmatter).diagnostics;
|
|
28
|
+
const technicalDiagnostics = parseTechnicalReferenceGroundingRecords(frontmatter).diagnostics;
|
|
29
|
+
return [...criteriaDiagnostics, ...technicalDiagnostics].map((diagnostic) => `GROUNDING_PARSE_DROPPED: "${diagnostic.identifier}" has an invalid ${diagnostic.field} ` +
|
|
30
|
+
`("${diagnostic.value}") — allowed: ${diagnostic.allowed.join(', ') || 'n/a'}`);
|
|
31
|
+
}
|
|
32
|
+
function checkScenariosWithoutTests(huRaw, bddScenarioCount) {
|
|
33
|
+
if (bddScenarioCount === 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return findScenariosWithoutTests(huRaw).map((title) => `scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
|
|
37
|
+
}
|
|
38
|
+
function checkZeroBddCriteria(criteriaLines) {
|
|
39
|
+
if (criteriaLines.length === 0 || criteriaLines.some(isExecutableBddLine)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
'NO_BDD_CRITERIA: no acceptance criterion parses as GIVEN/WHEN/THEN — restate at least one criterion in executable form',
|
|
44
|
+
];
|
|
45
|
+
}
|
|
18
46
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
19
47
|
function scoreHuCompleteness(spec) {
|
|
20
48
|
const blockers = [];
|
|
@@ -73,6 +101,15 @@ function countFrontmatterScenarios(raw) {
|
|
|
73
101
|
// Each scenario entry starts with " - title:" (2-space indent)
|
|
74
102
|
return (block.match(/^\s+-\s+title:/gm) ?? []).length;
|
|
75
103
|
}
|
|
104
|
+
function frontmatterScenarioScavengeWarning(bddScenarioCount, normalizedCriteria) {
|
|
105
|
+
const bodyCriteriaCount = normalizedCriteria.filter((criterion) => criterion.source === 'body').length;
|
|
106
|
+
if (bddScenarioCount === 0 || bddScenarioCount >= bodyCriteriaCount) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return (`frontmatter_scenarios_below_body_criteria: frontmatter declares ${String(bddScenarioCount)} ` +
|
|
110
|
+
`scenarios but body renders ${String(bodyCriteriaCount)} acceptance criteria — scenarios may ` +
|
|
111
|
+
`have been scavenged from prose`);
|
|
112
|
+
}
|
|
76
113
|
async function readFichaContent(spec) {
|
|
77
114
|
return readSpecTechnicalSection(spec);
|
|
78
115
|
}
|
|
@@ -418,6 +455,7 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
418
455
|
...earsBlockers,
|
|
419
456
|
];
|
|
420
457
|
const allWarnings = [...hu.warnings, ...criteria.warnings, ...files.warnings, ...deps.warnings];
|
|
458
|
+
allBlockers.push(...checkZeroBddCriteria(effectiveCriteriaLines));
|
|
421
459
|
if (mode === 'strict' && spec.scope !== 'trivial' && spec.difficulty >= 3) {
|
|
422
460
|
for (const issue of evaluateImplementationContract(huContent, canonicalCriteriaLines)) {
|
|
423
461
|
allBlockers.push(`${issue.code}: ${issue.message}`);
|
|
@@ -426,12 +464,12 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
426
464
|
// SPEC-732: Executable AC gate — block approved when any scenario lacks a `tests` array.
|
|
427
465
|
// Only enforce when the spec uses frontmatter scenarios (BDD format).
|
|
428
466
|
const bddScenarioCount = countFrontmatterScenarios(huRaw);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
467
|
+
allBlockers.push(...checkScenariosWithoutTests(huRaw, bddScenarioCount));
|
|
468
|
+
const scavengeWarning = frontmatterScenarioScavengeWarning(bddScenarioCount, normalizedCriteria);
|
|
469
|
+
if (scavengeWarning) {
|
|
470
|
+
allWarnings.push(scavengeWarning);
|
|
434
471
|
}
|
|
472
|
+
allWarnings.push(...groundingParseDiagnosticFindings(huRaw));
|
|
435
473
|
// SPEC-1214: BDD-criteria executable-evidence gate — when a spec has no frontmatter
|
|
436
474
|
// scenarios, its criteria must carry a TEST: marker mapping to an executable test file,
|
|
437
475
|
// or validation will later score 0 with "No executable scenarios declared".
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
3
3
|
import { SECTIONS_WITHOUT_LITERAL_BODY_TEXT, } from '../../types/index.js';
|
|
4
4
|
import { escapeRegex } from '../../core/shared/strings.js';
|
|
5
|
+
import { propagateMirrors } from './propagate-mirrors.js';
|
|
5
6
|
function findSectionBounds(content, section) {
|
|
6
7
|
const headingRe = new RegExp(`^(#{1,6})\\s+${escapeRegex(section)}\\s*$`, 'mi');
|
|
7
8
|
const match = headingRe.exec(content);
|
|
@@ -54,6 +55,7 @@ export async function applyChangesToSpec(specPath, changes) {
|
|
|
54
55
|
let modified = content;
|
|
55
56
|
let applied = 0;
|
|
56
57
|
const skipped = [];
|
|
58
|
+
const appliedSections = new Set();
|
|
57
59
|
for (const change of approvedTextChanges) {
|
|
58
60
|
const bounds = findSectionBounds(modified, change.section);
|
|
59
61
|
if (bounds === null) {
|
|
@@ -92,10 +94,12 @@ export async function applyChangesToSpec(specPath, changes) {
|
|
|
92
94
|
change.newValue +
|
|
93
95
|
modified.slice(absoluteIdx + change.originalValue.length);
|
|
94
96
|
applied++;
|
|
97
|
+
appliedSections.add(change.section);
|
|
95
98
|
}
|
|
96
99
|
if (applied === 0) {
|
|
97
100
|
return { applied: 0, skipped, fileWritten: false };
|
|
98
101
|
}
|
|
102
|
+
modified = propagateMirrors(modified, appliedSections);
|
|
99
103
|
await atomicWriteFile(specPath, modified);
|
|
100
104
|
return { applied, skipped, fileWritten: true };
|
|
101
105
|
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { escapeRegex } from '../../core/shared/strings.js';
|
|
2
|
+
import { normalizeCriterionText } from '../criterion-identity.js';
|
|
3
|
+
import { parseBddScenarios, renderBddScenariosYaml } from '../spec-format/bdd-parser.js';
|
|
4
|
+
import { extractSectionCriteria } from '../spec-format/acceptance-criteria.js';
|
|
5
|
+
import { extractListItems, findMarkdownSectionRange } from '../spec-format/markdown-sections.js';
|
|
6
|
+
import { parseCriterionGroundingRecords, parseTechnicalReferenceGroundingRecords, renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
|
|
7
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
|
|
8
|
+
const ACCEPTANCE_CRITERIA_HEADING = 'Acceptance Criteria';
|
|
9
|
+
const FILES_HEADING = 'Files';
|
|
10
|
+
const TECHNICAL_HEADING = 'Technical';
|
|
11
|
+
const FILE_WORK_PLAN_HEADING = 'File-Level Work Plan';
|
|
12
|
+
const ACCEPTANCE_MAP_HEADING = 'Acceptance-To-Verification Map';
|
|
13
|
+
const VERIFICATION_COMMANDS_HEADING = 'Verification Commands';
|
|
14
|
+
const VERIFICATION_STARTS_WITH_RE = /Verification starts with:\s*`([^`]+)`/i;
|
|
15
|
+
export function propagateMirrors(content, appliedSections) {
|
|
16
|
+
let result = content;
|
|
17
|
+
if (appliedSections.has(ACCEPTANCE_CRITERIA_HEADING)) {
|
|
18
|
+
result = propagateAcceptanceCriteria(result);
|
|
19
|
+
}
|
|
20
|
+
if (appliedSections.has(FILES_HEADING)) {
|
|
21
|
+
result = propagateFiles(result);
|
|
22
|
+
}
|
|
23
|
+
if (appliedSections.has(TECHNICAL_HEADING)) {
|
|
24
|
+
result = propagateTechnicalVerificationCommand(result);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function propagateAcceptanceCriteria(content) {
|
|
29
|
+
const section = findMarkdownSectionRange(content, ACCEPTANCE_CRITERIA_HEADING);
|
|
30
|
+
if (!section) {
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
const sectionBody = content.slice(section.contentStart, section.end);
|
|
34
|
+
const criteriaTexts = extractSectionCriteria(sectionBody);
|
|
35
|
+
const scenarios = criteriaTexts.map(criterionToScenario);
|
|
36
|
+
const oldRecords = mapWithFrontmatter(content, (fm) => indexCriterionRecords(parseCriterionGroundingRecords(fm).records));
|
|
37
|
+
const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
|
|
38
|
+
.records.filter((record) => record.section === 'test')
|
|
39
|
+
.map((record) => record.path));
|
|
40
|
+
const criteriaRecords = criteriaTexts.map((text) => oldRecords.get(normalizeCriterionText(text)) ?? {
|
|
41
|
+
text,
|
|
42
|
+
source: 'user_input',
|
|
43
|
+
evidence: ['reconcile_spec.changes'],
|
|
44
|
+
confidence: 'high',
|
|
45
|
+
});
|
|
46
|
+
let result = transformFrontmatter(content, (fm) => {
|
|
47
|
+
let updated = replaceFrontmatterKeyBlock(fm, 'scenarios', renderBddScenariosYaml(scenarios));
|
|
48
|
+
updated = replaceFrontmatterKeyBlock(updated, 'grounding', groundingBlockLines(criteriaRecords, technicalReferencesFromFrontmatter(fm)));
|
|
49
|
+
return updated;
|
|
50
|
+
});
|
|
51
|
+
result = withSectionBody(result, ACCEPTANCE_MAP_HEADING, (body) => syncAcceptanceMapForCriteria(body, criteriaTexts, oldTestPaths));
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function propagateFiles(content) {
|
|
55
|
+
const section = findMarkdownSectionRange(content, FILES_HEADING);
|
|
56
|
+
if (!section) {
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
const sectionBody = content.slice(section.contentStart, section.end);
|
|
60
|
+
const files = parseFilesSection(sectionBody);
|
|
61
|
+
const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
|
|
62
|
+
.records.filter((record) => record.section === 'test')
|
|
63
|
+
.map((record) => record.path));
|
|
64
|
+
const oldTechnicalReferences = mapWithFrontmatter(content, (fm) => indexTechnicalReferenceRecords(parseTechnicalReferenceGroundingRecords(fm).records));
|
|
65
|
+
const technicalReferences = buildTechnicalReferenceRecords(files, oldTechnicalReferences);
|
|
66
|
+
const newTestPaths = files.test;
|
|
67
|
+
let result = transformFrontmatter(content, (fm) => replaceFrontmatterKeyBlock(fm, 'grounding', groundingBlockLines(criteriaRecordsFromFrontmatter(fm), technicalReferences)));
|
|
68
|
+
result = replaceNamedSection(result, FILE_WORK_PLAN_HEADING, renderFileWorkPlan(files));
|
|
69
|
+
result = withSectionBody(result, ACCEPTANCE_MAP_HEADING, (body) => addVerifiedTestPathEntries(body, oldTestPaths, newTestPaths));
|
|
70
|
+
result = withSectionBody(result, VERIFICATION_COMMANDS_HEADING, (body) => syncVerificationCommandLines(body, oldTestPaths, newTestPaths));
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function propagateTechnicalVerificationCommand(content) {
|
|
74
|
+
const technicalSection = findMarkdownSectionRange(content, TECHNICAL_HEADING);
|
|
75
|
+
if (!technicalSection) {
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
const technicalBody = content.slice(technicalSection.contentStart, technicalSection.end);
|
|
79
|
+
const newCommand = VERIFICATION_STARTS_WITH_RE.exec(technicalBody)?.[1]?.trim();
|
|
80
|
+
if (!newCommand) {
|
|
81
|
+
return content;
|
|
82
|
+
}
|
|
83
|
+
const commandsSection = findMarkdownSectionRange(content, VERIFICATION_COMMANDS_HEADING);
|
|
84
|
+
if (!commandsSection) {
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
const currentBody = content.slice(commandsSection.contentStart, commandsSection.end);
|
|
88
|
+
if (currentBody.includes(newCommand)) {
|
|
89
|
+
return content;
|
|
90
|
+
}
|
|
91
|
+
return replaceNamedSection(content, VERIFICATION_COMMANDS_HEADING, [`- \`${newCommand}\``]);
|
|
92
|
+
}
|
|
93
|
+
function criterionToScenario(text) {
|
|
94
|
+
const gwtLines = text
|
|
95
|
+
.split(/\b(GIVEN|WHEN|THEN|AND)\b/)
|
|
96
|
+
.reduce((lines, part, index, parts) => {
|
|
97
|
+
if (index % 2 === 0) {
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
const keyword = part;
|
|
101
|
+
const rest = (parts[index + 1] ?? '').trim();
|
|
102
|
+
lines.push(`${keyword} ${rest}`);
|
|
103
|
+
return lines;
|
|
104
|
+
}, []);
|
|
105
|
+
const [scenario] = parseBddScenarios(gwtLines.join('\n'));
|
|
106
|
+
return scenario ?? { title: text, steps: [{ keyword: 'then', text }], done: false };
|
|
107
|
+
}
|
|
108
|
+
function indexCriterionRecords(records) {
|
|
109
|
+
const index = new Map();
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
index.set(normalizeCriterionText(record.text), record);
|
|
112
|
+
}
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
function indexTechnicalReferenceRecords(records) {
|
|
116
|
+
const index = new Map();
|
|
117
|
+
for (const record of records) {
|
|
118
|
+
index.set(record.path, record);
|
|
119
|
+
}
|
|
120
|
+
return index;
|
|
121
|
+
}
|
|
122
|
+
function parseFilesSection(sectionBody) {
|
|
123
|
+
return {
|
|
124
|
+
create: parseFilesSubsection(sectionBody, 'Create'),
|
|
125
|
+
modify: parseFilesSubsection(sectionBody, 'Modify'),
|
|
126
|
+
test: parseFilesSubsection(sectionBody, 'Test'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function parseFilesSubsection(sectionBody, heading) {
|
|
130
|
+
const range = findMarkdownSectionRange(sectionBody, heading);
|
|
131
|
+
if (!range) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const body = sectionBody.slice(range.contentStart, range.end);
|
|
135
|
+
return extractListItems(body).map((item) => item.replace(/`/g, '').trim());
|
|
136
|
+
}
|
|
137
|
+
function buildTechnicalReferenceRecords(files, oldRecords) {
|
|
138
|
+
const groups = [
|
|
139
|
+
['create', files.create],
|
|
140
|
+
['modify', files.modify],
|
|
141
|
+
['test', files.test],
|
|
142
|
+
];
|
|
143
|
+
const records = [];
|
|
144
|
+
for (const [section, paths] of groups) {
|
|
145
|
+
for (const path of paths) {
|
|
146
|
+
const existing = oldRecords.get(path);
|
|
147
|
+
records.push(existing
|
|
148
|
+
? { ...existing, section }
|
|
149
|
+
: {
|
|
150
|
+
path,
|
|
151
|
+
section,
|
|
152
|
+
source: 'user_input',
|
|
153
|
+
evidence: ['reconcile_spec.changes'],
|
|
154
|
+
confidence: 'high',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return records;
|
|
159
|
+
}
|
|
160
|
+
function renderFileWorkPlan(files) {
|
|
161
|
+
const groups = [
|
|
162
|
+
['Create', files.create],
|
|
163
|
+
['Modify', files.modify],
|
|
164
|
+
['Test', files.test],
|
|
165
|
+
];
|
|
166
|
+
return groups.flatMap(([label, paths]) => paths.map((path) => `- ${label}: \`${path}\` (pending)`));
|
|
167
|
+
}
|
|
168
|
+
function isVerifiedByLine(line, path) {
|
|
169
|
+
return /^-\s*Verified by:/i.test(line.trim()) && line.includes(path);
|
|
170
|
+
}
|
|
171
|
+
function addVerifiedTestPathEntries(body, oldPaths, newPaths) {
|
|
172
|
+
const removed = oldPaths.filter((path) => !newPaths.includes(path));
|
|
173
|
+
const added = newPaths.filter((path) => !oldPaths.includes(path));
|
|
174
|
+
if (removed.length === 0 && added.length === 0) {
|
|
175
|
+
return body;
|
|
176
|
+
}
|
|
177
|
+
let updated = removed.length === 0
|
|
178
|
+
? body
|
|
179
|
+
: body
|
|
180
|
+
.split('\n')
|
|
181
|
+
.filter((line) => !removed.some((path) => isVerifiedByLine(line, path)))
|
|
182
|
+
.join('\n');
|
|
183
|
+
if (added.length > 0) {
|
|
184
|
+
const bulletLines = added.map((path) => `- Verified by: \`${path}\``).join('\n');
|
|
185
|
+
updated = `${updated.trimEnd()}\n${bulletLines}\n`;
|
|
186
|
+
}
|
|
187
|
+
return updated;
|
|
188
|
+
}
|
|
189
|
+
function isTestRunnerCommandLine(line) {
|
|
190
|
+
return /`[^`]*\b(vitest|jest|mocha|pnpm test|npm test)\b[^`]*`/i.test(line);
|
|
191
|
+
}
|
|
192
|
+
function syncVerificationCommandLines(body, oldPaths, newPaths) {
|
|
193
|
+
const removed = oldPaths.filter((path) => !newPaths.includes(path));
|
|
194
|
+
const added = newPaths.filter((path) => !oldPaths.includes(path));
|
|
195
|
+
if (removed.length === 0 && added.length === 0) {
|
|
196
|
+
return body;
|
|
197
|
+
}
|
|
198
|
+
const lines = body.split('\n').map((line) => {
|
|
199
|
+
if (!oldPaths.some((path) => line.includes(path))) {
|
|
200
|
+
return line;
|
|
201
|
+
}
|
|
202
|
+
return substituteTestPathsInLine(line, removed, added);
|
|
203
|
+
});
|
|
204
|
+
const stillMissing = added.filter((path) => !lines.some((line) => line.includes(path)));
|
|
205
|
+
if (stillMissing.length === 0) {
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
const commandLineIndex = lines.findIndex(isTestRunnerCommandLine);
|
|
209
|
+
if (commandLineIndex !== -1) {
|
|
210
|
+
lines[commandLineIndex] = substituteTestPathsInLine(lines[commandLineIndex] ?? '', [], stillMissing);
|
|
211
|
+
return lines.join('\n');
|
|
212
|
+
}
|
|
213
|
+
while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') {
|
|
214
|
+
lines.pop();
|
|
215
|
+
}
|
|
216
|
+
return `${[...lines, ...stillMissing.map((path) => `- \`${path}\``)].join('\n')}\n`;
|
|
217
|
+
}
|
|
218
|
+
function syncAcceptanceMapForCriteria(body, criteriaTexts, testPaths) {
|
|
219
|
+
const lines = body.split('\n').filter((line) => line.trim().length > 0);
|
|
220
|
+
const kept = lines.filter((line) => criteriaTexts.some((text) => line.includes(text)));
|
|
221
|
+
const covered = new Set(kept.flatMap((line) => criteriaTexts.filter((text) => line.includes(text))));
|
|
222
|
+
const newEntries = criteriaTexts
|
|
223
|
+
.filter((text) => !covered.has(text))
|
|
224
|
+
.map((text) => renderAcceptanceMapEntry(text, testPaths));
|
|
225
|
+
const resultLines = [...kept, ...newEntries];
|
|
226
|
+
if (resultLines.length === 0) {
|
|
227
|
+
return body;
|
|
228
|
+
}
|
|
229
|
+
return `${resultLines.join('\n')}\n`;
|
|
230
|
+
}
|
|
231
|
+
function renderAcceptanceMapEntry(criterionText, testPaths) {
|
|
232
|
+
return testPaths.length > 0
|
|
233
|
+
? `- ${criterionText}: verified by ${testPaths.map((path) => `\`${path}\``).join(', ')}`
|
|
234
|
+
: `- ${criterionText}: verification pending — no test file listed yet`;
|
|
235
|
+
}
|
|
236
|
+
function substituteTestPathsInLine(line, removed, added) {
|
|
237
|
+
let updated = line;
|
|
238
|
+
for (const path of removed) {
|
|
239
|
+
updated = updated.replace(new RegExp(`\\s*${escapeRegex(path)}`), '');
|
|
240
|
+
}
|
|
241
|
+
if (added.length === 0) {
|
|
242
|
+
return updated;
|
|
243
|
+
}
|
|
244
|
+
const insertion = ` ${added.join(' ')}`;
|
|
245
|
+
if (updated.endsWith('`.')) {
|
|
246
|
+
return `${updated.slice(0, -2)}${insertion}\`.`;
|
|
247
|
+
}
|
|
248
|
+
if (updated.endsWith('`')) {
|
|
249
|
+
return `${updated.slice(0, -1)}${insertion}\``;
|
|
250
|
+
}
|
|
251
|
+
return `${updated}${insertion}`;
|
|
252
|
+
}
|
|
253
|
+
function replaceNamedSection(content, headingName, bodyLines) {
|
|
254
|
+
const range = findMarkdownSectionRange(content, headingName);
|
|
255
|
+
if (!range) {
|
|
256
|
+
return content;
|
|
257
|
+
}
|
|
258
|
+
const isLast = range.end >= content.length;
|
|
259
|
+
const body = bodyLines.join('\n');
|
|
260
|
+
const tail = isLast ? `${body}\n` : `${body}\n\n`;
|
|
261
|
+
return content.slice(0, range.contentStart) + tail + content.slice(range.end);
|
|
262
|
+
}
|
|
263
|
+
function withSectionBody(content, headingName, transform) {
|
|
264
|
+
const range = findMarkdownSectionRange(content, headingName);
|
|
265
|
+
if (!range) {
|
|
266
|
+
return content;
|
|
267
|
+
}
|
|
268
|
+
const body = content.slice(range.contentStart, range.end);
|
|
269
|
+
const updated = transform(body);
|
|
270
|
+
if (updated === body) {
|
|
271
|
+
return content;
|
|
272
|
+
}
|
|
273
|
+
return content.slice(0, range.contentStart) + updated + content.slice(range.end);
|
|
274
|
+
}
|
|
275
|
+
function transformFrontmatter(content, transform) {
|
|
276
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
277
|
+
if (!match?.[1]) {
|
|
278
|
+
return content;
|
|
279
|
+
}
|
|
280
|
+
const updated = transform(match[1]);
|
|
281
|
+
return `---\n${updated}\n---\n${content.slice(match[0].length)}`;
|
|
282
|
+
}
|
|
283
|
+
function mapWithFrontmatter(content, read) {
|
|
284
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
285
|
+
return read(match?.[1] ?? '');
|
|
286
|
+
}
|
|
287
|
+
function technicalReferencesFromFrontmatter(frontmatter) {
|
|
288
|
+
return parseTechnicalReferenceGroundingRecords(frontmatter).records;
|
|
289
|
+
}
|
|
290
|
+
function criteriaRecordsFromFrontmatter(frontmatter) {
|
|
291
|
+
return parseCriterionGroundingRecords(frontmatter).records;
|
|
292
|
+
}
|
|
293
|
+
function groundingBlockLines(criteria, technicalReferences) {
|
|
294
|
+
const criteriaLines = renderGroundingFrontmatter(criteria).slice(2);
|
|
295
|
+
const technicalReferenceLines = technicalReferences.length > 0
|
|
296
|
+
? renderTechnicalReferenceGroundingFrontmatter(technicalReferences)
|
|
297
|
+
: [];
|
|
298
|
+
return ['grounding:', ...criteriaLines, ...technicalReferenceLines];
|
|
299
|
+
}
|
|
300
|
+
function replaceFrontmatterKeyBlock(frontmatter, key, blockLines) {
|
|
301
|
+
const re = new RegExp(`^${escapeRegex(key)}:.*(?:\\n[ \\t]+.*)*`, 'm');
|
|
302
|
+
const match = re.exec(frontmatter);
|
|
303
|
+
const replacement = blockLines.join('\n');
|
|
304
|
+
if (!match) {
|
|
305
|
+
return `${frontmatter.trimEnd()}\n${replacement}`;
|
|
306
|
+
}
|
|
307
|
+
return (frontmatter.slice(0, match.index) +
|
|
308
|
+
replacement +
|
|
309
|
+
frontmatter.slice(match.index + match[0].length));
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=propagate-mirrors.js.map
|
|
@@ -3,6 +3,7 @@ import { applyTypescriptFix } from './strategies/typescript-fix.js';
|
|
|
3
3
|
import { applyLintFix } from './strategies/lint-fix.js';
|
|
4
4
|
import { applyTestFix } from './strategies/test-fix.js';
|
|
5
5
|
import { applyCoverageFix } from './strategies/coverage-fix.js';
|
|
6
|
+
import { qaCheckTimeoutMs } from '../qa-gate.js';
|
|
6
7
|
const MAX_ATTEMPTS = 3;
|
|
7
8
|
/**
|
|
8
9
|
* Maps a failure type to the appropriate fix strategy name.
|
|
@@ -24,14 +25,14 @@ export function identifyFixStrategy(failureType) {
|
|
|
24
25
|
/**
|
|
25
26
|
* Dispatches to the appropriate strategy based on strategy name.
|
|
26
27
|
*/
|
|
27
|
-
function runStrategy(strategyName, projectPath, details) {
|
|
28
|
+
function runStrategy(strategyName, projectPath, details, timeoutMs) {
|
|
28
29
|
switch (strategyName) {
|
|
29
30
|
case technologyValue('technology-typescript-969545'):
|
|
30
|
-
return applyTypescriptFix(projectPath, details);
|
|
31
|
+
return applyTypescriptFix(projectPath, details, timeoutMs);
|
|
31
32
|
case 'lint':
|
|
32
|
-
return applyLintFix(projectPath, details);
|
|
33
|
+
return applyLintFix(projectPath, details, timeoutMs);
|
|
33
34
|
case 'tests':
|
|
34
|
-
return applyTestFix(projectPath, details);
|
|
35
|
+
return applyTestFix(projectPath, details, timeoutMs);
|
|
35
36
|
case 'coverage':
|
|
36
37
|
return applyCoverageFix(projectPath, details);
|
|
37
38
|
}
|
|
@@ -47,8 +48,14 @@ export async function runHealingLoop(specId, projectPath, failureContext) {
|
|
|
47
48
|
const fixedIssues = [];
|
|
48
49
|
const remainingIssues = [];
|
|
49
50
|
let healed = false;
|
|
51
|
+
const deadline = Date.now() + qaCheckTimeoutMs();
|
|
50
52
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
51
|
-
const
|
|
53
|
+
const remainingMs = deadline - Date.now();
|
|
54
|
+
if (remainingMs <= 0) {
|
|
55
|
+
remainingIssues.push(`Attempt ${attempt}: skipped — healing wall-clock budget exhausted`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const result = await runStrategy(strategy, projectPath, failureContext.details, remainingMs);
|
|
52
59
|
if (result.fixed) {
|
|
53
60
|
attempts.push({
|
|
54
61
|
strategy,
|
|
@@ -2,5 +2,5 @@ import type { StrategyResult } from '../../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Runs eslint --fix on the project and reports what was fixed.
|
|
4
4
|
*/
|
|
5
|
-
export declare function applyLintFix(projectPath: string, _details: string): Promise<StrategyResult>;
|
|
5
|
+
export declare function applyLintFix(projectPath: string, _details: string, timeoutMs?: number): Promise<StrategyResult>;
|
|
6
6
|
//# sourceMappingURL=lint-fix.d.ts.map
|