@abelxiaoxing/cadence 1.8.3 → 2.0.0
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/README.md +324 -111
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/agents/design-explorer.md +14 -26
- package/agents/diagnosis-worker.md +14 -25
- package/agents/implementation-worker.md +16 -35
- package/config/.env.example +2 -5
- package/config/workflow-task.example.json +7 -0
- package/docs/remote-access.md +60 -0
- package/docs/reuse-and-distribution.md +44 -0
- package/docs/service-management.md +108 -0
- package/licenses/bundled-web.txt +6245 -0
- package/licenses/pi-web-ui-MIT.txt +21 -0
- package/licenses/pi-web-ui-reuse.md +22 -0
- package/package.json +94 -21
- package/src/index.ts +22 -1807
- package/src/native-build.ts +6 -0
- package/src/native-client.ts +103 -0
- package/src/native-export.ts +58 -0
- package/src/native-history.ts +359 -0
- package/src/operator-cli.mjs +453 -1937
- package/src/plan-proposal.ts +87 -0
- package/src/sdk-worker.mjs +549 -0
- package/src/service/contracts/active.ts +82 -0
- package/src/service/contracts/budget.ts +148 -0
- package/src/service/contracts/build.ts +67 -0
- package/src/service/contracts/capture.ts +48 -0
- package/src/service/contracts/core.ts +33 -0
- package/src/service/contracts/current.ts +261 -0
- package/src/service/contracts/domain.ts +68 -0
- package/src/service/contracts/history.ts +2245 -0
- package/src/service/contracts/models.ts +139 -0
- package/src/service/coordination.ts +862 -0
- package/src/service/private-files.ts +204 -0
- package/src/service-cli.mjs +5592 -0
- package/src/service-client.ts +747 -0
- package/src/service-contract.ts +245 -0
- package/src/service-host.mjs +22382 -0
- package/src/service-http-client.ts +425 -0
- package/src/service-wire.ts +372 -0
- package/src/task-shell.ts +1045 -0
- package/src/task-store.ts +479 -0
- package/src/task-types.ts +184 -0
- package/src/verification-diagnostics.ts +29 -72
- package/src/worker-capture.mjs +1197 -0
- package/src/worker-public.mjs +1753 -0
- package/src/worker-redaction.mjs +608 -0
- package/src/worker-tools.mjs +202 -0
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/cadence.css +2 -0
- package/web/dist/assets/cadence.js +7 -0
- package/web/dist/assets/markdown.js +296 -0
- package/web/dist/assets/react.js +9 -0
- package/web/dist/assets/rolldown-runtime.js +1 -0
- package/web/dist/assets/timeline-client.js +41 -0
- package/web/dist/assets/vis-timeline.css +1274 -0
- package/web/dist/assets/vis-timeline.js +32 -0
- package/web/dist/index.html +18 -0
- package/web/dist/manifest.json +505 -0
- package/config/plan-draft.example.json +0 -125
- package/config/plan-draft.multiple-tasks.example.json +0 -375
- package/config/plan-draft.quick.example.json +0 -78
- package/config/routes.example.json +0 -39
- package/prompts/abel-design.md +0 -382
- package/prompts/abel-diagnose.md +0 -71
- package/prompts/abel-implement.md +0 -284
- package/prompts/abel-init.md +0 -52
- package/src/activation.ts +0 -44
- package/src/activity-contracts.ts +0 -82
- package/src/agent-registry.ts +0 -53
- package/src/apply-transaction.ts +0 -1437
- package/src/artifact-store.ts +0 -213
- package/src/build-windows-job.mjs +0 -95
- package/src/candidate-artifact.ts +0 -143
- package/src/candidate-patch.ts +0 -271
- package/src/candidate-workspace.ts +0 -80
- package/src/canonical.ts +0 -16
- package/src/change-contract-schema.ts +0 -191
- package/src/change-contract.ts +0 -324
- package/src/change-verification.ts +0 -1617
- package/src/child-budget.ts +0 -58
- package/src/child-model.ts +0 -142
- package/src/child-session.ts +0 -640
- package/src/contracts.ts +0 -2476
- package/src/control-contracts.ts +0 -382
- package/src/delivery-compiler.ts +0 -1805
- package/src/delivery-revision.ts +0 -42
- package/src/design-control.ts +0 -1299
- package/src/design-diagnostics.ts +0 -181
- package/src/design-journal.ts +0 -1773
- package/src/directory-sync.ts +0 -36
- package/src/durable-contracts.ts +0 -231
- package/src/durable-workflow.ts +0 -978
- package/src/evidence-draft.ts +0 -122
- package/src/execution-profile.ts +0 -64
- package/src/execution-retention.ts +0 -77
- package/src/file-snapshot.ts +0 -163
- package/src/implement-continuation.ts +0 -559
- package/src/implement-graph.ts +0 -690
- package/src/implement-plan.ts +0 -72
- package/src/install-config.d.mts +0 -6
- package/src/isolation-backend.ts +0 -353
- package/src/model-source.ts +0 -22
- package/src/openspec-cli.ts +0 -494
- package/src/operator-cli-entry.mjs +0 -24
- package/src/operator-tools.ts +0 -309
- package/src/package-candidate.ts +0 -227
- package/src/package-delivery.ts +0 -486
- package/src/package-state.ts +0 -320
- package/src/package-verification.ts +0 -635
- package/src/package-workflow.ts +0 -342
- package/src/packet-runtime.ts +0 -570
- package/src/parent-provider.ts +0 -375
- package/src/phase-execution.ts +0 -2259
- package/src/pi-adapter.ts +0 -19
- package/src/plan-draft-summary.ts +0 -527
- package/src/plan-draft.ts +0 -502
- package/src/remove-path.ts +0 -32
- package/src/route-policy.ts +0 -545
- package/src/run-state.ts +0 -420
- package/src/run-store.ts +0 -1179
- package/src/safe-path.ts +0 -77
- package/src/scoped-grep-worker.mjs +0 -23
- package/src/scoped-tools.ts +0 -775
- package/src/single-task-draft.ts +0 -142
- package/src/sqlite-schema.ts +0 -171
- package/src/state-root.ts +0 -215
- package/src/storage-schema.ts +0 -360
- package/src/subagent-activity.ts +0 -1417
- package/src/submit-tool.ts +0 -950
- package/src/task-ledger.ts +0 -1338
- package/src/transport-budget.ts +0 -52
- package/src/verification-capability.ts +0 -1191
- package/src/verification-environment-io.ts +0 -389
- package/src/verification-environment.ts +0 -162
- package/src/verification-identity.ts +0 -77
- package/src/verification-scan.ts +0 -64
- package/src/windows-job-backend.ts +0 -419
- package/src/windows-job.c +0 -149
- package/src/worker-broker.ts +0 -806
- package/src/workflow-engine.ts +0 -28
- package/src/workflow-policy.ts +0 -1679
- package/src/workflow-recovery-policy.ts +0 -97
- package/src/workflow-scheduling.ts +0 -105
- package/src/workflow-state-machine.ts +0 -4355
- package/src/workflow-status.ts +0 -576
- package/src/workspace-io-entry.mjs +0 -76
- package/src/workspace-io-worker.mjs +0 -1312
- package/src/workspace-io.ts +0 -89
- package/src/workspace-store.ts +0 -634
package/src/delivery-compiler.ts
DELETED
|
@@ -1,1805 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { compareCanonicalStrings } from "./canonical.ts";
|
|
5
|
-
import {
|
|
6
|
-
assertPlanWithinChangeContract,
|
|
7
|
-
normalizeChangeContract,
|
|
8
|
-
} from "./change-contract.ts";
|
|
9
|
-
import {
|
|
10
|
-
type AgentsImpact,
|
|
11
|
-
type ImplementGraphBoundary,
|
|
12
|
-
type ImplementGraphOutput,
|
|
13
|
-
isAgentsPath,
|
|
14
|
-
isValidRelativePath,
|
|
15
|
-
type PhaseBoundary,
|
|
16
|
-
type StructuredVerificationContract,
|
|
17
|
-
validateImplementGraphBoundary,
|
|
18
|
-
validateVerificationContract,
|
|
19
|
-
verificationInputPaths,
|
|
20
|
-
verificationSteps,
|
|
21
|
-
} from "./contracts.ts";
|
|
22
|
-
import {
|
|
23
|
-
type DesignPlanDiagnostic,
|
|
24
|
-
DesignPlanValidationError,
|
|
25
|
-
} from "./design-diagnostics.ts";
|
|
26
|
-
import {
|
|
27
|
-
assessImplementGraphReadiness,
|
|
28
|
-
canonicalJson,
|
|
29
|
-
hashCanonicalValue,
|
|
30
|
-
type ImplementGraphReadiness,
|
|
31
|
-
} from "./implement-graph.ts";
|
|
32
|
-
import type {
|
|
33
|
-
ImplementPlan,
|
|
34
|
-
PlanAgentsCheckpointOperation,
|
|
35
|
-
PlanTaskDraft,
|
|
36
|
-
PlanTracking,
|
|
37
|
-
PlanVerification,
|
|
38
|
-
} from "./implement-plan.ts";
|
|
39
|
-
import { expandPlanDraft, preparePlanDraft } from "./plan-draft.ts";
|
|
40
|
-
import { isSafeRegularFile, observeSafePath } from "./safe-path.ts";
|
|
41
|
-
import { expandSingleTaskDraft } from "./single-task-draft.ts";
|
|
42
|
-
import {
|
|
43
|
-
bindDraftVerificationInputs,
|
|
44
|
-
validateVerificationAdapterCapability,
|
|
45
|
-
} from "./verification-capability.ts";
|
|
46
|
-
|
|
47
|
-
export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
|
|
48
|
-
export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
|
|
49
|
-
export const READY_RECEIPT_PATH = "ready.yaml" as const;
|
|
50
|
-
|
|
51
|
-
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
52
|
-
const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
53
|
-
const AGENTS_START = "<!-- ABEL:AGENTS-INDEX:START -->";
|
|
54
|
-
const AGENTS_END = "<!-- ABEL:AGENTS-INDEX:END -->";
|
|
55
|
-
|
|
56
|
-
export interface DeliveryArtifactBinding {
|
|
57
|
-
path: string;
|
|
58
|
-
rawSha256: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface GateApprovalProof {
|
|
62
|
-
revision: number;
|
|
63
|
-
contractHash: string;
|
|
64
|
-
recordHash: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface GateAReceipt {
|
|
68
|
-
change: string;
|
|
69
|
-
schema: string;
|
|
70
|
-
approval: GateApprovalProof;
|
|
71
|
-
artifacts: DeliveryArtifactBinding[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface DeliveryTraceability {
|
|
75
|
-
taskPath: "tasks.md";
|
|
76
|
-
referenceCount: number;
|
|
77
|
-
taskCount: number;
|
|
78
|
-
verificationCount: number;
|
|
79
|
-
canonicalHash: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface ReadyReceipt {
|
|
83
|
-
change: string;
|
|
84
|
-
schema: string;
|
|
85
|
-
deliveryRevision: number;
|
|
86
|
-
approvals: {
|
|
87
|
-
gateA: {
|
|
88
|
-
path: typeof GATE_A_RECEIPT_PATH;
|
|
89
|
-
rawSha256: string;
|
|
90
|
-
};
|
|
91
|
-
gateB: {
|
|
92
|
-
revision: number;
|
|
93
|
-
contractHash: string;
|
|
94
|
-
recordHash: string;
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
artifacts: DeliveryArtifactBinding[];
|
|
98
|
-
plan: CompiledDelivery["receipt"]["plan"];
|
|
99
|
-
verificationClosure: ImplementGraphReadiness["closure"];
|
|
100
|
-
traceability: DeliveryTraceability;
|
|
101
|
-
openspec: {
|
|
102
|
-
strict: true;
|
|
103
|
-
planningComplete: true;
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export type DeliveryTraceabilityAssessment =
|
|
108
|
-
| { ok: true; value: DeliveryTraceability }
|
|
109
|
-
| { ok: false; diagnostics: string[] };
|
|
110
|
-
|
|
111
|
-
export class DeliveryValidationError extends Error {
|
|
112
|
-
readonly diagnostics: readonly string[];
|
|
113
|
-
|
|
114
|
-
constructor(diagnostics: readonly string[]) {
|
|
115
|
-
const normalized = [...new Set(diagnostics)].sort();
|
|
116
|
-
super("delivery-invalid");
|
|
117
|
-
this.name = "DeliveryValidationError";
|
|
118
|
-
this.diagnostics = Object.freeze(normalized);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
123
|
-
|
|
124
|
-
function planErrorCode(error: unknown): string {
|
|
125
|
-
const message = error instanceof Error ? error.message : "";
|
|
126
|
-
return SAFE_PLAN_ERROR_CODE.test(message) ? message : "delivery-plan-invalid";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function planDiagnosticField(code: string): string | undefined {
|
|
130
|
-
const fields: Record<string, string> = {
|
|
131
|
-
"delivery-plan-invalid-task": "task",
|
|
132
|
-
"delivery-plan-invalid-task-phases": "phases",
|
|
133
|
-
"delivery-plan-invalid-phase": "phases",
|
|
134
|
-
"delivery-plan-empty-phase-boundary": "phases",
|
|
135
|
-
"delivery-plan-write-delete-overlap": "phases",
|
|
136
|
-
"delivery-verification-invalid": "verification",
|
|
137
|
-
"delivery-task-verification-invalid": "affectedVerification",
|
|
138
|
-
"delivery-agents-checkpoint-mismatch": "verification.agentsCheckpoint",
|
|
139
|
-
};
|
|
140
|
-
return fields[code];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function asPlanValidationError(
|
|
144
|
-
error: unknown,
|
|
145
|
-
context: Omit<DesignPlanDiagnostic, "code"> = {},
|
|
146
|
-
): DesignPlanValidationError {
|
|
147
|
-
if (error instanceof DesignPlanValidationError) {
|
|
148
|
-
const diagnostics = error.diagnostics.map((diagnostic) => ({
|
|
149
|
-
...context,
|
|
150
|
-
...diagnostic,
|
|
151
|
-
}));
|
|
152
|
-
return new DesignPlanValidationError(error.message, diagnostics);
|
|
153
|
-
}
|
|
154
|
-
const code = planErrorCode(error);
|
|
155
|
-
const field = planDiagnosticField(code);
|
|
156
|
-
return new DesignPlanValidationError(code, [
|
|
157
|
-
{ code, ...context, ...(field ? { field } : {}) },
|
|
158
|
-
]);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function assessDeliveryTraceability(input: {
|
|
162
|
-
tasksMarkdown: string;
|
|
163
|
-
specs: readonly { path: string; text: string }[];
|
|
164
|
-
plan: ImplementPlan;
|
|
165
|
-
}): DeliveryTraceabilityAssessment {
|
|
166
|
-
const diagnostics = new Set<string>();
|
|
167
|
-
let authorMarkdown = input.tasksMarkdown;
|
|
168
|
-
let bindings: string | undefined;
|
|
169
|
-
try {
|
|
170
|
-
const region = taskVerificationRegion(input.tasksMarkdown);
|
|
171
|
-
if (region) {
|
|
172
|
-
authorMarkdown =
|
|
173
|
-
input.tasksMarkdown.slice(0, region.start) +
|
|
174
|
-
input.tasksMarkdown.slice(region.end);
|
|
175
|
-
bindings = region.text;
|
|
176
|
-
if (bindings !== renderTasks(input.plan)) {
|
|
177
|
-
diagnostics.add("traceability-managed-region-stale");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
|
|
182
|
-
}
|
|
183
|
-
const headings = new Set<string>();
|
|
184
|
-
for (const spec of input.specs) {
|
|
185
|
-
if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
|
|
186
|
-
diagnostics.add("traceability-spec-path-invalid");
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
let requirement = "";
|
|
190
|
-
for (const line of spec.text.split(/\r?\n/u)) {
|
|
191
|
-
const requirementMatch = /^### Requirement: (.+)$/u.exec(line);
|
|
192
|
-
if (requirementMatch?.[1]) {
|
|
193
|
-
requirement = requirementMatch[1].trim();
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const scenarioMatch = /^#### Scenario: (.+)$/u.exec(line);
|
|
197
|
-
if (scenarioMatch?.[1] && requirement) {
|
|
198
|
-
const heading = `${spec.path}#${requirement}/${scenarioMatch[1].trim()}`;
|
|
199
|
-
if (headings.has(heading)) {
|
|
200
|
-
diagnostics.add("traceability-heading-duplicate");
|
|
201
|
-
}
|
|
202
|
-
headings.add(heading);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const references = [
|
|
207
|
-
...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
|
|
208
|
-
].flatMap((match) => (match[1] ? [match[1].trim()] : []));
|
|
209
|
-
if (references.length === 0) diagnostics.add("traceability-reference-empty");
|
|
210
|
-
const referenceSet = new Set(references);
|
|
211
|
-
if (referenceSet.size !== references.length) {
|
|
212
|
-
diagnostics.add("traceability-reference-duplicate");
|
|
213
|
-
}
|
|
214
|
-
if (references.some((reference) => !headings.has(reference))) {
|
|
215
|
-
diagnostics.add("traceability-reference-unresolved");
|
|
216
|
-
}
|
|
217
|
-
if ([...headings].some((heading) => !referenceSet.has(heading))) {
|
|
218
|
-
diagnostics.add("traceability-scenario-unowned");
|
|
219
|
-
}
|
|
220
|
-
const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
|
|
221
|
-
if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
|
|
222
|
-
diagnostics.add("traceability-task-unmapped");
|
|
223
|
-
}
|
|
224
|
-
const verificationIds = input.plan.tasks
|
|
225
|
-
.flatMap((task) => [
|
|
226
|
-
...(task.verificationMode && task.verificationMode !== "behavior"
|
|
227
|
-
? []
|
|
228
|
-
: [task.phases.red.verification.id]),
|
|
229
|
-
task.phases.green.verification.id,
|
|
230
|
-
...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
|
|
231
|
-
])
|
|
232
|
-
.sort();
|
|
233
|
-
if (
|
|
234
|
-
verificationIds.some(
|
|
235
|
-
(verificationId) =>
|
|
236
|
-
!(bindings ?? authorMarkdown).includes(verificationId),
|
|
237
|
-
)
|
|
238
|
-
) {
|
|
239
|
-
diagnostics.add("traceability-verification-unmapped");
|
|
240
|
-
}
|
|
241
|
-
if (diagnostics.size > 0) {
|
|
242
|
-
return { ok: false, diagnostics: [...diagnostics].sort() };
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
ok: true,
|
|
246
|
-
value: {
|
|
247
|
-
taskPath: "tasks.md",
|
|
248
|
-
referenceCount: references.length,
|
|
249
|
-
taskCount: taskIds.length,
|
|
250
|
-
verificationCount: verificationIds.length,
|
|
251
|
-
canonicalHash: hashCanonicalValue({
|
|
252
|
-
references: [...referenceSet].sort(),
|
|
253
|
-
taskIds,
|
|
254
|
-
verificationIds,
|
|
255
|
-
}),
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
261
|
-
export { DesignPlanValidationError } from "./design-diagnostics.ts";
|
|
262
|
-
export type {
|
|
263
|
-
ImplementPlan,
|
|
264
|
-
PlanAgentsCheckpointOperation,
|
|
265
|
-
PlanTaskDraft,
|
|
266
|
-
PlanTracking,
|
|
267
|
-
PlanVerification,
|
|
268
|
-
} from "./implement-plan.ts";
|
|
269
|
-
export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
|
|
270
|
-
|
|
271
|
-
export interface CompiledDelivery {
|
|
272
|
-
/** Static checks only: neither compilation nor preflight seals a delivery. */
|
|
273
|
-
checks: {
|
|
274
|
-
structure: "passed";
|
|
275
|
-
verificationCapability: "passed";
|
|
276
|
-
contractCoverage: "passed" | "not-checked";
|
|
277
|
-
sealing: "not-performed";
|
|
278
|
-
};
|
|
279
|
-
plan: ImplementPlan;
|
|
280
|
-
bytes: Uint8Array;
|
|
281
|
-
rawSha256: string;
|
|
282
|
-
planHash: string;
|
|
283
|
-
closure: ImplementGraphReadiness["closure"];
|
|
284
|
-
receipt: {
|
|
285
|
-
plan: {
|
|
286
|
-
path: typeof IMPLEMENT_PLAN_PATH;
|
|
287
|
-
rawSha256: string;
|
|
288
|
-
canonicalHash: string;
|
|
289
|
-
};
|
|
290
|
-
verificationClosure: ImplementGraphReadiness["closure"];
|
|
291
|
-
};
|
|
292
|
-
tasksMarkdown: string;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
296
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function exactKeys(
|
|
300
|
-
value: Record<string, unknown>,
|
|
301
|
-
keys: readonly string[],
|
|
302
|
-
): boolean {
|
|
303
|
-
const actual = Object.keys(value);
|
|
304
|
-
return (
|
|
305
|
-
actual.length === keys.length && actual.every((key) => keys.includes(key))
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function canonicalBytes(value: unknown): Uint8Array {
|
|
310
|
-
return new TextEncoder().encode(`${canonicalJson(value)}\n`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function normalizeArtifactBindings(
|
|
314
|
-
value: readonly DeliveryArtifactBinding[],
|
|
315
|
-
): DeliveryArtifactBinding[] {
|
|
316
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
317
|
-
throw new Error("delivery-artifacts-invalid");
|
|
318
|
-
}
|
|
319
|
-
const paths = new Set<string>();
|
|
320
|
-
const normalized = value.map((binding) => {
|
|
321
|
-
if (
|
|
322
|
-
!isRecord(binding) ||
|
|
323
|
-
!exactKeys(binding, ["path", "rawSha256"]) ||
|
|
324
|
-
typeof binding.path !== "string" ||
|
|
325
|
-
!isValidRelativePath(binding.path) ||
|
|
326
|
-
binding.path === READY_RECEIPT_PATH ||
|
|
327
|
-
typeof binding.rawSha256 !== "string" ||
|
|
328
|
-
!SHA256.test(binding.rawSha256) ||
|
|
329
|
-
paths.has(binding.path)
|
|
330
|
-
) {
|
|
331
|
-
throw new Error("delivery-artifacts-invalid");
|
|
332
|
-
}
|
|
333
|
-
paths.add(binding.path);
|
|
334
|
-
return { path: binding.path, rawSha256: binding.rawSha256 };
|
|
335
|
-
});
|
|
336
|
-
return normalized.sort((left, right) =>
|
|
337
|
-
compareCanonicalStrings(left.path, right.path),
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function requireReceiptIdentity(
|
|
342
|
-
change: unknown,
|
|
343
|
-
schema: unknown,
|
|
344
|
-
): asserts change is string {
|
|
345
|
-
if (
|
|
346
|
-
typeof change !== "string" ||
|
|
347
|
-
!CHANGE_NAME.test(change) ||
|
|
348
|
-
typeof schema !== "string" ||
|
|
349
|
-
schema.length === 0 ||
|
|
350
|
-
schema.length > 128
|
|
351
|
-
) {
|
|
352
|
-
throw new Error("delivery-receipt-identity-invalid");
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
|
|
357
|
-
if (
|
|
358
|
-
!isRecord(value) ||
|
|
359
|
-
!exactKeys(value, ["revision", "contractHash", "recordHash"]) ||
|
|
360
|
-
!Number.isSafeInteger(value.revision) ||
|
|
361
|
-
value.revision < 1 ||
|
|
362
|
-
typeof value.contractHash !== "string" ||
|
|
363
|
-
!SHA256.test(value.contractHash) ||
|
|
364
|
-
typeof value.recordHash !== "string" ||
|
|
365
|
-
!SHA256.test(value.recordHash)
|
|
366
|
-
) {
|
|
367
|
-
throw new Error("delivery-gate-proof-invalid");
|
|
368
|
-
}
|
|
369
|
-
return structuredClone(value);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export interface DeliveryReadOptions {
|
|
373
|
-
/** Only the proof-bound delivery loader may admit historical collection ordering. */
|
|
374
|
-
allowLegacyOrder?: boolean;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const legacyPlans = new WeakMap<object, string>();
|
|
378
|
-
|
|
379
|
-
function unorderedIdentity(value: unknown): string {
|
|
380
|
-
if (Array.isArray(value))
|
|
381
|
-
return `[${value.map(unorderedIdentity).sort().join(",")}]`;
|
|
382
|
-
if (isRecord(value))
|
|
383
|
-
return `{${Object.entries(value)
|
|
384
|
-
.sort(([a], [b]) => compareCanonicalStrings(a, b))
|
|
385
|
-
.map(
|
|
386
|
-
([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
|
|
387
|
-
)
|
|
388
|
-
.join(",")}}`;
|
|
389
|
-
return JSON.stringify(value) ?? "null";
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function sameNormalized(
|
|
393
|
-
left: unknown,
|
|
394
|
-
right: unknown,
|
|
395
|
-
options: DeliveryReadOptions,
|
|
396
|
-
): boolean {
|
|
397
|
-
return options.allowLegacyOrder
|
|
398
|
-
? unorderedIdentity(left) === unorderedIdentity(right)
|
|
399
|
-
: canonicalJson(left) === canonicalJson(right);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function parseCanonicalReceipt<T>(
|
|
403
|
-
bytes: Uint8Array,
|
|
404
|
-
code: string,
|
|
405
|
-
options: DeliveryReadOptions = {},
|
|
406
|
-
): T {
|
|
407
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
408
|
-
let parsed: unknown;
|
|
409
|
-
try {
|
|
410
|
-
parsed = JSON.parse(text);
|
|
411
|
-
} catch {
|
|
412
|
-
throw new Error(code);
|
|
413
|
-
}
|
|
414
|
-
if (
|
|
415
|
-
`${canonicalJson(parsed)}\n` !== text &&
|
|
416
|
-
(!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
|
|
417
|
-
)
|
|
418
|
-
throw new Error(code);
|
|
419
|
-
return parsed as T;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export function compileGateAReceipt(input: {
|
|
423
|
-
change: string;
|
|
424
|
-
schema: string;
|
|
425
|
-
approval: GateApprovalProof;
|
|
426
|
-
artifacts: readonly DeliveryArtifactBinding[];
|
|
427
|
-
}): { receipt: GateAReceipt; bytes: Uint8Array; rawSha256: string } {
|
|
428
|
-
requireReceiptIdentity(input.change, input.schema);
|
|
429
|
-
const receipt: GateAReceipt = {
|
|
430
|
-
change: input.change,
|
|
431
|
-
schema: input.schema,
|
|
432
|
-
approval: normalizeGateProof(input.approval),
|
|
433
|
-
artifacts: normalizeArtifactBindings(input.artifacts),
|
|
434
|
-
};
|
|
435
|
-
const bytes = canonicalBytes(receipt);
|
|
436
|
-
return {
|
|
437
|
-
receipt,
|
|
438
|
-
bytes,
|
|
439
|
-
rawSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function parseGateAReceipt(
|
|
444
|
-
bytes: Uint8Array,
|
|
445
|
-
options: DeliveryReadOptions = {},
|
|
446
|
-
): GateAReceipt {
|
|
447
|
-
const receipt = parseCanonicalReceipt<GateAReceipt>(
|
|
448
|
-
bytes,
|
|
449
|
-
"gate-a-receipt-invalid",
|
|
450
|
-
options,
|
|
451
|
-
);
|
|
452
|
-
if (
|
|
453
|
-
!isRecord(receipt) ||
|
|
454
|
-
!exactKeys(receipt, ["change", "schema", "approval", "artifacts"]) ||
|
|
455
|
-
!isRecord(receipt.approval) ||
|
|
456
|
-
!exactKeys(receipt.approval, ["revision", "contractHash", "recordHash"])
|
|
457
|
-
) {
|
|
458
|
-
throw new Error("gate-a-receipt-invalid");
|
|
459
|
-
}
|
|
460
|
-
requireReceiptIdentity(receipt.change, receipt.schema);
|
|
461
|
-
const approval = normalizeGateProof(receipt.approval);
|
|
462
|
-
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
463
|
-
if (
|
|
464
|
-
!sameNormalized(approval, receipt.approval, options) ||
|
|
465
|
-
!sameNormalized(artifacts, receipt.artifacts, options)
|
|
466
|
-
) {
|
|
467
|
-
throw new Error("gate-a-receipt-invalid");
|
|
468
|
-
}
|
|
469
|
-
return structuredClone(receipt);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function normalizeTraceability(
|
|
473
|
-
value: DeliveryTraceability,
|
|
474
|
-
): DeliveryTraceability {
|
|
475
|
-
if (
|
|
476
|
-
!isRecord(value) ||
|
|
477
|
-
!exactKeys(value, [
|
|
478
|
-
"taskPath",
|
|
479
|
-
"referenceCount",
|
|
480
|
-
"taskCount",
|
|
481
|
-
"verificationCount",
|
|
482
|
-
"canonicalHash",
|
|
483
|
-
]) ||
|
|
484
|
-
value.taskPath !== "tasks.md" ||
|
|
485
|
-
!Number.isSafeInteger(value.referenceCount) ||
|
|
486
|
-
value.referenceCount < 1 ||
|
|
487
|
-
!Number.isSafeInteger(value.taskCount) ||
|
|
488
|
-
value.taskCount < 1 ||
|
|
489
|
-
!Number.isSafeInteger(value.verificationCount) ||
|
|
490
|
-
value.verificationCount < 1 ||
|
|
491
|
-
typeof value.canonicalHash !== "string" ||
|
|
492
|
-
!SHA256.test(value.canonicalHash)
|
|
493
|
-
) {
|
|
494
|
-
throw new Error("delivery-traceability-invalid");
|
|
495
|
-
}
|
|
496
|
-
return structuredClone(value);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export function compileReadyReceipt(input: {
|
|
500
|
-
change: string;
|
|
501
|
-
schema: string;
|
|
502
|
-
deliveryRevision: number;
|
|
503
|
-
gateA: { rawSha256: string };
|
|
504
|
-
gateB: GateApprovalProof;
|
|
505
|
-
artifacts: readonly DeliveryArtifactBinding[];
|
|
506
|
-
compiledPlan: CompiledDelivery;
|
|
507
|
-
traceability: DeliveryTraceability;
|
|
508
|
-
}): { receipt: ReadyReceipt; bytes: Uint8Array; rawSha256: string } {
|
|
509
|
-
requireReceiptIdentity(input.change, input.schema);
|
|
510
|
-
if (
|
|
511
|
-
!Number.isSafeInteger(input.deliveryRevision) ||
|
|
512
|
-
input.deliveryRevision < 1 ||
|
|
513
|
-
!SHA256.test(input.gateA.rawSha256) ||
|
|
514
|
-
input.compiledPlan.plan.changeId !== input.change ||
|
|
515
|
-
input.gateB.contractHash !== input.compiledPlan.planHash
|
|
516
|
-
) {
|
|
517
|
-
throw new Error("delivery-receipt-invalid");
|
|
518
|
-
}
|
|
519
|
-
const receipt: ReadyReceipt = {
|
|
520
|
-
change: input.change,
|
|
521
|
-
schema: input.schema,
|
|
522
|
-
deliveryRevision: input.deliveryRevision,
|
|
523
|
-
approvals: {
|
|
524
|
-
gateA: {
|
|
525
|
-
path: GATE_A_RECEIPT_PATH,
|
|
526
|
-
rawSha256: input.gateA.rawSha256,
|
|
527
|
-
},
|
|
528
|
-
gateB: normalizeGateProof(input.gateB),
|
|
529
|
-
},
|
|
530
|
-
artifacts: normalizeArtifactBindings(input.artifacts),
|
|
531
|
-
plan: structuredClone(input.compiledPlan.receipt.plan),
|
|
532
|
-
verificationClosure: structuredClone(
|
|
533
|
-
input.compiledPlan.receipt.verificationClosure,
|
|
534
|
-
),
|
|
535
|
-
traceability: normalizeTraceability(input.traceability),
|
|
536
|
-
openspec: { strict: true, planningComplete: true },
|
|
537
|
-
};
|
|
538
|
-
const bytes = canonicalBytes(receipt);
|
|
539
|
-
return {
|
|
540
|
-
receipt,
|
|
541
|
-
bytes,
|
|
542
|
-
rawSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
export function parseReadyReceipt(
|
|
547
|
-
bytes: Uint8Array,
|
|
548
|
-
options: DeliveryReadOptions = {},
|
|
549
|
-
): ReadyReceipt {
|
|
550
|
-
const receipt = parseCanonicalReceipt<ReadyReceipt>(
|
|
551
|
-
bytes,
|
|
552
|
-
"delivery-receipt-invalid",
|
|
553
|
-
options,
|
|
554
|
-
);
|
|
555
|
-
if (
|
|
556
|
-
!isRecord(receipt) ||
|
|
557
|
-
!exactKeys(receipt, [
|
|
558
|
-
"change",
|
|
559
|
-
"schema",
|
|
560
|
-
"deliveryRevision",
|
|
561
|
-
"approvals",
|
|
562
|
-
"artifacts",
|
|
563
|
-
"plan",
|
|
564
|
-
"verificationClosure",
|
|
565
|
-
"traceability",
|
|
566
|
-
"openspec",
|
|
567
|
-
]) ||
|
|
568
|
-
!Number.isSafeInteger(receipt.deliveryRevision) ||
|
|
569
|
-
receipt.deliveryRevision < 1 ||
|
|
570
|
-
!isRecord(receipt.approvals) ||
|
|
571
|
-
!exactKeys(receipt.approvals, ["gateA", "gateB"]) ||
|
|
572
|
-
!isRecord(receipt.approvals.gateA) ||
|
|
573
|
-
!exactKeys(receipt.approvals.gateA, ["path", "rawSha256"]) ||
|
|
574
|
-
receipt.approvals.gateA.path !== GATE_A_RECEIPT_PATH ||
|
|
575
|
-
!SHA256.test(receipt.approvals.gateA.rawSha256) ||
|
|
576
|
-
!isRecord(receipt.approvals.gateB) ||
|
|
577
|
-
!exactKeys(receipt.approvals.gateB, [
|
|
578
|
-
"revision",
|
|
579
|
-
"contractHash",
|
|
580
|
-
"recordHash",
|
|
581
|
-
]) ||
|
|
582
|
-
!isRecord(receipt.plan) ||
|
|
583
|
-
!exactKeys(receipt.plan, ["path", "rawSha256", "canonicalHash"]) ||
|
|
584
|
-
receipt.plan.path !== IMPLEMENT_PLAN_PATH ||
|
|
585
|
-
!SHA256.test(receipt.plan.rawSha256) ||
|
|
586
|
-
!SHA256.test(receipt.plan.canonicalHash) ||
|
|
587
|
-
!isRecord(receipt.verificationClosure) ||
|
|
588
|
-
!exactKeys(receipt.verificationClosure, ["executable", "diagnostics"]) ||
|
|
589
|
-
receipt.verificationClosure.executable !== true ||
|
|
590
|
-
!Array.isArray(receipt.verificationClosure.diagnostics) ||
|
|
591
|
-
receipt.verificationClosure.diagnostics.length !== 0 ||
|
|
592
|
-
!isRecord(receipt.openspec) ||
|
|
593
|
-
!exactKeys(receipt.openspec, ["strict", "planningComplete"]) ||
|
|
594
|
-
receipt.openspec.strict !== true ||
|
|
595
|
-
receipt.openspec.planningComplete !== true
|
|
596
|
-
) {
|
|
597
|
-
throw new Error("delivery-receipt-invalid");
|
|
598
|
-
}
|
|
599
|
-
requireReceiptIdentity(receipt.change, receipt.schema);
|
|
600
|
-
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
601
|
-
const gateB = normalizeGateProof(receipt.approvals.gateB);
|
|
602
|
-
const traceability = normalizeTraceability(receipt.traceability);
|
|
603
|
-
if (
|
|
604
|
-
gateB.contractHash !== receipt.plan.canonicalHash ||
|
|
605
|
-
!sameNormalized(gateB, receipt.approvals.gateB, options) ||
|
|
606
|
-
!sameNormalized(artifacts, receipt.artifacts, options) ||
|
|
607
|
-
!sameNormalized(traceability, receipt.traceability, options)
|
|
608
|
-
) {
|
|
609
|
-
throw new Error("delivery-receipt-invalid");
|
|
610
|
-
}
|
|
611
|
-
return structuredClone(receipt);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function sortStrings(values: readonly string[]): string[] {
|
|
615
|
-
return [...values].sort((left, right) =>
|
|
616
|
-
compareCanonicalStrings(left, right),
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function sortCanonical<T>(values: readonly T[]): T[] {
|
|
621
|
-
return [...values].sort((left, right) =>
|
|
622
|
-
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function normalizeVerification(
|
|
627
|
-
verification: unknown,
|
|
628
|
-
): StructuredVerificationContract {
|
|
629
|
-
const validation = validateVerificationContract(verification);
|
|
630
|
-
if (!validation.ok) throw new Error("delivery-verification-invalid");
|
|
631
|
-
const cloned = validation.value;
|
|
632
|
-
if (cloned.kind === "vitest") {
|
|
633
|
-
cloned.testFiles = sortStrings(cloned.testFiles);
|
|
634
|
-
} else if (cloned.kind === "steps") {
|
|
635
|
-
cloned.steps = cloned.steps.map((step) => {
|
|
636
|
-
if (step.kind !== "vitest") return step;
|
|
637
|
-
return { ...step, testFiles: sortStrings(step.testFiles) };
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return cloned;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
|
|
644
|
-
if (
|
|
645
|
-
!isRecord(value) ||
|
|
646
|
-
!exactKeys(value, [
|
|
647
|
-
"read",
|
|
648
|
-
"write",
|
|
649
|
-
"delete",
|
|
650
|
-
"verification",
|
|
651
|
-
"verificationInputs",
|
|
652
|
-
...(Object.hasOwn(value, "verificationLock") ? ["verificationLock"] : []),
|
|
653
|
-
]) ||
|
|
654
|
-
!Array.isArray(value.read) ||
|
|
655
|
-
!Array.isArray(value.write) ||
|
|
656
|
-
!Array.isArray(value.delete) ||
|
|
657
|
-
!Array.isArray(value.verificationInputs) ||
|
|
658
|
-
!value.read.every(isValidRelativePath) ||
|
|
659
|
-
!value.write.every(isValidRelativePath) ||
|
|
660
|
-
!value.delete.every(isValidRelativePath)
|
|
661
|
-
) {
|
|
662
|
-
throw new Error("delivery-plan-invalid-phase");
|
|
663
|
-
}
|
|
664
|
-
const write = sortStrings(value.write as string[]);
|
|
665
|
-
const deletions = sortStrings(value.delete as string[]);
|
|
666
|
-
if (!readOnly && write.length + deletions.length === 0) {
|
|
667
|
-
throw new Error("delivery-plan-empty-phase-boundary");
|
|
668
|
-
}
|
|
669
|
-
if (write.some((candidate) => deletions.includes(candidate))) {
|
|
670
|
-
throw new Error("delivery-plan-write-delete-overlap");
|
|
671
|
-
}
|
|
672
|
-
return {
|
|
673
|
-
read: sortStrings(value.read as string[]),
|
|
674
|
-
write,
|
|
675
|
-
delete: deletions,
|
|
676
|
-
verification: normalizeVerification(
|
|
677
|
-
value.verification as StructuredVerificationContract,
|
|
678
|
-
),
|
|
679
|
-
verificationInputs: sortCanonical(
|
|
680
|
-
structuredClone(
|
|
681
|
-
value.verificationInputs,
|
|
682
|
-
) as PhaseBoundary["verificationInputs"],
|
|
683
|
-
),
|
|
684
|
-
...(typeof value.verificationLock === "string"
|
|
685
|
-
? { verificationLock: value.verificationLock }
|
|
686
|
-
: {}),
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function normalizeTask(
|
|
691
|
-
value: unknown,
|
|
692
|
-
options: {
|
|
693
|
-
consumerRoot?: string;
|
|
694
|
-
preserveMissingBaseline?: boolean;
|
|
695
|
-
} = {},
|
|
696
|
-
): PlanTaskDraft {
|
|
697
|
-
const taskId =
|
|
698
|
-
isRecord(value) && typeof value.taskId === "string"
|
|
699
|
-
? value.taskId
|
|
700
|
-
: undefined;
|
|
701
|
-
const context = taskId ? { taskId } : {};
|
|
702
|
-
try {
|
|
703
|
-
if (
|
|
704
|
-
!isRecord(value) ||
|
|
705
|
-
!isRecord(value.phases) ||
|
|
706
|
-
!isRecord(value.affectedVerification) ||
|
|
707
|
-
!isRecord(value.repairVerification)
|
|
708
|
-
) {
|
|
709
|
-
throw new Error("delivery-plan-invalid-task");
|
|
710
|
-
}
|
|
711
|
-
const phases = value.phases;
|
|
712
|
-
if (!Object.hasOwn(phases, "red") || !Object.hasOwn(phases, "green")) {
|
|
713
|
-
throw new Error("delivery-plan-invalid-task-phases");
|
|
714
|
-
}
|
|
715
|
-
const cloned = structuredClone(value) as unknown as PlanTaskDraft;
|
|
716
|
-
let affectedVerification: StructuredVerificationContract;
|
|
717
|
-
try {
|
|
718
|
-
affectedVerification = normalizeVerification(value.affectedVerification);
|
|
719
|
-
} catch (error) {
|
|
720
|
-
throw asPlanValidationError(error, {
|
|
721
|
-
...context,
|
|
722
|
-
field: "affectedVerification",
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
let repairVerification: StructuredVerificationContract;
|
|
726
|
-
try {
|
|
727
|
-
repairVerification = normalizeVerification(value.repairVerification);
|
|
728
|
-
} catch (error) {
|
|
729
|
-
throw asPlanValidationError(error, {
|
|
730
|
-
...context,
|
|
731
|
-
field: "repairVerification",
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
let baselineVerification: StructuredVerificationContract | undefined;
|
|
735
|
-
if (value.baselineVerification !== undefined) {
|
|
736
|
-
try {
|
|
737
|
-
baselineVerification = normalizeVerification(
|
|
738
|
-
value.baselineVerification,
|
|
739
|
-
);
|
|
740
|
-
} catch (error) {
|
|
741
|
-
throw asPlanValidationError(error, {
|
|
742
|
-
...context,
|
|
743
|
-
field: "baselineVerification",
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
} else if (!options.preserveMissingBaseline) {
|
|
747
|
-
const unavailable = verificationInputPaths(affectedVerification).filter(
|
|
748
|
-
(input) =>
|
|
749
|
-
!options.consumerRoot ||
|
|
750
|
-
!isSafeRegularFile(options.consumerRoot, input),
|
|
751
|
-
);
|
|
752
|
-
if (unavailable.length > 0) {
|
|
753
|
-
throw new DesignPlanValidationError(
|
|
754
|
-
"delivery-task-baseline-unavailable",
|
|
755
|
-
unavailable.map((input) => ({
|
|
756
|
-
code: "workspace-input-unavailable",
|
|
757
|
-
...context,
|
|
758
|
-
field: "baselineVerification",
|
|
759
|
-
verificationId: affectedVerification.id,
|
|
760
|
-
path: input,
|
|
761
|
-
})),
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
baselineVerification = structuredClone(affectedVerification);
|
|
765
|
-
}
|
|
766
|
-
if (
|
|
767
|
-
(baselineVerification &&
|
|
768
|
-
baselineVerification.classification !== "expected-green") ||
|
|
769
|
-
affectedVerification.classification !== "expected-green" ||
|
|
770
|
-
repairVerification.classification !== "expected-green"
|
|
771
|
-
) {
|
|
772
|
-
throw new Error("delivery-task-verification-invalid");
|
|
773
|
-
}
|
|
774
|
-
const normalizeTaskPhase = (
|
|
775
|
-
phase: "red" | "green" | "refactor",
|
|
776
|
-
candidate: unknown,
|
|
777
|
-
) => {
|
|
778
|
-
try {
|
|
779
|
-
return normalizePhase(
|
|
780
|
-
candidate,
|
|
781
|
-
phase === "red" &&
|
|
782
|
-
value.verificationMode !== undefined &&
|
|
783
|
-
value.verificationMode !== "behavior",
|
|
784
|
-
);
|
|
785
|
-
} catch (error) {
|
|
786
|
-
throw asPlanValidationError(error, {
|
|
787
|
-
...context,
|
|
788
|
-
phase,
|
|
789
|
-
field: `phases.${phase}`,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
return {
|
|
794
|
-
...cloned,
|
|
795
|
-
dependsOn: sortStrings(cloned.dependsOn),
|
|
796
|
-
roots: sortStrings(cloned.roots),
|
|
797
|
-
phases: {
|
|
798
|
-
red: normalizeTaskPhase("red", phases.red),
|
|
799
|
-
green: normalizeTaskPhase("green", phases.green),
|
|
800
|
-
...(phases.refactor !== undefined
|
|
801
|
-
? { refactor: normalizeTaskPhase("refactor", phases.refactor) }
|
|
802
|
-
: {}),
|
|
803
|
-
},
|
|
804
|
-
scheduling: {
|
|
805
|
-
conflicts: sortStrings(cloned.scheduling.conflicts),
|
|
806
|
-
resources: sortStrings(cloned.scheduling.resources),
|
|
807
|
-
},
|
|
808
|
-
approvedDependencies: sortStrings(cloned.approvedDependencies),
|
|
809
|
-
...(baselineVerification ? { baselineVerification } : {}),
|
|
810
|
-
affectedVerification,
|
|
811
|
-
repairVerification,
|
|
812
|
-
impactClosure: {
|
|
813
|
-
changedSurfaces: sortStrings(
|
|
814
|
-
cloned.impactClosure.changedSurfaces,
|
|
815
|
-
) as PlanTaskDraft["impactClosure"]["changedSurfaces"],
|
|
816
|
-
searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
|
|
817
|
-
relatedTests: [...cloned.impactClosure.relatedTests].sort(
|
|
818
|
-
(left, right) =>
|
|
819
|
-
compareCanonicalStrings(left.path, right.path) ||
|
|
820
|
-
compareCanonicalStrings(left.disposition, right.disposition),
|
|
821
|
-
),
|
|
822
|
-
affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
} catch (error) {
|
|
826
|
-
throw asPlanValidationError(error, context);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
|
|
831
|
-
const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
|
|
832
|
-
read: phase.read,
|
|
833
|
-
write: phase.write,
|
|
834
|
-
delete: phase.delete,
|
|
835
|
-
verification: phase.verification,
|
|
836
|
-
verificationInputs: phase.verificationInputs,
|
|
837
|
-
...(phase.verificationLock
|
|
838
|
-
? { verificationLock: phase.verificationLock }
|
|
839
|
-
: {}),
|
|
840
|
-
});
|
|
841
|
-
return {
|
|
842
|
-
changeId: plan.changeId,
|
|
843
|
-
tasks: plan.tasks.map((task) => {
|
|
844
|
-
const {
|
|
845
|
-
baselineVerification: _baselineVerification,
|
|
846
|
-
affectedVerification: _affectedVerification,
|
|
847
|
-
repairVerification: _repairVerification,
|
|
848
|
-
...boundary
|
|
849
|
-
} = task;
|
|
850
|
-
return {
|
|
851
|
-
...boundary,
|
|
852
|
-
phases: {
|
|
853
|
-
red: stripPhase(task.phases.red),
|
|
854
|
-
green: stripPhase(task.phases.green),
|
|
855
|
-
...(task.phases.refactor
|
|
856
|
-
? { refactor: stripPhase(task.phases.refactor) }
|
|
857
|
-
: {}),
|
|
858
|
-
},
|
|
859
|
-
};
|
|
860
|
-
}),
|
|
861
|
-
outputs: structuredClone(plan.outputs),
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
type PathOperation = {
|
|
866
|
-
taskId: string;
|
|
867
|
-
phase: "red" | "green" | "refactor";
|
|
868
|
-
kind: "write" | "delete";
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
const PLAN_PHASE_INDEX = { red: 0, green: 1, refactor: 2 } as const;
|
|
872
|
-
|
|
873
|
-
function taskAncestors(
|
|
874
|
-
taskId: string,
|
|
875
|
-
tasks: ReadonlyMap<string, PlanTaskDraft>,
|
|
876
|
-
): Set<string> {
|
|
877
|
-
const ancestors = new Set<string>();
|
|
878
|
-
const pending = [...(tasks.get(taskId)?.dependsOn ?? [])];
|
|
879
|
-
while (pending.length > 0) {
|
|
880
|
-
const dependency = pending.pop() as string;
|
|
881
|
-
if (ancestors.has(dependency)) continue;
|
|
882
|
-
ancestors.add(dependency);
|
|
883
|
-
pending.push(...(tasks.get(dependency)?.dependsOn ?? []));
|
|
884
|
-
}
|
|
885
|
-
return ancestors;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function pathOperations(plan: ImplementPlan, input: string): PathOperation[] {
|
|
889
|
-
return plan.tasks.flatMap((task) =>
|
|
890
|
-
(["red", "green", "refactor"] as const).flatMap((phase) => {
|
|
891
|
-
const boundary = task.phases[phase];
|
|
892
|
-
if (!boundary) return [];
|
|
893
|
-
return [
|
|
894
|
-
...(boundary.write.includes(input)
|
|
895
|
-
? [{ taskId: task.taskId, phase, kind: "write" as const }]
|
|
896
|
-
: []),
|
|
897
|
-
...(boundary.delete.includes(input)
|
|
898
|
-
? [{ taskId: task.taskId, phase, kind: "delete" as const }]
|
|
899
|
-
: []),
|
|
900
|
-
];
|
|
901
|
-
}),
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function operationBefore(
|
|
906
|
-
left: PathOperation,
|
|
907
|
-
right: PathOperation,
|
|
908
|
-
ancestors: ReadonlyMap<string, ReadonlySet<string>>,
|
|
909
|
-
): boolean {
|
|
910
|
-
return left.taskId === right.taskId
|
|
911
|
-
? PLAN_PHASE_INDEX[left.phase] < PLAN_PHASE_INDEX[right.phase]
|
|
912
|
-
: (ancestors.get(right.taskId)?.has(left.taskId) ?? false);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function pathSurvives(
|
|
916
|
-
operations: readonly PathOperation[],
|
|
917
|
-
ancestors: ReadonlyMap<string, ReadonlySet<string>>,
|
|
918
|
-
): boolean {
|
|
919
|
-
return operations
|
|
920
|
-
.filter((operation) => operation.kind === "delete")
|
|
921
|
-
.every((deletion) =>
|
|
922
|
-
operations.some(
|
|
923
|
-
(operation) =>
|
|
924
|
-
operation.kind === "write" &&
|
|
925
|
-
operationBefore(deletion, operation, ancestors),
|
|
926
|
-
),
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* File-level acceptance entrypoints named directly by a verification contract.
|
|
932
|
-
* Vitest identifies its test files and a Node static check identifies its
|
|
933
|
-
* executable script. Package scripts do not expose selected files, and an
|
|
934
|
-
* unrelated helper or data output is not promoted merely because of its path.
|
|
935
|
-
*/
|
|
936
|
-
function verificationAcceptanceEntrypoints(
|
|
937
|
-
verification: StructuredVerificationContract,
|
|
938
|
-
): string[] {
|
|
939
|
-
return verificationSteps(verification).flatMap((step) => {
|
|
940
|
-
if (step.kind === "vitest") return step.testFiles;
|
|
941
|
-
if (step.kind === "static-check" && step.runner.kind === "node") {
|
|
942
|
-
return [step.runner.script];
|
|
943
|
-
}
|
|
944
|
-
return [];
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function agentsTargetAvailable(
|
|
949
|
-
root: string,
|
|
950
|
-
operation: PlanAgentsCheckpointOperation,
|
|
951
|
-
): boolean {
|
|
952
|
-
if (operation.impact === "create-index") {
|
|
953
|
-
return ["absent", "file"].includes(
|
|
954
|
-
observeSafePath(root, operation.target).kind,
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
if (!isSafeRegularFile(root, operation.target)) return false;
|
|
958
|
-
if (operation.impact === "update-existing") return true;
|
|
959
|
-
try {
|
|
960
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
961
|
-
readFileSync(path.join(root, operation.target)),
|
|
962
|
-
);
|
|
963
|
-
const start = text.indexOf(AGENTS_START);
|
|
964
|
-
const end = text.indexOf(AGENTS_END);
|
|
965
|
-
if (
|
|
966
|
-
start < 0 ||
|
|
967
|
-
end <= start ||
|
|
968
|
-
text.indexOf(AGENTS_START, start + AGENTS_START.length) >= 0 ||
|
|
969
|
-
text.indexOf(AGENTS_END, end + AGENTS_END.length) >= 0
|
|
970
|
-
) {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
return (
|
|
974
|
-
`${text.slice(0, start)}${text.slice(end + AGENTS_END.length)}`.trim()
|
|
975
|
-
.length > 0
|
|
976
|
-
);
|
|
977
|
-
} catch {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
function inputTimingDiagnostics(
|
|
983
|
-
plan: ImplementPlan,
|
|
984
|
-
consumerRoot: string,
|
|
985
|
-
): DesignPlanDiagnostic[] {
|
|
986
|
-
const diagnostics: DesignPlanDiagnostic[] = [];
|
|
987
|
-
const tasks = new Map(plan.tasks.map((task) => [task.taskId, task]));
|
|
988
|
-
const ancestors = new Map(
|
|
989
|
-
plan.tasks.map((task) => [task.taskId, taskAncestors(task.taskId, tasks)]),
|
|
990
|
-
);
|
|
991
|
-
const outputByPath = new Map(
|
|
992
|
-
plan.outputs.map((output) => [output.path, output]),
|
|
993
|
-
);
|
|
994
|
-
const agentsOperations = new Map(
|
|
995
|
-
plan.verification.agentsCheckpoint.operations.map((operation) => [
|
|
996
|
-
operation.target,
|
|
997
|
-
operation,
|
|
998
|
-
]),
|
|
999
|
-
);
|
|
1000
|
-
const unavailable = (input: {
|
|
1001
|
-
field: string;
|
|
1002
|
-
verification: StructuredVerificationContract;
|
|
1003
|
-
path: string;
|
|
1004
|
-
task?: PlanTaskDraft;
|
|
1005
|
-
code: "workspace-input-unavailable" | "producer-output-unavailable";
|
|
1006
|
-
}) => {
|
|
1007
|
-
const output = outputByPath.get(input.path);
|
|
1008
|
-
diagnostics.push({
|
|
1009
|
-
code: input.code,
|
|
1010
|
-
...(input.task ? { taskId: input.task.taskId } : {}),
|
|
1011
|
-
field: input.field,
|
|
1012
|
-
verificationId: input.verification.id,
|
|
1013
|
-
path: input.path,
|
|
1014
|
-
...(output
|
|
1015
|
-
? {
|
|
1016
|
-
outputId: output.id,
|
|
1017
|
-
producerTaskId: output.producer.taskId,
|
|
1018
|
-
producerPhase: output.producer.phase,
|
|
1019
|
-
}
|
|
1020
|
-
: {}),
|
|
1021
|
-
});
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
const requireOriginalInputs = (
|
|
1025
|
-
field: string,
|
|
1026
|
-
verification: StructuredVerificationContract,
|
|
1027
|
-
task?: PlanTaskDraft,
|
|
1028
|
-
) => {
|
|
1029
|
-
for (const input of verificationInputPaths(verification)) {
|
|
1030
|
-
if (!isSafeRegularFile(consumerRoot, input)) {
|
|
1031
|
-
unavailable({
|
|
1032
|
-
field,
|
|
1033
|
-
verification,
|
|
1034
|
-
path: input,
|
|
1035
|
-
task,
|
|
1036
|
-
code: "workspace-input-unavailable",
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
const requireInput = (
|
|
1042
|
-
field: string,
|
|
1043
|
-
verification: StructuredVerificationContract,
|
|
1044
|
-
input: string,
|
|
1045
|
-
task?: PlanTaskDraft,
|
|
1046
|
-
) => {
|
|
1047
|
-
const actualPaths = verificationInputPaths(verification).sort(
|
|
1048
|
-
compareCanonicalStrings,
|
|
1049
|
-
);
|
|
1050
|
-
if (actualPaths.includes(input)) return;
|
|
1051
|
-
diagnostics.push({
|
|
1052
|
-
code: "verification-input-binding-mismatch",
|
|
1053
|
-
...(task ? { taskId: task.taskId } : {}),
|
|
1054
|
-
field,
|
|
1055
|
-
verificationId: verification.id,
|
|
1056
|
-
path: input,
|
|
1057
|
-
expectedPaths: [...new Set([...actualPaths, input])].sort(
|
|
1058
|
-
compareCanonicalStrings,
|
|
1059
|
-
),
|
|
1060
|
-
actualPaths,
|
|
1061
|
-
});
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
for (const task of plan.tasks) {
|
|
1065
|
-
if (task.baselineVerification) {
|
|
1066
|
-
requireOriginalInputs(
|
|
1067
|
-
"baselineVerification",
|
|
1068
|
-
task.baselineVerification,
|
|
1069
|
-
task,
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
const permittedTasks = new Set([
|
|
1073
|
-
task.taskId,
|
|
1074
|
-
...(ancestors.get(task.taskId) ?? []),
|
|
1075
|
-
]);
|
|
1076
|
-
for (const [field, verification] of [
|
|
1077
|
-
["affectedVerification", task.affectedVerification],
|
|
1078
|
-
["repairVerification", task.repairVerification],
|
|
1079
|
-
] as const) {
|
|
1080
|
-
for (const input of verificationInputPaths(verification)) {
|
|
1081
|
-
const output = outputByPath.get(input);
|
|
1082
|
-
if (output && !permittedTasks.has(output.producer.taskId)) {
|
|
1083
|
-
diagnostics.push({
|
|
1084
|
-
code: "producer-not-dependency",
|
|
1085
|
-
taskId: task.taskId,
|
|
1086
|
-
field,
|
|
1087
|
-
verificationId: verification.id,
|
|
1088
|
-
path: input,
|
|
1089
|
-
outputId: output.id,
|
|
1090
|
-
producerTaskId: output.producer.taskId,
|
|
1091
|
-
producerPhase: output.producer.phase,
|
|
1092
|
-
});
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
const operations = pathOperations(plan, input).filter((operation) =>
|
|
1096
|
-
permittedTasks.has(operation.taskId),
|
|
1097
|
-
);
|
|
1098
|
-
if (
|
|
1099
|
-
(!output && !isSafeRegularFile(consumerRoot, input)) ||
|
|
1100
|
-
!pathSurvives(operations, ancestors)
|
|
1101
|
-
) {
|
|
1102
|
-
unavailable({
|
|
1103
|
-
field,
|
|
1104
|
-
verification,
|
|
1105
|
-
path: input,
|
|
1106
|
-
task,
|
|
1107
|
-
code: output
|
|
1108
|
-
? "producer-output-unavailable"
|
|
1109
|
-
: "workspace-input-unavailable",
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
requireOriginalInputs(
|
|
1117
|
-
"verification.baseline.fullSuite",
|
|
1118
|
-
plan.verification.baseline.fullSuite,
|
|
1119
|
-
);
|
|
1120
|
-
const deliveryAcceptanceEntrypoints = new Set([
|
|
1121
|
-
...verificationAcceptanceEntrypoints(plan.verification.change.fullSuite),
|
|
1122
|
-
...verificationAcceptanceEntrypoints(plan.verification.change.postApply),
|
|
1123
|
-
...(plan.verification.agentsCheckpoint.verification
|
|
1124
|
-
? verificationAcceptanceEntrypoints(
|
|
1125
|
-
plan.verification.agentsCheckpoint.verification,
|
|
1126
|
-
)
|
|
1127
|
-
: []),
|
|
1128
|
-
]);
|
|
1129
|
-
for (const output of plan.outputs) {
|
|
1130
|
-
const producer = tasks.get(output.producer.taskId);
|
|
1131
|
-
const target = producer?.phases[output.producer.phase];
|
|
1132
|
-
if (
|
|
1133
|
-
!producer ||
|
|
1134
|
-
!target ||
|
|
1135
|
-
observeSafePath(consumerRoot, output.path).kind !== "absent"
|
|
1136
|
-
) {
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
const declaredTests = new Set([
|
|
1140
|
-
...producer.impactClosure.relatedTests.map((test) => test.path),
|
|
1141
|
-
...producer.impactClosure.affectedSuite,
|
|
1142
|
-
...(["red", "green", "refactor"] as const).flatMap((phase) => {
|
|
1143
|
-
const boundary = producer.phases[phase];
|
|
1144
|
-
return boundary
|
|
1145
|
-
? verificationAcceptanceEntrypoints(boundary.verification)
|
|
1146
|
-
: [];
|
|
1147
|
-
}),
|
|
1148
|
-
...verificationAcceptanceEntrypoints(producer.affectedVerification),
|
|
1149
|
-
...verificationAcceptanceEntrypoints(producer.repairVerification),
|
|
1150
|
-
...deliveryAcceptanceEntrypoints,
|
|
1151
|
-
]);
|
|
1152
|
-
if (!declaredTests.has(output.path)) continue;
|
|
1153
|
-
for (const phase of ["red", "green", "refactor"] as const) {
|
|
1154
|
-
const boundary = producer.phases[phase];
|
|
1155
|
-
if (
|
|
1156
|
-
!boundary ||
|
|
1157
|
-
PLAN_PHASE_INDEX[phase] < PLAN_PHASE_INDEX[output.producer.phase]
|
|
1158
|
-
) {
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
requireInput(
|
|
1162
|
-
`phases.${phase}.verification`,
|
|
1163
|
-
boundary.verification,
|
|
1164
|
-
output.path,
|
|
1165
|
-
producer,
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
requireInput(
|
|
1169
|
-
"affectedVerification",
|
|
1170
|
-
producer.affectedVerification,
|
|
1171
|
-
output.path,
|
|
1172
|
-
producer,
|
|
1173
|
-
);
|
|
1174
|
-
requireInput(
|
|
1175
|
-
"repairVerification",
|
|
1176
|
-
producer.repairVerification,
|
|
1177
|
-
output.path,
|
|
1178
|
-
producer,
|
|
1179
|
-
);
|
|
1180
|
-
requireInput(
|
|
1181
|
-
"verification.change.fullSuite",
|
|
1182
|
-
plan.verification.change.fullSuite,
|
|
1183
|
-
output.path,
|
|
1184
|
-
);
|
|
1185
|
-
requireInput(
|
|
1186
|
-
"verification.change.postApply",
|
|
1187
|
-
plan.verification.change.postApply,
|
|
1188
|
-
output.path,
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
for (const [field, verification, afterAgents] of [
|
|
1192
|
-
[
|
|
1193
|
-
"verification.change.fullSuite",
|
|
1194
|
-
plan.verification.change.fullSuite,
|
|
1195
|
-
false,
|
|
1196
|
-
],
|
|
1197
|
-
["verification.change.postApply", plan.verification.change.postApply, true],
|
|
1198
|
-
...(plan.verification.agentsCheckpoint.verification
|
|
1199
|
-
? ([
|
|
1200
|
-
[
|
|
1201
|
-
"verification.agentsCheckpoint.verification",
|
|
1202
|
-
plan.verification.agentsCheckpoint.verification,
|
|
1203
|
-
true,
|
|
1204
|
-
],
|
|
1205
|
-
] as const)
|
|
1206
|
-
: []),
|
|
1207
|
-
] as const) {
|
|
1208
|
-
for (const input of verificationInputPaths(verification)) {
|
|
1209
|
-
const output = outputByPath.get(input);
|
|
1210
|
-
const operations = pathOperations(plan, input);
|
|
1211
|
-
const agentsOperation = afterAgents
|
|
1212
|
-
? agentsOperations.get(input)
|
|
1213
|
-
: undefined;
|
|
1214
|
-
if (
|
|
1215
|
-
agentsOperation
|
|
1216
|
-
? !agentsTargetAvailable(consumerRoot, agentsOperation)
|
|
1217
|
-
: (!output && !isSafeRegularFile(consumerRoot, input)) ||
|
|
1218
|
-
!pathSurvives(operations, ancestors)
|
|
1219
|
-
) {
|
|
1220
|
-
unavailable({
|
|
1221
|
-
field,
|
|
1222
|
-
verification,
|
|
1223
|
-
path: input,
|
|
1224
|
-
code: output
|
|
1225
|
-
? "producer-output-unavailable"
|
|
1226
|
-
: "workspace-input-unavailable",
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return diagnostics;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function deliveryVerificationDiagnostics(
|
|
1235
|
-
plan: ImplementPlan,
|
|
1236
|
-
consumerRoot: string,
|
|
1237
|
-
): Array<Record<string, unknown>> {
|
|
1238
|
-
const contracts: Array<{
|
|
1239
|
-
owner: string;
|
|
1240
|
-
verification: StructuredVerificationContract;
|
|
1241
|
-
}> = plan.tasks.flatMap((task) => [
|
|
1242
|
-
...(task.baselineVerification
|
|
1243
|
-
? [
|
|
1244
|
-
{
|
|
1245
|
-
owner: `task:${task.taskId}:baseline`,
|
|
1246
|
-
verification: task.baselineVerification,
|
|
1247
|
-
},
|
|
1248
|
-
]
|
|
1249
|
-
: []),
|
|
1250
|
-
{
|
|
1251
|
-
owner: `task:${task.taskId}:affected`,
|
|
1252
|
-
verification: task.affectedVerification,
|
|
1253
|
-
},
|
|
1254
|
-
{
|
|
1255
|
-
owner: `task:${task.taskId}:repair`,
|
|
1256
|
-
verification: task.repairVerification,
|
|
1257
|
-
},
|
|
1258
|
-
]);
|
|
1259
|
-
contracts.push(
|
|
1260
|
-
{
|
|
1261
|
-
owner: "baseline:full-suite",
|
|
1262
|
-
verification: plan.verification.baseline.fullSuite,
|
|
1263
|
-
},
|
|
1264
|
-
{
|
|
1265
|
-
owner: "change:full-suite",
|
|
1266
|
-
verification: plan.verification.change.fullSuite,
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
owner: "change:post-apply",
|
|
1270
|
-
verification: plan.verification.change.postApply,
|
|
1271
|
-
},
|
|
1272
|
-
);
|
|
1273
|
-
if (plan.verification.agentsCheckpoint.verification) {
|
|
1274
|
-
contracts.push({
|
|
1275
|
-
owner: "agents-checkpoint",
|
|
1276
|
-
verification: plan.verification.agentsCheckpoint.verification,
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
return contracts.flatMap(({ owner, verification }) => {
|
|
1280
|
-
const capability = validateVerificationAdapterCapability(
|
|
1281
|
-
consumerRoot,
|
|
1282
|
-
verification,
|
|
1283
|
-
{ dependencyOwner: consumerRoot },
|
|
1284
|
-
);
|
|
1285
|
-
if (capability.ok) return [];
|
|
1286
|
-
const { message: _message, ...diagnostic } = capability.diagnostic;
|
|
1287
|
-
return [{ owner, ...diagnostic }];
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function normalizeExpectedGreenVerification(
|
|
1292
|
-
value: unknown,
|
|
1293
|
-
): StructuredVerificationContract {
|
|
1294
|
-
const verification = normalizeVerification(value);
|
|
1295
|
-
if (verification.classification !== "expected-green") {
|
|
1296
|
-
throw new Error("delivery-verification-classification-invalid");
|
|
1297
|
-
}
|
|
1298
|
-
return verification;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function normalizeAgentsManagedBlock(value: unknown): string {
|
|
1302
|
-
if (
|
|
1303
|
-
typeof value !== "string" ||
|
|
1304
|
-
value.length === 0 ||
|
|
1305
|
-
Buffer.byteLength(value) > 64 * 1024 ||
|
|
1306
|
-
value.includes("\0") ||
|
|
1307
|
-
value.includes("\r")
|
|
1308
|
-
) {
|
|
1309
|
-
throw new Error("delivery-agents-checkpoint-invalid");
|
|
1310
|
-
}
|
|
1311
|
-
const lines = value.endsWith("\n")
|
|
1312
|
-
? value.slice(0, -1).split("\n")
|
|
1313
|
-
: value.split("\n");
|
|
1314
|
-
if (
|
|
1315
|
-
lines[0] !== AGENTS_START ||
|
|
1316
|
-
lines.at(-1) !== AGENTS_END ||
|
|
1317
|
-
lines.filter((line) => line === AGENTS_START).length !== 1 ||
|
|
1318
|
-
lines.filter((line) => line === AGENTS_END).length !== 1
|
|
1319
|
-
) {
|
|
1320
|
-
throw new Error("delivery-agents-checkpoint-invalid");
|
|
1321
|
-
}
|
|
1322
|
-
return lines.join("\n");
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
function normalizeAgentsCheckpointOperations(
|
|
1326
|
-
value: unknown,
|
|
1327
|
-
): PlanAgentsCheckpointOperation[] {
|
|
1328
|
-
if (!Array.isArray(value) || value.length > 64) {
|
|
1329
|
-
throw new Error("delivery-agents-checkpoint-invalid");
|
|
1330
|
-
}
|
|
1331
|
-
const targets = new Set<string>();
|
|
1332
|
-
return value
|
|
1333
|
-
.map((operation): PlanAgentsCheckpointOperation => {
|
|
1334
|
-
if (
|
|
1335
|
-
!isRecord(operation) ||
|
|
1336
|
-
!exactKeys(operation, [
|
|
1337
|
-
"target",
|
|
1338
|
-
"impact",
|
|
1339
|
-
"taskIds",
|
|
1340
|
-
"managedBlock",
|
|
1341
|
-
]) ||
|
|
1342
|
-
!isAgentsPath(operation.target) ||
|
|
1343
|
-
!["update-existing", "create-index", "remove-index"].includes(
|
|
1344
|
-
String(operation.impact),
|
|
1345
|
-
) ||
|
|
1346
|
-
targets.has(operation.target) ||
|
|
1347
|
-
!Array.isArray(operation.taskIds) ||
|
|
1348
|
-
operation.taskIds.length === 0 ||
|
|
1349
|
-
operation.taskIds.some(
|
|
1350
|
-
(taskId) => typeof taskId !== "string" || taskId.length === 0,
|
|
1351
|
-
) ||
|
|
1352
|
-
new Set(operation.taskIds).size !== operation.taskIds.length
|
|
1353
|
-
) {
|
|
1354
|
-
throw new Error("delivery-agents-checkpoint-invalid");
|
|
1355
|
-
}
|
|
1356
|
-
targets.add(operation.target);
|
|
1357
|
-
const impact = operation.impact as Exclude<AgentsImpact, "none">;
|
|
1358
|
-
if (
|
|
1359
|
-
(impact === "remove-index" && operation.managedBlock !== null) ||
|
|
1360
|
-
(impact !== "remove-index" &&
|
|
1361
|
-
typeof operation.managedBlock !== "string")
|
|
1362
|
-
) {
|
|
1363
|
-
throw new Error("delivery-agents-checkpoint-invalid");
|
|
1364
|
-
}
|
|
1365
|
-
return {
|
|
1366
|
-
target: operation.target,
|
|
1367
|
-
impact,
|
|
1368
|
-
taskIds: sortStrings(operation.taskIds as string[]),
|
|
1369
|
-
managedBlock:
|
|
1370
|
-
impact === "remove-index"
|
|
1371
|
-
? null
|
|
1372
|
-
: normalizeAgentsManagedBlock(operation.managedBlock),
|
|
1373
|
-
};
|
|
1374
|
-
})
|
|
1375
|
-
.sort((left, right) => compareCanonicalStrings(left.target, right.target));
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function normalizeVerificationPlan(value: unknown): PlanVerification {
|
|
1379
|
-
if (
|
|
1380
|
-
isRecord(value) &&
|
|
1381
|
-
isRecord(value.change) &&
|
|
1382
|
-
value.change.affected !== "task-affected-contracts"
|
|
1383
|
-
) {
|
|
1384
|
-
throw new DesignPlanValidationError("delivery-verification-plan-invalid", [
|
|
1385
|
-
{
|
|
1386
|
-
code: "delivery-verification-plan-invalid",
|
|
1387
|
-
field: "verification.change.affected",
|
|
1388
|
-
category: "enum",
|
|
1389
|
-
allowedValues: ["task-affected-contracts"],
|
|
1390
|
-
},
|
|
1391
|
-
]);
|
|
1392
|
-
}
|
|
1393
|
-
if (
|
|
1394
|
-
!isRecord(value) ||
|
|
1395
|
-
!exactKeys(value, [
|
|
1396
|
-
"baseline",
|
|
1397
|
-
"change",
|
|
1398
|
-
"artifactCorrection",
|
|
1399
|
-
"repair",
|
|
1400
|
-
"agentsCheckpoint",
|
|
1401
|
-
]) ||
|
|
1402
|
-
!isRecord(value.baseline) ||
|
|
1403
|
-
!exactKeys(value.baseline, [
|
|
1404
|
-
"target",
|
|
1405
|
-
"affected",
|
|
1406
|
-
"fullSuite",
|
|
1407
|
-
"failureIdentity",
|
|
1408
|
-
]) ||
|
|
1409
|
-
value.baseline.target !== "task-red-contracts" ||
|
|
1410
|
-
value.baseline.affected !== "task-affected-contracts" ||
|
|
1411
|
-
value.baseline.failureIdentity !== "normalized" ||
|
|
1412
|
-
!isRecord(value.change) ||
|
|
1413
|
-
!exactKeys(value.change, ["affected", "fullSuite", "postApply"]) ||
|
|
1414
|
-
value.change.affected !== "task-affected-contracts" ||
|
|
1415
|
-
!isRecord(value.artifactCorrection) ||
|
|
1416
|
-
!exactKeys(value.artifactCorrection, ["maxAttempts"]) ||
|
|
1417
|
-
!Number.isSafeInteger(value.artifactCorrection.maxAttempts) ||
|
|
1418
|
-
(value.artifactCorrection.maxAttempts as number) < 2 ||
|
|
1419
|
-
(value.artifactCorrection.maxAttempts as number) > 3 ||
|
|
1420
|
-
!isRecord(value.repair) ||
|
|
1421
|
-
!exactKeys(value.repair, [
|
|
1422
|
-
"maxAttempts",
|
|
1423
|
-
"inBoundaryOnly",
|
|
1424
|
-
"approvalOnBoundaryExpansion",
|
|
1425
|
-
"attribution",
|
|
1426
|
-
]) ||
|
|
1427
|
-
typeof value.repair.maxAttempts !== "number" ||
|
|
1428
|
-
!Number.isSafeInteger(value.repair.maxAttempts) ||
|
|
1429
|
-
value.repair.maxAttempts < 1 ||
|
|
1430
|
-
value.repair.maxAttempts > 3 ||
|
|
1431
|
-
value.repair.inBoundaryOnly !== true ||
|
|
1432
|
-
value.repair.approvalOnBoundaryExpansion !== true ||
|
|
1433
|
-
canonicalJson(value.repair.attribution) !==
|
|
1434
|
-
canonicalJson([
|
|
1435
|
-
"pre-existing",
|
|
1436
|
-
"introduced",
|
|
1437
|
-
"unresolved",
|
|
1438
|
-
"environment",
|
|
1439
|
-
]) ||
|
|
1440
|
-
!isRecord(value.agentsCheckpoint) ||
|
|
1441
|
-
!exactKeys(value.agentsCheckpoint, [
|
|
1442
|
-
"required",
|
|
1443
|
-
"verification",
|
|
1444
|
-
"operations",
|
|
1445
|
-
]) ||
|
|
1446
|
-
typeof value.agentsCheckpoint.required !== "boolean" ||
|
|
1447
|
-
(value.agentsCheckpoint.required &&
|
|
1448
|
-
!isRecord(value.agentsCheckpoint.verification)) ||
|
|
1449
|
-
(!value.agentsCheckpoint.required &&
|
|
1450
|
-
value.agentsCheckpoint.verification !== null) ||
|
|
1451
|
-
!Array.isArray(value.agentsCheckpoint.operations)
|
|
1452
|
-
) {
|
|
1453
|
-
throw new Error("delivery-verification-plan-invalid");
|
|
1454
|
-
}
|
|
1455
|
-
return {
|
|
1456
|
-
baseline: {
|
|
1457
|
-
target: "task-red-contracts",
|
|
1458
|
-
affected: "task-affected-contracts",
|
|
1459
|
-
fullSuite: normalizeExpectedGreenVerification(value.baseline.fullSuite),
|
|
1460
|
-
failureIdentity: "normalized",
|
|
1461
|
-
},
|
|
1462
|
-
change: {
|
|
1463
|
-
affected: "task-affected-contracts",
|
|
1464
|
-
fullSuite: normalizeExpectedGreenVerification(value.change.fullSuite),
|
|
1465
|
-
postApply: normalizeExpectedGreenVerification(value.change.postApply),
|
|
1466
|
-
},
|
|
1467
|
-
artifactCorrection: {
|
|
1468
|
-
maxAttempts: value.artifactCorrection.maxAttempts as number,
|
|
1469
|
-
},
|
|
1470
|
-
repair: {
|
|
1471
|
-
maxAttempts: value.repair.maxAttempts as number,
|
|
1472
|
-
inBoundaryOnly: true,
|
|
1473
|
-
approvalOnBoundaryExpansion: true,
|
|
1474
|
-
attribution: ["pre-existing", "introduced", "unresolved", "environment"],
|
|
1475
|
-
},
|
|
1476
|
-
agentsCheckpoint: {
|
|
1477
|
-
required: value.agentsCheckpoint.required,
|
|
1478
|
-
verification: value.agentsCheckpoint.required
|
|
1479
|
-
? normalizeExpectedGreenVerification(
|
|
1480
|
-
value.agentsCheckpoint.verification,
|
|
1481
|
-
)
|
|
1482
|
-
: null,
|
|
1483
|
-
operations: normalizeAgentsCheckpointOperations(
|
|
1484
|
-
value.agentsCheckpoint.operations,
|
|
1485
|
-
),
|
|
1486
|
-
},
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function normalizeTracking(value: unknown): PlanTracking {
|
|
1491
|
-
if (
|
|
1492
|
-
!isRecord(value) ||
|
|
1493
|
-
!exactKeys(value, ["path", "format", "taskIds", "completionOwner"]) ||
|
|
1494
|
-
value.path !== "tasks.md" ||
|
|
1495
|
-
value.format !== "markdown-checkbox" ||
|
|
1496
|
-
!Array.isArray(value.taskIds) ||
|
|
1497
|
-
!value.taskIds.every((taskId) => typeof taskId === "string") ||
|
|
1498
|
-
new Set(value.taskIds).size !== value.taskIds.length ||
|
|
1499
|
-
value.completionOwner !== "parent"
|
|
1500
|
-
) {
|
|
1501
|
-
throw new Error("delivery-tracking-invalid");
|
|
1502
|
-
}
|
|
1503
|
-
return {
|
|
1504
|
-
path: "tasks.md",
|
|
1505
|
-
format: "markdown-checkbox",
|
|
1506
|
-
taskIds: sortStrings(value.taskIds as string[]),
|
|
1507
|
-
completionOwner: "parent",
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function normalizeDraft(
|
|
1512
|
-
value: unknown,
|
|
1513
|
-
options: {
|
|
1514
|
-
consumerRoot?: string;
|
|
1515
|
-
preserveMissingTaskBaselines?: boolean;
|
|
1516
|
-
} = {},
|
|
1517
|
-
): ImplementPlan {
|
|
1518
|
-
value = structuredClone(value);
|
|
1519
|
-
if (
|
|
1520
|
-
!isRecord(value) ||
|
|
1521
|
-
!exactKeys(value, [
|
|
1522
|
-
"changeId",
|
|
1523
|
-
"tasks",
|
|
1524
|
-
"outputs",
|
|
1525
|
-
"verification",
|
|
1526
|
-
"tracking",
|
|
1527
|
-
...(value.changeContract === undefined ? [] : ["changeContract"]),
|
|
1528
|
-
]) ||
|
|
1529
|
-
typeof value.changeId !== "string" ||
|
|
1530
|
-
!CHANGE_NAME.test(value.changeId) ||
|
|
1531
|
-
!Array.isArray(value.tasks) ||
|
|
1532
|
-
!Array.isArray(value.outputs)
|
|
1533
|
-
) {
|
|
1534
|
-
throw new Error("delivery-plan-invalid");
|
|
1535
|
-
}
|
|
1536
|
-
const source = value;
|
|
1537
|
-
const taskDrafts = value.tasks.map((task: unknown) => {
|
|
1538
|
-
if (
|
|
1539
|
-
!isRecord(task) ||
|
|
1540
|
-
!["mechanical", "refactor"].includes(String(task.verificationMode))
|
|
1541
|
-
)
|
|
1542
|
-
return task;
|
|
1543
|
-
if (!isRecord(task.phases) || !isRecord(task.phases.green))
|
|
1544
|
-
throw new Error("delivery-plan-invalid-task-phases");
|
|
1545
|
-
if (!source.changeContract)
|
|
1546
|
-
throw new Error("change-contract-verification-mode-required");
|
|
1547
|
-
const green = task.phases.green;
|
|
1548
|
-
return {
|
|
1549
|
-
...task,
|
|
1550
|
-
phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
|
|
1551
|
-
};
|
|
1552
|
-
});
|
|
1553
|
-
const draftGraph = validateImplementGraphBoundary({
|
|
1554
|
-
changeId: value.changeId,
|
|
1555
|
-
tasks: taskDrafts.map((candidate) => {
|
|
1556
|
-
if (!isRecord(candidate)) return candidate;
|
|
1557
|
-
const {
|
|
1558
|
-
baselineVerification: _baselineVerification,
|
|
1559
|
-
affectedVerification: _affectedVerification,
|
|
1560
|
-
repairVerification: _repairVerification,
|
|
1561
|
-
...boundary
|
|
1562
|
-
} = candidate;
|
|
1563
|
-
return boundary;
|
|
1564
|
-
}),
|
|
1565
|
-
outputs: value.outputs,
|
|
1566
|
-
});
|
|
1567
|
-
if (!draftGraph.ok) {
|
|
1568
|
-
throw new DesignPlanValidationError(
|
|
1569
|
-
`delivery-plan-invalid:${draftGraph.reason}`,
|
|
1570
|
-
draftGraph.diagnostics ?? [draftGraph.diagnostic],
|
|
1571
|
-
);
|
|
1572
|
-
}
|
|
1573
|
-
const plan: ImplementPlan = {
|
|
1574
|
-
...(value.changeContract === undefined
|
|
1575
|
-
? {}
|
|
1576
|
-
: { changeContract: normalizeChangeContract(value.changeContract) }),
|
|
1577
|
-
changeId: value.changeId,
|
|
1578
|
-
tasks: taskDrafts
|
|
1579
|
-
.map((task) =>
|
|
1580
|
-
normalizeTask(task, {
|
|
1581
|
-
consumerRoot: options.consumerRoot,
|
|
1582
|
-
preserveMissingBaseline: options.preserveMissingTaskBaselines,
|
|
1583
|
-
}),
|
|
1584
|
-
)
|
|
1585
|
-
.sort((left, right) =>
|
|
1586
|
-
compareCanonicalStrings(left.taskId, right.taskId),
|
|
1587
|
-
),
|
|
1588
|
-
outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
|
|
1589
|
-
(left, right) => compareCanonicalStrings(left.id, right.id),
|
|
1590
|
-
),
|
|
1591
|
-
verification: normalizeVerificationPlan(value.verification),
|
|
1592
|
-
tracking: normalizeTracking(value.tracking),
|
|
1593
|
-
};
|
|
1594
|
-
if (
|
|
1595
|
-
canonicalJson(plan.tracking.taskIds) !==
|
|
1596
|
-
canonicalJson(plan.tasks.map((task) => task.taskId).sort())
|
|
1597
|
-
) {
|
|
1598
|
-
throw new Error("delivery-tracking-task-mismatch");
|
|
1599
|
-
}
|
|
1600
|
-
const agentsTasks = plan.tasks.filter(
|
|
1601
|
-
(task) => task.agents.impact !== "none" && task.agents.target,
|
|
1602
|
-
);
|
|
1603
|
-
const agentsByTarget = new Map<string, PlanTaskDraft[]>();
|
|
1604
|
-
for (const task of agentsTasks) {
|
|
1605
|
-
const target = task.agents.target as string;
|
|
1606
|
-
const existing = agentsByTarget.get(target) ?? [];
|
|
1607
|
-
existing.push(task);
|
|
1608
|
-
agentsByTarget.set(target, existing);
|
|
1609
|
-
}
|
|
1610
|
-
const operations = plan.verification.agentsCheckpoint.operations;
|
|
1611
|
-
const operationsByTarget = new Map(
|
|
1612
|
-
operations.map((operation) => [operation.target, operation]),
|
|
1613
|
-
);
|
|
1614
|
-
const agentsRequired = agentsTasks.length > 0;
|
|
1615
|
-
if (
|
|
1616
|
-
plan.verification.agentsCheckpoint.required !== agentsRequired ||
|
|
1617
|
-
operations.length !== agentsByTarget.size ||
|
|
1618
|
-
[...agentsByTarget].some(([target, tasks]) => {
|
|
1619
|
-
const operation = operationsByTarget.get(target);
|
|
1620
|
-
return (
|
|
1621
|
-
!operation ||
|
|
1622
|
-
tasks.some((task) => task.agents.impact !== operation.impact) ||
|
|
1623
|
-
canonicalJson(operation.taskIds) !==
|
|
1624
|
-
canonicalJson(tasks.map((task) => task.taskId).sort())
|
|
1625
|
-
);
|
|
1626
|
-
})
|
|
1627
|
-
) {
|
|
1628
|
-
throw new Error("delivery-agents-checkpoint-mismatch");
|
|
1629
|
-
}
|
|
1630
|
-
if (plan.changeContract)
|
|
1631
|
-
assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
|
|
1632
|
-
plan.verification.change.fullSuite,
|
|
1633
|
-
plan.verification.change.postApply,
|
|
1634
|
-
]);
|
|
1635
|
-
const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
|
|
1636
|
-
if (!graphValidation.ok) {
|
|
1637
|
-
throw new DesignPlanValidationError(
|
|
1638
|
-
`delivery-plan-invalid:${graphValidation.reason}`,
|
|
1639
|
-
[graphValidation.diagnostic],
|
|
1640
|
-
);
|
|
1641
|
-
}
|
|
1642
|
-
return plan;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
|
|
1646
|
-
const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
|
|
1647
|
-
|
|
1648
|
-
function taskVerificationRegion(markdown: string) {
|
|
1649
|
-
const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
|
|
1650
|
-
if (markers.length === 0) return undefined;
|
|
1651
|
-
const match =
|
|
1652
|
-
/^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
|
|
1653
|
-
markdown,
|
|
1654
|
-
);
|
|
1655
|
-
if (markers.length !== 2 || !match) {
|
|
1656
|
-
throw new DesignPlanValidationError("traceability-managed-region-invalid", [
|
|
1657
|
-
{ code: "traceability-managed-region-invalid", path: "tasks.md" },
|
|
1658
|
-
]);
|
|
1659
|
-
}
|
|
1660
|
-
return {
|
|
1661
|
-
start: match.index,
|
|
1662
|
-
end: match.index + match[0].length,
|
|
1663
|
-
text: match[0],
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// Only identity bindings are generated. Author task checkboxes and Scenario
|
|
1668
|
-
// ownership remain outside this region and are validated independently.
|
|
1669
|
-
function renderTasks(plan: ImplementPlan): string {
|
|
1670
|
-
const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
|
|
1671
|
-
for (const task of [...plan.tasks].sort((a, b) =>
|
|
1672
|
-
compareCanonicalStrings(a.taskId, b.taskId),
|
|
1673
|
-
)) {
|
|
1674
|
-
lines.push(
|
|
1675
|
-
`- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
|
|
1676
|
-
);
|
|
1677
|
-
if (!task.verificationMode || task.verificationMode === "behavior") {
|
|
1678
|
-
lines.push(` - Red: \`${task.phases.red.verification.id}\``);
|
|
1679
|
-
}
|
|
1680
|
-
lines.push(` - Green: \`${task.phases.green.verification.id}\``);
|
|
1681
|
-
if (task.phases.refactor) {
|
|
1682
|
-
lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
lines.push(TASK_BINDINGS_END);
|
|
1686
|
-
return lines.join("\n");
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
export function bindTaskVerifications(
|
|
1690
|
-
markdown: string,
|
|
1691
|
-
plan: ImplementPlan,
|
|
1692
|
-
): string {
|
|
1693
|
-
const region = taskVerificationRegion(markdown);
|
|
1694
|
-
const bindings = renderTasks(plan);
|
|
1695
|
-
if (region) {
|
|
1696
|
-
return (
|
|
1697
|
-
markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
|
|
1698
|
-
);
|
|
1699
|
-
}
|
|
1700
|
-
return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
export function compileImplementPlan(
|
|
1704
|
-
draft: unknown,
|
|
1705
|
-
options: { consumerRoot: string; bindExecutionInputs?: boolean },
|
|
1706
|
-
): CompiledDelivery {
|
|
1707
|
-
try {
|
|
1708
|
-
draft = expandSingleTaskDraft(draft);
|
|
1709
|
-
// Resolve omitted manifest command bytes before named definitions are
|
|
1710
|
-
// validated and purpose identities are hashed. The second pass below
|
|
1711
|
-
// binds execution inputs on the expanded contracts with their final IDs.
|
|
1712
|
-
if (options.bindExecutionInputs)
|
|
1713
|
-
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1714
|
-
draft = expandPlanDraft(draft);
|
|
1715
|
-
if (options.bindExecutionInputs)
|
|
1716
|
-
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1717
|
-
draft = preparePlanDraft(draft);
|
|
1718
|
-
const retainedLegacy =
|
|
1719
|
-
isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft);
|
|
1720
|
-
const normalized = normalizeDraft(draft, {
|
|
1721
|
-
consumerRoot: options.consumerRoot,
|
|
1722
|
-
preserveMissingTaskBaselines: retainedLegacy,
|
|
1723
|
-
});
|
|
1724
|
-
const plan = retainedLegacy
|
|
1725
|
-
? (draft as unknown as ImplementPlan)
|
|
1726
|
-
: normalized;
|
|
1727
|
-
const graph = graphFromPlan(plan);
|
|
1728
|
-
const readiness = assessImplementGraphReadiness(
|
|
1729
|
-
options.consumerRoot,
|
|
1730
|
-
graph,
|
|
1731
|
-
{
|
|
1732
|
-
completedTasks: [],
|
|
1733
|
-
blockedTasks: [],
|
|
1734
|
-
appliedPhases: [],
|
|
1735
|
-
},
|
|
1736
|
-
);
|
|
1737
|
-
const diagnostics = [
|
|
1738
|
-
...readiness.closure.diagnostics,
|
|
1739
|
-
...inputTimingDiagnostics(plan, options.consumerRoot),
|
|
1740
|
-
...deliveryVerificationDiagnostics(plan, options.consumerRoot),
|
|
1741
|
-
] as DesignPlanDiagnostic[];
|
|
1742
|
-
if (diagnostics.length > 0) {
|
|
1743
|
-
throw new DesignPlanValidationError(
|
|
1744
|
-
`delivery-plan-not-executable:${canonicalJson(diagnostics)}`,
|
|
1745
|
-
diagnostics,
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
const canonical = canonicalJson(plan);
|
|
1749
|
-
const bytes = new TextEncoder().encode(`${canonical}\n`);
|
|
1750
|
-
const rawSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
1751
|
-
const planHash = hashCanonicalValue(plan);
|
|
1752
|
-
const closure = structuredClone(readiness.closure);
|
|
1753
|
-
return {
|
|
1754
|
-
checks: {
|
|
1755
|
-
structure: "passed",
|
|
1756
|
-
verificationCapability: "passed",
|
|
1757
|
-
contractCoverage: plan.changeContract ? "passed" : "not-checked",
|
|
1758
|
-
sealing: "not-performed",
|
|
1759
|
-
},
|
|
1760
|
-
plan,
|
|
1761
|
-
bytes,
|
|
1762
|
-
rawSha256,
|
|
1763
|
-
planHash,
|
|
1764
|
-
closure,
|
|
1765
|
-
receipt: {
|
|
1766
|
-
plan: {
|
|
1767
|
-
path: IMPLEMENT_PLAN_PATH,
|
|
1768
|
-
rawSha256,
|
|
1769
|
-
canonicalHash: planHash,
|
|
1770
|
-
},
|
|
1771
|
-
verificationClosure: structuredClone(closure),
|
|
1772
|
-
},
|
|
1773
|
-
tasksMarkdown: renderTasks(plan),
|
|
1774
|
-
};
|
|
1775
|
-
} catch (error) {
|
|
1776
|
-
throw asPlanValidationError(error);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
export function parseImplementPlan(
|
|
1781
|
-
bytes: Uint8Array,
|
|
1782
|
-
options: DeliveryReadOptions = {},
|
|
1783
|
-
): ImplementPlan {
|
|
1784
|
-
const text = new TextDecoder().decode(bytes);
|
|
1785
|
-
let parsed: unknown;
|
|
1786
|
-
try {
|
|
1787
|
-
parsed = JSON.parse(text);
|
|
1788
|
-
} catch {
|
|
1789
|
-
throw new Error("delivery-plan-json-invalid");
|
|
1790
|
-
}
|
|
1791
|
-
const normalized = normalizeDraft(parsed, {
|
|
1792
|
-
preserveMissingTaskBaselines: true,
|
|
1793
|
-
});
|
|
1794
|
-
if (`${canonicalJson(normalized)}\n` === text) return normalized;
|
|
1795
|
-
if (
|
|
1796
|
-
!options.allowLegacyOrder ||
|
|
1797
|
-
`${canonicalJson(parsed)}\n` !== text ||
|
|
1798
|
-
!sameNormalized(normalized, parsed, options)
|
|
1799
|
-
) {
|
|
1800
|
-
throw new Error("delivery-plan-not-canonical");
|
|
1801
|
-
}
|
|
1802
|
-
// Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
|
|
1803
|
-
legacyPlans.set(parsed as object, JSON.stringify(parsed));
|
|
1804
|
-
return parsed as ImplementPlan;
|
|
1805
|
-
}
|