@abelxiaoxing/cadence 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -13
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/delivery-compiler.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
3
|
+
import {
|
|
4
|
+
assertPlanWithinChangeContract,
|
|
5
|
+
normalizeChangeContract,
|
|
6
|
+
} from "./change-contract.ts";
|
|
2
7
|
import {
|
|
3
8
|
type AgentsImpact,
|
|
4
9
|
type ImplementGraphBoundary,
|
|
5
10
|
type ImplementGraphOutput,
|
|
6
|
-
type ImplementTaskBoundary,
|
|
7
11
|
isAgentsPath,
|
|
8
12
|
isValidRelativePath,
|
|
9
13
|
type PhaseBoundary,
|
|
@@ -11,13 +15,28 @@ import {
|
|
|
11
15
|
validateImplementGraphBoundary,
|
|
12
16
|
validateVerificationContract,
|
|
13
17
|
} from "./contracts.ts";
|
|
18
|
+
import {
|
|
19
|
+
type DesignPlanDiagnostic,
|
|
20
|
+
DesignPlanValidationError,
|
|
21
|
+
} from "./design-diagnostics.ts";
|
|
14
22
|
import {
|
|
15
23
|
assessImplementGraphReadiness,
|
|
16
24
|
canonicalJson,
|
|
17
25
|
hashCanonicalValue,
|
|
18
26
|
type ImplementGraphReadiness,
|
|
19
27
|
} from "./implement-graph.ts";
|
|
20
|
-
import {
|
|
28
|
+
import type {
|
|
29
|
+
ImplementPlan,
|
|
30
|
+
PlanAgentsCheckpointOperation,
|
|
31
|
+
PlanTaskDraft,
|
|
32
|
+
PlanTracking,
|
|
33
|
+
PlanVerification,
|
|
34
|
+
} from "./implement-plan.ts";
|
|
35
|
+
import { expandPlanDraft, preparePlanDraft } from "./plan-draft.ts";
|
|
36
|
+
import {
|
|
37
|
+
bindDraftVerificationInputs,
|
|
38
|
+
validateVerificationAdapterCapability,
|
|
39
|
+
} from "./verification-capability.ts";
|
|
21
40
|
|
|
22
41
|
export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
|
|
23
42
|
export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
|
|
@@ -94,40 +113,6 @@ export class DeliveryValidationError extends Error {
|
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
|
|
97
|
-
export interface DesignPlanDiagnostic {
|
|
98
|
-
code: string;
|
|
99
|
-
taskId?: string;
|
|
100
|
-
phase?: string;
|
|
101
|
-
field?: string;
|
|
102
|
-
category?: string;
|
|
103
|
-
owner?: string;
|
|
104
|
-
verificationId?: string;
|
|
105
|
-
outputId?: string;
|
|
106
|
-
dependencyTaskId?: string;
|
|
107
|
-
producerTaskId?: string;
|
|
108
|
-
producerPhase?: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export class DesignPlanValidationError extends Error {
|
|
112
|
-
readonly diagnostics: readonly DesignPlanDiagnostic[];
|
|
113
|
-
|
|
114
|
-
constructor(message: string, diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
115
|
-
const normalized = [
|
|
116
|
-
...new Map(
|
|
117
|
-
diagnostics
|
|
118
|
-
.map((diagnostic) => structuredClone(diagnostic))
|
|
119
|
-
.sort((left, right) =>
|
|
120
|
-
canonicalJson(left).localeCompare(canonicalJson(right)),
|
|
121
|
-
)
|
|
122
|
-
.map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
|
|
123
|
-
).values(),
|
|
124
|
-
];
|
|
125
|
-
super(message);
|
|
126
|
-
this.name = "DesignPlanValidationError";
|
|
127
|
-
this.diagnostics = Object.freeze(normalized);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
116
|
const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
132
117
|
|
|
133
118
|
function planErrorCode(error: unknown): string {
|
|
@@ -173,6 +158,22 @@ export function assessDeliveryTraceability(input: {
|
|
|
173
158
|
plan: ImplementPlan;
|
|
174
159
|
}): DeliveryTraceabilityAssessment {
|
|
175
160
|
const diagnostics = new Set<string>();
|
|
161
|
+
let authorMarkdown = input.tasksMarkdown;
|
|
162
|
+
let bindings: string | undefined;
|
|
163
|
+
try {
|
|
164
|
+
const region = taskVerificationRegion(input.tasksMarkdown);
|
|
165
|
+
if (region) {
|
|
166
|
+
authorMarkdown =
|
|
167
|
+
input.tasksMarkdown.slice(0, region.start) +
|
|
168
|
+
input.tasksMarkdown.slice(region.end);
|
|
169
|
+
bindings = region.text;
|
|
170
|
+
if (bindings !== renderTasks(input.plan)) {
|
|
171
|
+
diagnostics.add("traceability-managed-region-stale");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
|
|
176
|
+
}
|
|
176
177
|
const headings = new Set<string>();
|
|
177
178
|
for (const spec of input.specs) {
|
|
178
179
|
if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
|
|
@@ -197,7 +198,7 @@ export function assessDeliveryTraceability(input: {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
const references = [
|
|
200
|
-
...
|
|
201
|
+
...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
|
|
201
202
|
].flatMap((match) => (match[1] ? [match[1].trim()] : []));
|
|
202
203
|
if (references.length === 0) diagnostics.add("traceability-reference-empty");
|
|
203
204
|
const referenceSet = new Set(references);
|
|
@@ -211,21 +212,22 @@ export function assessDeliveryTraceability(input: {
|
|
|
211
212
|
diagnostics.add("traceability-scenario-unowned");
|
|
212
213
|
}
|
|
213
214
|
const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
|
|
214
|
-
if (
|
|
215
|
-
taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
|
|
216
|
-
) {
|
|
215
|
+
if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
|
|
217
216
|
diagnostics.add("traceability-task-unmapped");
|
|
218
217
|
}
|
|
219
218
|
const verificationIds = input.plan.tasks
|
|
220
219
|
.flatMap((task) => [
|
|
221
|
-
task.
|
|
220
|
+
...(task.verificationMode && task.verificationMode !== "behavior"
|
|
221
|
+
? []
|
|
222
|
+
: [task.phases.red.verification.id]),
|
|
222
223
|
task.phases.green.verification.id,
|
|
223
224
|
...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
|
|
224
225
|
])
|
|
225
226
|
.sort();
|
|
226
227
|
if (
|
|
227
228
|
verificationIds.some(
|
|
228
|
-
(verificationId) =>
|
|
229
|
+
(verificationId) =>
|
|
230
|
+
!(bindings ?? authorMarkdown).includes(verificationId),
|
|
229
231
|
)
|
|
230
232
|
) {
|
|
231
233
|
diagnostics.add("traceability-verification-unmapped");
|
|
@@ -249,69 +251,16 @@ export function assessDeliveryTraceability(input: {
|
|
|
249
251
|
};
|
|
250
252
|
}
|
|
251
253
|
|
|
252
|
-
export type
|
|
253
|
-
|
|
254
|
-
export
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export interface PlanVerification {
|
|
265
|
-
baseline: {
|
|
266
|
-
target: "task-red-contracts";
|
|
267
|
-
affected: "task-affected-contracts";
|
|
268
|
-
fullSuite: StructuredVerificationContract;
|
|
269
|
-
failureIdentity: "normalized";
|
|
270
|
-
};
|
|
271
|
-
change: {
|
|
272
|
-
affected: "task-affected-contracts";
|
|
273
|
-
fullSuite: StructuredVerificationContract;
|
|
274
|
-
postApply: StructuredVerificationContract;
|
|
275
|
-
};
|
|
276
|
-
artifactCorrection: {
|
|
277
|
-
maxAttempts: number;
|
|
278
|
-
};
|
|
279
|
-
repair: {
|
|
280
|
-
maxAttempts: number;
|
|
281
|
-
inBoundaryOnly: true;
|
|
282
|
-
approvalOnBoundaryExpansion: true;
|
|
283
|
-
attribution: ["pre-existing", "introduced", "unresolved", "environment"];
|
|
284
|
-
};
|
|
285
|
-
agentsCheckpoint: {
|
|
286
|
-
required: boolean;
|
|
287
|
-
verification: StructuredVerificationContract | null;
|
|
288
|
-
operations: PlanAgentsCheckpointOperation[];
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export interface PlanAgentsCheckpointOperation {
|
|
293
|
-
target: string;
|
|
294
|
-
impact: Exclude<AgentsImpact, "none">;
|
|
295
|
-
taskIds: string[];
|
|
296
|
-
managedBlock: string | null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export interface PlanTracking {
|
|
300
|
-
path: "tasks.md";
|
|
301
|
-
format: "markdown-checkbox";
|
|
302
|
-
taskIds: string[];
|
|
303
|
-
completionOwner: "parent";
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export interface PlanDraft {
|
|
307
|
-
changeId: string;
|
|
308
|
-
tasks: PlanTaskDraft[];
|
|
309
|
-
outputs: ImplementGraphOutput[];
|
|
310
|
-
verification: PlanVerification;
|
|
311
|
-
tracking: PlanTracking;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export type ImplementPlan = PlanDraft;
|
|
254
|
+
export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
255
|
+
export { DesignPlanValidationError } from "./design-diagnostics.ts";
|
|
256
|
+
export type {
|
|
257
|
+
ImplementPlan,
|
|
258
|
+
PlanAgentsCheckpointOperation,
|
|
259
|
+
PlanTaskDraft,
|
|
260
|
+
PlanTracking,
|
|
261
|
+
PlanVerification,
|
|
262
|
+
} from "./implement-plan.ts";
|
|
263
|
+
export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
|
|
315
264
|
|
|
316
265
|
export interface CompiledDelivery {
|
|
317
266
|
plan: ImplementPlan;
|
|
@@ -371,7 +320,9 @@ function normalizeArtifactBindings(
|
|
|
371
320
|
paths.add(binding.path);
|
|
372
321
|
return { path: binding.path, rawSha256: binding.rawSha256 };
|
|
373
322
|
});
|
|
374
|
-
return normalized.sort((left, right) =>
|
|
323
|
+
return normalized.sort((left, right) =>
|
|
324
|
+
compareCanonicalStrings(left.path, right.path),
|
|
325
|
+
);
|
|
375
326
|
}
|
|
376
327
|
|
|
377
328
|
function requireReceiptIdentity(
|
|
@@ -405,7 +356,41 @@ function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
|
|
|
405
356
|
return structuredClone(value);
|
|
406
357
|
}
|
|
407
358
|
|
|
408
|
-
|
|
359
|
+
export interface DeliveryReadOptions {
|
|
360
|
+
/** Only the proof-bound delivery loader may admit historical collection ordering. */
|
|
361
|
+
allowLegacyOrder?: boolean;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const legacyPlans = new WeakMap<object, string>();
|
|
365
|
+
|
|
366
|
+
function unorderedIdentity(value: unknown): string {
|
|
367
|
+
if (Array.isArray(value))
|
|
368
|
+
return `[${value.map(unorderedIdentity).sort().join(",")}]`;
|
|
369
|
+
if (isRecord(value))
|
|
370
|
+
return `{${Object.entries(value)
|
|
371
|
+
.sort(([a], [b]) => compareCanonicalStrings(a, b))
|
|
372
|
+
.map(
|
|
373
|
+
([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
|
|
374
|
+
)
|
|
375
|
+
.join(",")}}`;
|
|
376
|
+
return JSON.stringify(value) ?? "null";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sameNormalized(
|
|
380
|
+
left: unknown,
|
|
381
|
+
right: unknown,
|
|
382
|
+
options: DeliveryReadOptions,
|
|
383
|
+
): boolean {
|
|
384
|
+
return options.allowLegacyOrder
|
|
385
|
+
? unorderedIdentity(left) === unorderedIdentity(right)
|
|
386
|
+
: canonicalJson(left) === canonicalJson(right);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function parseCanonicalReceipt<T>(
|
|
390
|
+
bytes: Uint8Array,
|
|
391
|
+
code: string,
|
|
392
|
+
options: DeliveryReadOptions = {},
|
|
393
|
+
): T {
|
|
409
394
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
410
395
|
let parsed: unknown;
|
|
411
396
|
try {
|
|
@@ -413,7 +398,11 @@ function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
|
|
|
413
398
|
} catch {
|
|
414
399
|
throw new Error(code);
|
|
415
400
|
}
|
|
416
|
-
if (
|
|
401
|
+
if (
|
|
402
|
+
`${canonicalJson(parsed)}\n` !== text &&
|
|
403
|
+
(!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
|
|
404
|
+
)
|
|
405
|
+
throw new Error(code);
|
|
417
406
|
return parsed as T;
|
|
418
407
|
}
|
|
419
408
|
|
|
@@ -438,10 +427,14 @@ export function compileGateAReceipt(input: {
|
|
|
438
427
|
};
|
|
439
428
|
}
|
|
440
429
|
|
|
441
|
-
export function parseGateAReceipt(
|
|
430
|
+
export function parseGateAReceipt(
|
|
431
|
+
bytes: Uint8Array,
|
|
432
|
+
options: DeliveryReadOptions = {},
|
|
433
|
+
): GateAReceipt {
|
|
442
434
|
const receipt = parseCanonicalReceipt<GateAReceipt>(
|
|
443
435
|
bytes,
|
|
444
436
|
"gate-a-receipt-invalid",
|
|
437
|
+
options,
|
|
445
438
|
);
|
|
446
439
|
if (
|
|
447
440
|
!isRecord(receipt) ||
|
|
@@ -455,8 +448,8 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
|
|
|
455
448
|
const approval = normalizeGateProof(receipt.approval);
|
|
456
449
|
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
457
450
|
if (
|
|
458
|
-
|
|
459
|
-
|
|
451
|
+
!sameNormalized(approval, receipt.approval, options) ||
|
|
452
|
+
!sameNormalized(artifacts, receipt.artifacts, options)
|
|
460
453
|
) {
|
|
461
454
|
throw new Error("gate-a-receipt-invalid");
|
|
462
455
|
}
|
|
@@ -537,10 +530,14 @@ export function compileReadyReceipt(input: {
|
|
|
537
530
|
};
|
|
538
531
|
}
|
|
539
532
|
|
|
540
|
-
export function parseReadyReceipt(
|
|
533
|
+
export function parseReadyReceipt(
|
|
534
|
+
bytes: Uint8Array,
|
|
535
|
+
options: DeliveryReadOptions = {},
|
|
536
|
+
): ReadyReceipt {
|
|
541
537
|
const receipt = parseCanonicalReceipt<ReadyReceipt>(
|
|
542
538
|
bytes,
|
|
543
539
|
"delivery-receipt-invalid",
|
|
540
|
+
options,
|
|
544
541
|
);
|
|
545
542
|
if (
|
|
546
543
|
!isRecord(receipt) ||
|
|
@@ -592,9 +589,9 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
|
|
|
592
589
|
const traceability = normalizeTraceability(receipt.traceability);
|
|
593
590
|
if (
|
|
594
591
|
gateB.contractHash !== receipt.plan.canonicalHash ||
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
592
|
+
!sameNormalized(gateB, receipt.approvals.gateB, options) ||
|
|
593
|
+
!sameNormalized(artifacts, receipt.artifacts, options) ||
|
|
594
|
+
!sameNormalized(traceability, receipt.traceability, options)
|
|
598
595
|
) {
|
|
599
596
|
throw new Error("delivery-receipt-invalid");
|
|
600
597
|
}
|
|
@@ -602,12 +599,14 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
|
|
|
602
599
|
}
|
|
603
600
|
|
|
604
601
|
function sortStrings(values: readonly string[]): string[] {
|
|
605
|
-
return [...values].sort((left, right) =>
|
|
602
|
+
return [...values].sort((left, right) =>
|
|
603
|
+
compareCanonicalStrings(left, right),
|
|
604
|
+
);
|
|
606
605
|
}
|
|
607
606
|
|
|
608
607
|
function sortCanonical<T>(values: readonly T[]): T[] {
|
|
609
608
|
return [...values].sort((left, right) =>
|
|
610
|
-
canonicalJson(left)
|
|
609
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
611
610
|
);
|
|
612
611
|
}
|
|
613
612
|
|
|
@@ -628,7 +627,7 @@ function normalizeVerification(
|
|
|
628
627
|
return cloned;
|
|
629
628
|
}
|
|
630
629
|
|
|
631
|
-
function normalizePhase(value: unknown):
|
|
630
|
+
function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
|
|
632
631
|
if (
|
|
633
632
|
!isRecord(value) ||
|
|
634
633
|
!exactKeys(value, [
|
|
@@ -651,7 +650,7 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
|
|
|
651
650
|
}
|
|
652
651
|
const write = sortStrings(value.write as string[]);
|
|
653
652
|
const deletions = sortStrings(value.delete as string[]);
|
|
654
|
-
if (write.length + deletions.length === 0) {
|
|
653
|
+
if (!readOnly && write.length + deletions.length === 0) {
|
|
655
654
|
throw new Error("delivery-plan-empty-phase-boundary");
|
|
656
655
|
}
|
|
657
656
|
if (write.some((candidate) => deletions.includes(candidate))) {
|
|
@@ -724,7 +723,12 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
724
723
|
candidate: unknown,
|
|
725
724
|
) => {
|
|
726
725
|
try {
|
|
727
|
-
return normalizePhase(
|
|
726
|
+
return normalizePhase(
|
|
727
|
+
candidate,
|
|
728
|
+
phase === "red" &&
|
|
729
|
+
value.verificationMode !== undefined &&
|
|
730
|
+
value.verificationMode !== "behavior",
|
|
731
|
+
);
|
|
728
732
|
} catch (error) {
|
|
729
733
|
throw asPlanValidationError(error, {
|
|
730
734
|
...context,
|
|
@@ -758,8 +762,8 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
758
762
|
searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
|
|
759
763
|
relatedTests: [...cloned.impactClosure.relatedTests].sort(
|
|
760
764
|
(left, right) =>
|
|
761
|
-
left.path
|
|
762
|
-
left.disposition
|
|
765
|
+
compareCanonicalStrings(left.path, right.path) ||
|
|
766
|
+
compareCanonicalStrings(left.disposition, right.disposition),
|
|
763
767
|
),
|
|
764
768
|
affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
|
|
765
769
|
},
|
|
@@ -770,7 +774,7 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
770
774
|
}
|
|
771
775
|
|
|
772
776
|
function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
|
|
773
|
-
const stripPhase = (phase:
|
|
777
|
+
const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
|
|
774
778
|
read: phase.read,
|
|
775
779
|
write: phase.write,
|
|
776
780
|
delete: phase.delete,
|
|
@@ -936,7 +940,7 @@ function normalizeAgentsCheckpointOperations(
|
|
|
936
940
|
: normalizeAgentsManagedBlock(operation.managedBlock),
|
|
937
941
|
};
|
|
938
942
|
})
|
|
939
|
-
.sort((left, right) => left.target
|
|
943
|
+
.sort((left, right) => compareCanonicalStrings(left.target, right.target));
|
|
940
944
|
}
|
|
941
945
|
|
|
942
946
|
function normalizeVerificationPlan(value: unknown): PlanVerification {
|
|
@@ -1059,6 +1063,7 @@ function normalizeTracking(value: unknown): PlanTracking {
|
|
|
1059
1063
|
}
|
|
1060
1064
|
|
|
1061
1065
|
function normalizeDraft(value: unknown): ImplementPlan {
|
|
1066
|
+
value = structuredClone(value);
|
|
1062
1067
|
if (
|
|
1063
1068
|
!isRecord(value) ||
|
|
1064
1069
|
!exactKeys(value, [
|
|
@@ -1067,6 +1072,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1067
1072
|
"outputs",
|
|
1068
1073
|
"verification",
|
|
1069
1074
|
"tracking",
|
|
1075
|
+
...(value.changeContract === undefined ? [] : ["changeContract"]),
|
|
1070
1076
|
]) ||
|
|
1071
1077
|
typeof value.changeId !== "string" ||
|
|
1072
1078
|
!CHANGE_NAME.test(value.changeId) ||
|
|
@@ -1075,9 +1081,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1075
1081
|
) {
|
|
1076
1082
|
throw new Error("delivery-plan-invalid");
|
|
1077
1083
|
}
|
|
1084
|
+
const source = value;
|
|
1085
|
+
const taskDrafts = value.tasks.map((task: unknown) => {
|
|
1086
|
+
if (
|
|
1087
|
+
!isRecord(task) ||
|
|
1088
|
+
!["mechanical", "refactor"].includes(String(task.verificationMode))
|
|
1089
|
+
)
|
|
1090
|
+
return task;
|
|
1091
|
+
if (!isRecord(task.phases) || !isRecord(task.phases.green))
|
|
1092
|
+
throw new Error("delivery-plan-invalid-task-phases");
|
|
1093
|
+
if (!source.changeContract)
|
|
1094
|
+
throw new Error("change-contract-verification-mode-required");
|
|
1095
|
+
const green = task.phases.green;
|
|
1096
|
+
return {
|
|
1097
|
+
...task,
|
|
1098
|
+
phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1078
1101
|
const draftGraph = validateImplementGraphBoundary({
|
|
1079
1102
|
changeId: value.changeId,
|
|
1080
|
-
tasks:
|
|
1103
|
+
tasks: taskDrafts.map((candidate) => {
|
|
1081
1104
|
if (!isRecord(candidate)) return candidate;
|
|
1082
1105
|
const {
|
|
1083
1106
|
affectedVerification: _affectedVerification,
|
|
@@ -1091,16 +1114,21 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1091
1114
|
if (!draftGraph.ok) {
|
|
1092
1115
|
throw new DesignPlanValidationError(
|
|
1093
1116
|
`delivery-plan-invalid:${draftGraph.reason}`,
|
|
1094
|
-
[draftGraph.diagnostic],
|
|
1117
|
+
draftGraph.diagnostics ?? [draftGraph.diagnostic],
|
|
1095
1118
|
);
|
|
1096
1119
|
}
|
|
1097
1120
|
const plan: ImplementPlan = {
|
|
1121
|
+
...(value.changeContract === undefined
|
|
1122
|
+
? {}
|
|
1123
|
+
: { changeContract: normalizeChangeContract(value.changeContract) }),
|
|
1098
1124
|
changeId: value.changeId,
|
|
1099
|
-
tasks:
|
|
1125
|
+
tasks: taskDrafts
|
|
1100
1126
|
.map(normalizeTask)
|
|
1101
|
-
.sort((left, right) =>
|
|
1127
|
+
.sort((left, right) =>
|
|
1128
|
+
compareCanonicalStrings(left.taskId, right.taskId),
|
|
1129
|
+
),
|
|
1102
1130
|
outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
|
|
1103
|
-
(left, right) => left.id
|
|
1131
|
+
(left, right) => compareCanonicalStrings(left.id, right.id),
|
|
1104
1132
|
),
|
|
1105
1133
|
verification: normalizeVerificationPlan(value.verification),
|
|
1106
1134
|
tracking: normalizeTracking(value.tracking),
|
|
@@ -1141,6 +1169,11 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1141
1169
|
) {
|
|
1142
1170
|
throw new Error("delivery-agents-checkpoint-mismatch");
|
|
1143
1171
|
}
|
|
1172
|
+
if (plan.changeContract)
|
|
1173
|
+
assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
|
|
1174
|
+
plan.verification.change.fullSuite,
|
|
1175
|
+
plan.verification.change.postApply,
|
|
1176
|
+
]);
|
|
1144
1177
|
const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
|
|
1145
1178
|
if (!graphValidation.ok) {
|
|
1146
1179
|
throw new DesignPlanValidationError(
|
|
@@ -1151,26 +1184,83 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1151
1184
|
return plan;
|
|
1152
1185
|
}
|
|
1153
1186
|
|
|
1187
|
+
const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
|
|
1188
|
+
const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
|
|
1189
|
+
|
|
1190
|
+
function taskVerificationRegion(markdown: string) {
|
|
1191
|
+
const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
|
|
1192
|
+
if (markers.length === 0) return undefined;
|
|
1193
|
+
const match =
|
|
1194
|
+
/^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
|
|
1195
|
+
markdown,
|
|
1196
|
+
);
|
|
1197
|
+
if (markers.length !== 2 || !match) {
|
|
1198
|
+
throw new DesignPlanValidationError("traceability-managed-region-invalid", [
|
|
1199
|
+
{ code: "traceability-managed-region-invalid", path: "tasks.md" },
|
|
1200
|
+
]);
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
start: match.index,
|
|
1204
|
+
end: match.index + match[0].length,
|
|
1205
|
+
text: match[0],
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Only identity bindings are generated. Author task checkboxes and Scenario
|
|
1210
|
+
// ownership remain outside this region and are validated independently.
|
|
1154
1211
|
function renderTasks(plan: ImplementPlan): string {
|
|
1155
|
-
const lines = ["
|
|
1156
|
-
for (const task of plan.tasks)
|
|
1157
|
-
|
|
1212
|
+
const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
|
|
1213
|
+
for (const task of [...plan.tasks].sort((a, b) =>
|
|
1214
|
+
compareCanonicalStrings(a.taskId, b.taskId),
|
|
1215
|
+
)) {
|
|
1158
1216
|
lines.push(
|
|
1159
|
-
|
|
1217
|
+
`- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
|
|
1160
1218
|
);
|
|
1161
|
-
|
|
1162
|
-
|
|
1219
|
+
if (!task.verificationMode || task.verificationMode === "behavior") {
|
|
1220
|
+
lines.push(` - Red: \`${task.phases.red.verification.id}\``);
|
|
1221
|
+
}
|
|
1222
|
+
lines.push(` - Green: \`${task.phases.green.verification.id}\``);
|
|
1223
|
+
if (task.phases.refactor) {
|
|
1224
|
+
lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
|
|
1225
|
+
}
|
|
1163
1226
|
}
|
|
1164
|
-
lines.push(
|
|
1227
|
+
lines.push(TASK_BINDINGS_END);
|
|
1165
1228
|
return lines.join("\n");
|
|
1166
1229
|
}
|
|
1167
1230
|
|
|
1231
|
+
export function bindTaskVerifications(
|
|
1232
|
+
markdown: string,
|
|
1233
|
+
plan: ImplementPlan,
|
|
1234
|
+
): string {
|
|
1235
|
+
const region = taskVerificationRegion(markdown);
|
|
1236
|
+
const bindings = renderTasks(plan);
|
|
1237
|
+
if (region) {
|
|
1238
|
+
return (
|
|
1239
|
+
markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1168
1245
|
export function compileImplementPlan(
|
|
1169
1246
|
draft: unknown,
|
|
1170
|
-
options: { consumerRoot: string },
|
|
1247
|
+
options: { consumerRoot: string; bindExecutionInputs?: boolean },
|
|
1171
1248
|
): CompiledDelivery {
|
|
1172
1249
|
try {
|
|
1173
|
-
|
|
1250
|
+
// Resolve omitted manifest command bytes before named definitions are
|
|
1251
|
+
// validated and purpose identities are hashed. The second pass below
|
|
1252
|
+
// binds execution inputs on the expanded contracts with their final IDs.
|
|
1253
|
+
if (options.bindExecutionInputs)
|
|
1254
|
+
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1255
|
+
draft = expandPlanDraft(draft);
|
|
1256
|
+
if (options.bindExecutionInputs)
|
|
1257
|
+
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1258
|
+
draft = preparePlanDraft(draft);
|
|
1259
|
+
const normalized = normalizeDraft(draft);
|
|
1260
|
+
const plan =
|
|
1261
|
+
isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
|
|
1262
|
+
? (draft as unknown as ImplementPlan)
|
|
1263
|
+
: normalized;
|
|
1174
1264
|
const graph = graphFromPlan(plan);
|
|
1175
1265
|
const readiness = assessImplementGraphReadiness(
|
|
1176
1266
|
options.consumerRoot,
|
|
@@ -1217,7 +1307,10 @@ export function compileImplementPlan(
|
|
|
1217
1307
|
}
|
|
1218
1308
|
}
|
|
1219
1309
|
|
|
1220
|
-
export function parseImplementPlan(
|
|
1310
|
+
export function parseImplementPlan(
|
|
1311
|
+
bytes: Uint8Array,
|
|
1312
|
+
options: DeliveryReadOptions = {},
|
|
1313
|
+
): ImplementPlan {
|
|
1221
1314
|
const text = new TextDecoder().decode(bytes);
|
|
1222
1315
|
let parsed: unknown;
|
|
1223
1316
|
try {
|
|
@@ -1226,8 +1319,15 @@ export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
|
|
|
1226
1319
|
throw new Error("delivery-plan-json-invalid");
|
|
1227
1320
|
}
|
|
1228
1321
|
const normalized = normalizeDraft(parsed);
|
|
1229
|
-
if (`${canonicalJson(normalized)}\n`
|
|
1322
|
+
if (`${canonicalJson(normalized)}\n` === text) return normalized;
|
|
1323
|
+
if (
|
|
1324
|
+
!options.allowLegacyOrder ||
|
|
1325
|
+
`${canonicalJson(parsed)}\n` !== text ||
|
|
1326
|
+
!sameNormalized(normalized, parsed, options)
|
|
1327
|
+
) {
|
|
1230
1328
|
throw new Error("delivery-plan-not-canonical");
|
|
1231
1329
|
}
|
|
1232
|
-
|
|
1330
|
+
// Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
|
|
1331
|
+
legacyPlans.set(parsed as object, JSON.stringify(parsed));
|
|
1332
|
+
return parsed as ImplementPlan;
|
|
1233
1333
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ImplementPlan } from "./implement-plan.ts";
|
|
2
|
+
import {
|
|
3
|
+
deliveryBoundPaths,
|
|
4
|
+
type EngineTaskRow,
|
|
5
|
+
taskApprovalBoundary,
|
|
6
|
+
} from "./workflow-policy.ts";
|
|
7
|
+
/** Compare authority only; retained bytes still require executor currentness and replay. */
|
|
8
|
+
export function compareDeliveryRevision(
|
|
9
|
+
priorPlan: ImplementPlan | undefined,
|
|
10
|
+
nextPlan: ImplementPlan,
|
|
11
|
+
priorRows: readonly Pick<EngineTaskRow, "task_id">[],
|
|
12
|
+
) {
|
|
13
|
+
const priorTasks = new Map(
|
|
14
|
+
(priorPlan?.tasks ?? []).map((task) => [task.taskId, task]),
|
|
15
|
+
);
|
|
16
|
+
const nextTasks = new Map(nextPlan.tasks.map((task) => [task.taskId, task]));
|
|
17
|
+
const priorBoundPaths = new Set(
|
|
18
|
+
priorPlan ? deliveryBoundPaths(priorPlan) : [],
|
|
19
|
+
);
|
|
20
|
+
const boundaryExpanded = deliveryBoundPaths(nextPlan).some(
|
|
21
|
+
(relative) => !priorBoundPaths.has(relative),
|
|
22
|
+
);
|
|
23
|
+
const compatible = new Set<string>();
|
|
24
|
+
if (priorPlan) {
|
|
25
|
+
for (const [taskId, priorTask] of priorTasks) {
|
|
26
|
+
const nextTask = nextTasks.get(taskId);
|
|
27
|
+
if (
|
|
28
|
+
nextTask &&
|
|
29
|
+
taskApprovalBoundary(priorPlan, priorTask) ===
|
|
30
|
+
taskApprovalBoundary(nextPlan, nextTask)
|
|
31
|
+
) {
|
|
32
|
+
compatible.add(taskId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const invalidated = new Set(
|
|
37
|
+
priorRows
|
|
38
|
+
.filter((row) => !compatible.has(row.task_id))
|
|
39
|
+
.map((row) => row.task_id),
|
|
40
|
+
);
|
|
41
|
+
return { compatible, invalidated, boundaryExpanded };
|
|
42
|
+
}
|