@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { frontendHumanDecisionEnvelopeSchema, frontendHumanDecisionReceiptSchema } from "./frontend-human-decision.js";
|
|
4
|
+
import { frontendOperationStateSchema, frontendOperationTerminalSchema, isFrontendOperationTerminalState, } from "./frontend-recovery-capsule.js";
|
|
5
|
+
const idSchema = z.string().trim().min(1);
|
|
6
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
7
|
+
const positiveIntegerSchema = z.number().int().positive();
|
|
8
|
+
const nonnegativeIntegerSchema = z.number().int().nonnegative();
|
|
9
|
+
const commitStateSchema = z.literal("committed");
|
|
10
|
+
export const frontendCommittedFactRefSchema = z
|
|
11
|
+
.object({ ref: idSchema, digest: digestSchema, commitState: commitStateSchema })
|
|
12
|
+
.strict();
|
|
13
|
+
function parseBoundedPath(value) {
|
|
14
|
+
if (!value || value === "." || value === "**" || value === "*" || value.startsWith("/") || value.includes("\\"))
|
|
15
|
+
return undefined;
|
|
16
|
+
const rawSegments = value.split("/");
|
|
17
|
+
if (rawSegments.some((segment) => segment.length === 0 || segment === "." || segment === ".."))
|
|
18
|
+
return undefined;
|
|
19
|
+
const trailingGlobstar = rawSegments.at(-1) === "**";
|
|
20
|
+
const segments = trailingGlobstar ? rawSegments.slice(0, -1) : rawSegments;
|
|
21
|
+
if (segments.length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (segment.includes("**"))
|
|
25
|
+
return undefined;
|
|
26
|
+
// The canonical contract intentionally supports only literal characters and
|
|
27
|
+
// `*` within one path segment. Syntax whose language cannot be decided by
|
|
28
|
+
// the matcher below is rejected rather than approximated by a prefix.
|
|
29
|
+
if (/[?\[\]{}()!+@]/.test(segment))
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return { segments, trailingGlobstar };
|
|
33
|
+
}
|
|
34
|
+
function isBoundedPath(value) {
|
|
35
|
+
return parseBoundedPath(value) !== undefined;
|
|
36
|
+
}
|
|
37
|
+
const boundedPathSchema = z.string().refine(isBoundedPath, "path must use the supported bounded repo-relative glob grammar");
|
|
38
|
+
function canonicalValue(value) {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.map(canonicalValue);
|
|
41
|
+
if (value !== null && typeof value === "object") {
|
|
42
|
+
return Object.fromEntries(Object.entries(value)
|
|
43
|
+
.filter(([, item]) => item !== undefined)
|
|
44
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
45
|
+
.map(([key, item]) => [key, canonicalValue(item)]));
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
export function canonicalFrontendSplitJson(value) {
|
|
50
|
+
return JSON.stringify(canonicalValue(value));
|
|
51
|
+
}
|
|
52
|
+
export function computeFrontendSplitDigest(value) {
|
|
53
|
+
return createHash("sha256").update(canonicalFrontendSplitJson(value), "utf8").digest("hex");
|
|
54
|
+
}
|
|
55
|
+
function unique(values, label) {
|
|
56
|
+
if (new Set(values).size !== values.length)
|
|
57
|
+
throw new Error(`duplicate ${label}`);
|
|
58
|
+
}
|
|
59
|
+
function sortStrings(values) {
|
|
60
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
61
|
+
}
|
|
62
|
+
const requirementSchema = z.object({ requirementId: idSchema, implementable: z.boolean() }).strict();
|
|
63
|
+
const coverageSchema = z.object({ requirementId: idSchema, childId: idSchema, role: z.enum(["primary", "supporting"]) }).strict();
|
|
64
|
+
const childSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
childId: idSchema,
|
|
67
|
+
taskId: idSchema,
|
|
68
|
+
operationId: idSchema,
|
|
69
|
+
allowedPaths: z.array(boundedPathSchema).min(1),
|
|
70
|
+
writeSet: z.array(boundedPathSchema).min(1),
|
|
71
|
+
required: z.boolean(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
const dependencySchema = z.object({ childId: idSchema, dependsOn: idSchema }).strict();
|
|
75
|
+
const sharedSurfaceSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
path: boundedPathSchema,
|
|
78
|
+
childIds: z.array(idSchema).min(2),
|
|
79
|
+
policy: z.enum(["foundation", "shared-surface"]),
|
|
80
|
+
foundationOwnerChildId: idSchema.optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict()
|
|
83
|
+
.superRefine((value, ctx) => {
|
|
84
|
+
if (value.policy === "foundation" && value.foundationOwnerChildId === undefined)
|
|
85
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["foundationOwnerChildId"], message: "foundation policy requires an owner" });
|
|
86
|
+
if (value.policy !== "foundation" && value.foundationOwnerChildId !== undefined)
|
|
87
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["foundationOwnerChildId"], message: "only foundation policy may name an owner" });
|
|
88
|
+
});
|
|
89
|
+
const integrationRequirementSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
required: z.boolean(),
|
|
92
|
+
operationId: idSchema.optional(),
|
|
93
|
+
allowedPaths: z.array(boundedPathSchema),
|
|
94
|
+
writeSet: z.array(boundedPathSchema),
|
|
95
|
+
verifyRefs: z.array(idSchema),
|
|
96
|
+
})
|
|
97
|
+
.strict()
|
|
98
|
+
.superRefine((value, ctx) => {
|
|
99
|
+
if (value.required && value.operationId === undefined)
|
|
100
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["operationId"], message: "required integration needs operationId" });
|
|
101
|
+
if (!value.required && value.operationId !== undefined)
|
|
102
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["operationId"], message: "optional integration must not register operationId" });
|
|
103
|
+
});
|
|
104
|
+
const manifestFields = {
|
|
105
|
+
schemaVersion: z.literal(1),
|
|
106
|
+
parentOperationId: idSchema,
|
|
107
|
+
sourceRunId: idSchema,
|
|
108
|
+
sourceDigest: digestSchema,
|
|
109
|
+
requirements: z.array(requirementSchema).min(1),
|
|
110
|
+
coverage: z.array(coverageSchema),
|
|
111
|
+
children: z.array(childSchema).min(1),
|
|
112
|
+
dependencies: z.array(dependencySchema),
|
|
113
|
+
sharedSurfaces: z.array(sharedSurfaceSchema),
|
|
114
|
+
integration: integrationRequirementSchema,
|
|
115
|
+
verifyRefs: z.array(idSchema),
|
|
116
|
+
closeoutRefs: z.array(idSchema),
|
|
117
|
+
};
|
|
118
|
+
export const frontendSplitManifestInputSchema = z.object(manifestFields).strict();
|
|
119
|
+
const frontendSplitManifestStructuralSchema = z
|
|
120
|
+
.object({ ...manifestFields, topologicalOrder: z.array(idSchema), digest: digestSchema })
|
|
121
|
+
.strict();
|
|
122
|
+
function segmentClosure(pattern, positions) {
|
|
123
|
+
const closure = new Set(positions);
|
|
124
|
+
const pending = [...closure];
|
|
125
|
+
while (pending.length > 0) {
|
|
126
|
+
const position = pending.pop();
|
|
127
|
+
if (pattern[position] === "*" && !closure.has(position + 1)) {
|
|
128
|
+
closure.add(position + 1);
|
|
129
|
+
pending.push(position + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...closure].sort((a, b) => a - b);
|
|
133
|
+
}
|
|
134
|
+
function segmentStep(pattern, state, symbol) {
|
|
135
|
+
const next = new Set();
|
|
136
|
+
for (const position of state) {
|
|
137
|
+
const token = pattern[position];
|
|
138
|
+
if (token === "*")
|
|
139
|
+
next.add(position);
|
|
140
|
+
else if (token === symbol)
|
|
141
|
+
next.add(position + 1);
|
|
142
|
+
}
|
|
143
|
+
return segmentClosure(pattern, next);
|
|
144
|
+
}
|
|
145
|
+
function segmentAlphabet(left, right) {
|
|
146
|
+
const literals = new Set([...left, ...right].filter((character) => character !== "*"));
|
|
147
|
+
return [...literals].sort().concat("\0OTHER");
|
|
148
|
+
}
|
|
149
|
+
function segmentTransition(pattern, state, symbol) {
|
|
150
|
+
return segmentStep(pattern, state, symbol === "\0OTHER" ? "\0OTHER" : symbol);
|
|
151
|
+
}
|
|
152
|
+
function segmentLanguageOverlap(left, right) {
|
|
153
|
+
const alphabet = segmentAlphabet(left, right);
|
|
154
|
+
const initialLeft = segmentClosure(left, [0]);
|
|
155
|
+
const initialRight = segmentClosure(right, [0]);
|
|
156
|
+
const pending = [[initialLeft, initialRight]];
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
while (pending.length > 0) {
|
|
159
|
+
const [leftState, rightState] = pending.pop();
|
|
160
|
+
const key = `${leftState.join(",")}|${rightState.join(",")}`;
|
|
161
|
+
if (seen.has(key))
|
|
162
|
+
continue;
|
|
163
|
+
seen.add(key);
|
|
164
|
+
if (leftState.includes(left.length) && rightState.includes(right.length))
|
|
165
|
+
return true;
|
|
166
|
+
for (const symbol of alphabet) {
|
|
167
|
+
const nextLeft = segmentTransition(left, leftState, symbol);
|
|
168
|
+
const nextRight = segmentTransition(right, rightState, symbol);
|
|
169
|
+
if (nextLeft.length > 0 && nextRight.length > 0)
|
|
170
|
+
pending.push([nextLeft, nextRight]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function segmentLanguageContains(allowed, target) {
|
|
176
|
+
const alphabet = segmentAlphabet(allowed, target);
|
|
177
|
+
const initialAllowed = segmentClosure(allowed, [0]);
|
|
178
|
+
const initialTarget = segmentClosure(target, [0]);
|
|
179
|
+
const pending = [[initialAllowed, initialTarget]];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
while (pending.length > 0) {
|
|
182
|
+
const [allowedState, targetState] = pending.pop();
|
|
183
|
+
const key = `${allowedState.join(",")}|${targetState.join(",")}`;
|
|
184
|
+
if (seen.has(key))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
if (targetState.includes(target.length) && !allowedState.includes(allowed.length))
|
|
188
|
+
return false;
|
|
189
|
+
for (const symbol of alphabet) {
|
|
190
|
+
const nextTarget = segmentTransition(target, targetState, symbol);
|
|
191
|
+
if (nextTarget.length === 0)
|
|
192
|
+
continue;
|
|
193
|
+
pending.push([segmentTransition(allowed, allowedState, symbol), nextTarget]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
function pathContains(allowedValue, targetValue) {
|
|
199
|
+
const allowed = parseBoundedPath(allowedValue);
|
|
200
|
+
const target = parseBoundedPath(targetValue);
|
|
201
|
+
if (!allowed || !target)
|
|
202
|
+
return false;
|
|
203
|
+
if (!allowed.trailingGlobstar) {
|
|
204
|
+
if (target.trailingGlobstar || allowed.segments.length !== target.segments.length)
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
else if (target.segments.length < allowed.segments.length) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return allowed.segments.every((segment, index) => segmentLanguageContains(segment, target.segments[index]));
|
|
211
|
+
}
|
|
212
|
+
function pathsOverlap(leftValue, rightValue) {
|
|
213
|
+
const left = parseBoundedPath(leftValue);
|
|
214
|
+
const right = parseBoundedPath(rightValue);
|
|
215
|
+
if (!left || !right)
|
|
216
|
+
return false;
|
|
217
|
+
if (!left.trailingGlobstar && !right.trailingGlobstar && left.segments.length !== right.segments.length)
|
|
218
|
+
return false;
|
|
219
|
+
if (!left.trailingGlobstar && left.segments.length < right.segments.length)
|
|
220
|
+
return false;
|
|
221
|
+
if (!right.trailingGlobstar && right.segments.length < left.segments.length)
|
|
222
|
+
return false;
|
|
223
|
+
const sharedLength = Math.min(left.segments.length, right.segments.length);
|
|
224
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
225
|
+
if (!segmentLanguageOverlap(left.segments[index], right.segments[index]))
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
function topo(children, dependencies) {
|
|
231
|
+
const ids = new Set(children.map((child) => child.childId));
|
|
232
|
+
const incoming = new Map([...ids].map((id) => [id, 0]));
|
|
233
|
+
const outgoing = new Map([...ids].map((id) => [id, []]));
|
|
234
|
+
for (const edge of dependencies) {
|
|
235
|
+
if (!ids.has(edge.childId) || !ids.has(edge.dependsOn))
|
|
236
|
+
throw new Error("dependency references unknown child");
|
|
237
|
+
if (edge.childId === edge.dependsOn)
|
|
238
|
+
throw new Error("self dependency");
|
|
239
|
+
incoming.set(edge.childId, (incoming.get(edge.childId) ?? 0) + 1);
|
|
240
|
+
outgoing.get(edge.dependsOn).push(edge.childId);
|
|
241
|
+
}
|
|
242
|
+
const ready = [...ids].filter((id) => incoming.get(id) === 0).sort();
|
|
243
|
+
const result = [];
|
|
244
|
+
while (ready.length > 0) {
|
|
245
|
+
const id = ready.shift();
|
|
246
|
+
result.push(id);
|
|
247
|
+
for (const dependent of outgoing.get(id).sort()) {
|
|
248
|
+
incoming.set(dependent, incoming.get(dependent) - 1);
|
|
249
|
+
if (incoming.get(dependent) === 0) {
|
|
250
|
+
ready.push(dependent);
|
|
251
|
+
ready.sort();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (result.length !== children.length)
|
|
256
|
+
throw new Error("dependency cycle");
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function reachable(from, to, dependencies) {
|
|
260
|
+
const next = new Map();
|
|
261
|
+
for (const edge of dependencies)
|
|
262
|
+
(next.get(edge.dependsOn) ?? (next.set(edge.dependsOn, []), next.get(edge.dependsOn))).push(edge.childId);
|
|
263
|
+
const pending = [from];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
while (pending.length > 0) {
|
|
266
|
+
const current = pending.pop();
|
|
267
|
+
if (current === to)
|
|
268
|
+
return true;
|
|
269
|
+
if (seen.has(current))
|
|
270
|
+
continue;
|
|
271
|
+
seen.add(current);
|
|
272
|
+
pending.push(...(next.get(current) ?? []));
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
function validateManifest(input) {
|
|
277
|
+
unique(input.requirements.map((item) => item.requirementId), "requirementId");
|
|
278
|
+
unique(input.children.map((item) => item.childId), "childId");
|
|
279
|
+
unique(input.children.map((item) => item.taskId), "taskId");
|
|
280
|
+
unique(input.children.map((item) => item.operationId), "operationId");
|
|
281
|
+
unique(input.dependencies.map((item) => `${item.childId}\0${item.dependsOn}`), "dependency");
|
|
282
|
+
unique(input.coverage.map((item) => `${item.requirementId}\0${item.childId}\0${item.role}`), "coverage");
|
|
283
|
+
unique(input.verifyRefs, "verifyRef");
|
|
284
|
+
unique(input.closeoutRefs, "closeoutRef");
|
|
285
|
+
unique(input.integration.allowedPaths, "integration allowedPath");
|
|
286
|
+
unique(input.integration.writeSet, "integration writeSet");
|
|
287
|
+
unique(input.integration.verifyRefs, "integration verifyRef");
|
|
288
|
+
unique(input.sharedSurfaces.map((surface) => surface.path), "shared surface path");
|
|
289
|
+
const childIds = new Set(input.children.map((item) => item.childId));
|
|
290
|
+
const requirements = new Map(input.requirements.map((item) => [item.requirementId, item]));
|
|
291
|
+
for (const item of input.coverage) {
|
|
292
|
+
if (!requirements.has(item.requirementId) || !childIds.has(item.childId))
|
|
293
|
+
throw new Error("coverage references unknown requirement or child");
|
|
294
|
+
}
|
|
295
|
+
for (const requirement of input.requirements) {
|
|
296
|
+
const primary = input.coverage.filter((item) => item.requirementId === requirement.requirementId && item.role === "primary");
|
|
297
|
+
if (requirement.implementable && primary.length !== 1)
|
|
298
|
+
throw new Error("implementable requirement must have exactly one primary owner");
|
|
299
|
+
if (!requirement.implementable && primary.length > 0)
|
|
300
|
+
throw new Error("non-implementable requirement cannot have primary owner");
|
|
301
|
+
}
|
|
302
|
+
const order = topo(input.children, input.dependencies);
|
|
303
|
+
for (const child of input.children) {
|
|
304
|
+
unique(child.allowedPaths, `allowedPath for ${child.childId}`);
|
|
305
|
+
unique(child.writeSet, `writeSet for ${child.childId}`);
|
|
306
|
+
for (const target of child.writeSet)
|
|
307
|
+
if (!child.allowedPaths.some((allowed) => pathContains(allowed, target)))
|
|
308
|
+
throw new Error(`writeSet outside allowedPaths for ${child.childId}`);
|
|
309
|
+
}
|
|
310
|
+
for (let i = 0; i < input.children.length; i += 1) {
|
|
311
|
+
for (let j = i + 1; j < input.children.length; j += 1) {
|
|
312
|
+
const left = input.children[i];
|
|
313
|
+
const right = input.children[j];
|
|
314
|
+
const overlap = left.writeSet.some((a) => right.writeSet.some((b) => pathsOverlap(a, b)));
|
|
315
|
+
if (!overlap)
|
|
316
|
+
continue;
|
|
317
|
+
const serialized = reachable(left.childId, right.childId, input.dependencies) || reachable(right.childId, left.childId, input.dependencies);
|
|
318
|
+
const foundationSerialized = input.sharedSurfaces.some((surface) => {
|
|
319
|
+
if (surface.policy !== "foundation" || !surface.foundationOwnerChildId)
|
|
320
|
+
return false;
|
|
321
|
+
if (!surface.childIds.includes(left.childId) || !surface.childIds.includes(right.childId))
|
|
322
|
+
return false;
|
|
323
|
+
if (!left.writeSet.some((a) => pathsOverlap(a, surface.path)) || !right.writeSet.some((b) => pathsOverlap(b, surface.path)))
|
|
324
|
+
return false;
|
|
325
|
+
const other = surface.foundationOwnerChildId === left.childId ? right.childId : surface.foundationOwnerChildId === right.childId ? left.childId : undefined;
|
|
326
|
+
return other !== undefined && reachable(surface.foundationOwnerChildId, other, input.dependencies);
|
|
327
|
+
});
|
|
328
|
+
if (!serialized && !foundationSerialized)
|
|
329
|
+
throw new Error(`overlapping writeSet is not serialized: ${left.childId}/${right.childId}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const surface of input.sharedSurfaces) {
|
|
333
|
+
unique(surface.childIds, "shared surface childId");
|
|
334
|
+
if (surface.childIds.some((id) => !childIds.has(id)))
|
|
335
|
+
throw new Error("shared surface references unknown child");
|
|
336
|
+
if (surface.foundationOwnerChildId !== undefined && !surface.childIds.includes(surface.foundationOwnerChildId))
|
|
337
|
+
throw new Error("foundation owner must belong to shared surface");
|
|
338
|
+
for (const childId of surface.childIds) {
|
|
339
|
+
const child = input.children.find((item) => item.childId === childId);
|
|
340
|
+
if (!child.writeSet.some((target) => pathsOverlap(target, surface.path)))
|
|
341
|
+
throw new Error("shared surface must overlap every declared child writeSet");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (input.integration.writeSet.some((target) => !input.integration.allowedPaths.some((allowed) => pathContains(allowed, target))))
|
|
345
|
+
throw new Error("integration writeSet outside allowedPaths");
|
|
346
|
+
return order;
|
|
347
|
+
}
|
|
348
|
+
function canonicalManifestInput(input) {
|
|
349
|
+
return {
|
|
350
|
+
...input,
|
|
351
|
+
requirements: [...input.requirements].sort((a, b) => a.requirementId.localeCompare(b.requirementId)),
|
|
352
|
+
coverage: [...input.coverage].sort((a, b) => `${a.requirementId}\0${a.childId}\0${a.role}`.localeCompare(`${b.requirementId}\0${b.childId}\0${b.role}`)),
|
|
353
|
+
children: [...input.children].map((child) => ({ ...child, allowedPaths: sortStrings(child.allowedPaths), writeSet: sortStrings(child.writeSet) })).sort((a, b) => a.childId.localeCompare(b.childId)),
|
|
354
|
+
dependencies: [...input.dependencies].sort((a, b) => `${a.childId}\0${a.dependsOn}`.localeCompare(`${b.childId}\0${b.dependsOn}`)),
|
|
355
|
+
sharedSurfaces: [...input.sharedSurfaces].map((surface) => ({ ...surface, childIds: sortStrings(surface.childIds) })).sort((a, b) => a.path.localeCompare(b.path)),
|
|
356
|
+
integration: { ...input.integration, allowedPaths: sortStrings(input.integration.allowedPaths), writeSet: sortStrings(input.integration.writeSet), verifyRefs: sortStrings(input.integration.verifyRefs) },
|
|
357
|
+
verifyRefs: sortStrings(input.verifyRefs),
|
|
358
|
+
closeoutRefs: sortStrings(input.closeoutRefs),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export function buildFrontendSplitManifest(raw) {
|
|
362
|
+
const parsed = frontendSplitManifestInputSchema.parse(raw);
|
|
363
|
+
const canonical = canonicalManifestInput(parsed);
|
|
364
|
+
const topologicalOrder = validateManifest(canonical);
|
|
365
|
+
const withoutDigest = { ...canonical, topologicalOrder };
|
|
366
|
+
return frontendSplitManifestStructuralSchema.parse({ ...withoutDigest, digest: computeFrontendSplitDigest(withoutDigest) });
|
|
367
|
+
}
|
|
368
|
+
export const frontendSplitManifestSchema = frontendSplitManifestStructuralSchema.superRefine((value, ctx) => {
|
|
369
|
+
try {
|
|
370
|
+
const { digest, topologicalOrder, ...input } = value;
|
|
371
|
+
const rebuilt = buildFrontendSplitManifest(input);
|
|
372
|
+
if (canonicalFrontendSplitJson(topologicalOrder) !== canonicalFrontendSplitJson(rebuilt.topologicalOrder) || digest !== rebuilt.digest)
|
|
373
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "manifest canonical order or digest mismatch" });
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: error instanceof Error ? error.message : "invalid manifest" });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
export function parseFrontendSplitManifest(value) {
|
|
380
|
+
return frontendSplitManifestSchema.parse(value);
|
|
381
|
+
}
|
|
382
|
+
const operationExecutionBaseFields = {
|
|
383
|
+
operationId: idSchema,
|
|
384
|
+
operationRevision: positiveIntegerSchema,
|
|
385
|
+
sourceRunId: idSchema,
|
|
386
|
+
sourceDigest: digestSchema,
|
|
387
|
+
};
|
|
388
|
+
const singleTaskExecutionInputSchema = z.object({ kind: z.literal("single-task"), ...operationExecutionBaseFields }).strict();
|
|
389
|
+
const splitParentExecutionInputSchema = z.object({ kind: z.literal("split-parent"), ...operationExecutionBaseFields, generation: positiveIntegerSchema, parentRecordDigest: digestSchema }).strict();
|
|
390
|
+
export const frontendOperationExecutionInputSchema = z.discriminatedUnion("kind", [singleTaskExecutionInputSchema, splitParentExecutionInputSchema]);
|
|
391
|
+
const singleTaskExecutionSchema = singleTaskExecutionInputSchema.extend({ digest: digestSchema }).strict();
|
|
392
|
+
const splitParentExecutionSchema = splitParentExecutionInputSchema.extend({ digest: digestSchema }).strict();
|
|
393
|
+
const frontendOperationExecutionStructuralSchema = z.discriminatedUnion("kind", [singleTaskExecutionSchema, splitParentExecutionSchema]);
|
|
394
|
+
export const frontendOperationExecutionSchema = frontendOperationExecutionStructuralSchema.superRefine((value, ctx) => {
|
|
395
|
+
const { digest, ...payload } = value;
|
|
396
|
+
if (computeFrontendSplitDigest(payload) !== digest)
|
|
397
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "operation execution digest mismatch" });
|
|
398
|
+
});
|
|
399
|
+
export function buildFrontendOperationExecution(raw) {
|
|
400
|
+
const payload = frontendOperationExecutionInputSchema.parse(raw);
|
|
401
|
+
return frontendOperationExecutionStructuralSchema.parse({ ...payload, digest: computeFrontendSplitDigest(payload) });
|
|
402
|
+
}
|
|
403
|
+
export function parseFrontendOperationExecution(value) {
|
|
404
|
+
return frontendOperationExecutionSchema.parse(value);
|
|
405
|
+
}
|
|
406
|
+
export const frontendPendingBlockRefSchema = z.object({ generation: positiveIntegerSchema, childId: idSchema, operationId: idSchema, kind: z.enum(["human", "external"]), ref: idSchema, digest: digestSchema }).strict();
|
|
407
|
+
export function parseFrontendPendingBlockRef(value) {
|
|
408
|
+
return frontendPendingBlockRefSchema.parse(value);
|
|
409
|
+
}
|
|
410
|
+
const committedOperationStateFactSchema = z.object({
|
|
411
|
+
operationId: idSchema,
|
|
412
|
+
state: frontendOperationStateSchema,
|
|
413
|
+
stateDigest: digestSchema,
|
|
414
|
+
fact: frontendCommittedFactRefSchema,
|
|
415
|
+
}).strict();
|
|
416
|
+
const authoritativeOperationStateFactSchema = committedOperationStateFactSchema.extend({
|
|
417
|
+
authoritativeRevision: positiveIntegerSchema,
|
|
418
|
+
authoritativeSequence: nonnegativeIntegerSchema,
|
|
419
|
+
}).strict();
|
|
420
|
+
const childObservationSchema = z.object({
|
|
421
|
+
childId: idSchema,
|
|
422
|
+
operationId: idSchema,
|
|
423
|
+
authoritativeRevision: positiveIntegerSchema,
|
|
424
|
+
authoritativeSequence: nonnegativeIntegerSchema,
|
|
425
|
+
state: frontendOperationStateSchema,
|
|
426
|
+
stateDigest: digestSchema,
|
|
427
|
+
observationFact: frontendCommittedFactRefSchema,
|
|
428
|
+
candidateHash: digestSchema.optional(),
|
|
429
|
+
closeoutRef: frontendCommittedFactRefSchema.optional(),
|
|
430
|
+
blockRef: frontendPendingBlockRefSchema.optional(),
|
|
431
|
+
}).strict().superRefine((value, ctx) => {
|
|
432
|
+
if (value.state === "success" && (!value.candidateHash || !value.closeoutRef))
|
|
433
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "successful child requires candidateHash and closeoutRef" });
|
|
434
|
+
if (value.state !== "success" && (value.candidateHash || value.closeoutRef))
|
|
435
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "only successful child carries candidate output" });
|
|
436
|
+
});
|
|
437
|
+
const parentRecordFields = {
|
|
438
|
+
schemaVersion: z.literal(1), generation: positiveIntegerSchema, operationId: idSchema, operationRevision: positiveIntegerSchema,
|
|
439
|
+
sourceRunId: idSchema, sourceDigest: digestSchema, manifestDigest: digestSchema,
|
|
440
|
+
operationState: frontendOperationStateSchema, operationTerminal: frontendOperationTerminalSchema.optional(),
|
|
441
|
+
phase: z.enum(["materializing-children", "awaiting-children", "join-complete", "awaiting-integration", "integration-complete", "closed"]),
|
|
442
|
+
children: z.array(z.object({ childId: idSchema, operationId: idSchema }).strict()),
|
|
443
|
+
observations: z.array(childObservationSchema), pendingBlockRef: frontendPendingBlockRefSchema.optional(),
|
|
444
|
+
};
|
|
445
|
+
const parentRecordStructuralSchema = z.object({ ...parentRecordFields, digest: digestSchema }).strict();
|
|
446
|
+
function parentWithoutDigest(parent) {
|
|
447
|
+
const { digest: _digest, ...rest } = parent;
|
|
448
|
+
return rest;
|
|
449
|
+
}
|
|
450
|
+
function buildParent(value) {
|
|
451
|
+
const normalized = { ...value, children: [...value.children].sort((a, b) => a.childId.localeCompare(b.childId)), observations: [...value.observations].sort((a, b) => a.childId.localeCompare(b.childId)) };
|
|
452
|
+
return parentRecordStructuralSchema.parse({ ...normalized, digest: computeFrontendSplitDigest(normalized) });
|
|
453
|
+
}
|
|
454
|
+
export const frontendSplitParentRecordSchema = parentRecordStructuralSchema.superRefine((value, ctx) => {
|
|
455
|
+
const expected = computeFrontendSplitDigest(parentWithoutDigest(value));
|
|
456
|
+
if (expected !== value.digest)
|
|
457
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "parent digest mismatch" });
|
|
458
|
+
if (isFrontendOperationTerminalState(value.operationState) ? value.operationTerminal !== value.operationState : value.operationTerminal !== undefined)
|
|
459
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["operationTerminal"], message: "terminal/state mismatch" });
|
|
460
|
+
if (new Set(value.children.map((item) => item.childId)).size !== value.children.length || new Set(value.children.map((item) => item.operationId)).size !== value.children.length)
|
|
461
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["children"], message: "duplicate parent child binding" });
|
|
462
|
+
if (new Set(value.observations.map((item) => item.childId)).size !== value.observations.length)
|
|
463
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observations"], message: "duplicate parent observation" });
|
|
464
|
+
if (new Set(value.observations.map((item) => `${item.observationFact.ref}\0${item.observationFact.digest}`)).size !== value.observations.length)
|
|
465
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observations"], message: "authoritative fact identity must be unique" });
|
|
466
|
+
for (const [index, observation] of value.observations.entries()) {
|
|
467
|
+
if (!value.children.some((item) => item.childId === observation.childId && item.operationId === observation.operationId))
|
|
468
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observations", index], message: "observation child binding mismatch" });
|
|
469
|
+
if (observation.blockRef && (observation.blockRef.generation !== value.generation || observation.blockRef.childId !== observation.childId || observation.blockRef.operationId !== observation.operationId || (observation.state === "blocked-human" ? observation.blockRef.kind !== "human" : observation.state === "blocked-external" ? observation.blockRef.kind !== "external" : true)))
|
|
470
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observations", index, "blockRef"], message: "observation block binding mismatch" });
|
|
471
|
+
if ((observation.state === "blocked-human" || observation.state === "blocked-external") !== (observation.blockRef !== undefined))
|
|
472
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observations", index, "blockRef"], message: "blocked state/ref mismatch" });
|
|
473
|
+
}
|
|
474
|
+
if (value.pendingBlockRef && (value.pendingBlockRef.generation !== value.generation || !value.observations.some((item) => item.blockRef && canonicalFrontendSplitJson(item.blockRef) === canonicalFrontendSplitJson(value.pendingBlockRef))))
|
|
475
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pendingBlockRef"], message: "pending block binding mismatch" });
|
|
476
|
+
if (canonicalFrontendSplitJson(value.children) !== canonicalFrontendSplitJson([...value.children].sort((a, b) => a.childId.localeCompare(b.childId))) || canonicalFrontendSplitJson(value.observations) !== canonicalFrontendSplitJson([...value.observations].sort((a, b) => a.childId.localeCompare(b.childId))))
|
|
477
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "parent arrays are not canonical" });
|
|
478
|
+
});
|
|
479
|
+
export function parseFrontendSplitParentRecord(value) { return frontendSplitParentRecordSchema.parse(value); }
|
|
480
|
+
const splitHumanEnvelopeSchema = frontendHumanDecisionEnvelopeSchema.refine((value) => value.kind === "split-materialization-approval", "human split authorization requires a split-materialization-approval envelope");
|
|
481
|
+
const splitHumanReceiptSchema = frontendHumanDecisionReceiptSchema.refine((value) => value.kind === "split-materialization-approval", "human split authorization requires split-materialization-approval");
|
|
482
|
+
const authorizationSchema = z.discriminatedUnion("mode", [
|
|
483
|
+
z.object({ mode: z.literal("automatic"), authorized: z.literal(true) }).strict(),
|
|
484
|
+
z.object({ mode: z.literal("human"), envelope: splitHumanEnvelopeSchema, receipt: splitHumanReceiptSchema }).strict(),
|
|
485
|
+
]);
|
|
486
|
+
export const initializeFrontendSplitParentInputSchema = z.object({
|
|
487
|
+
execution: frontendOperationExecutionSchema,
|
|
488
|
+
manifest: frontendSplitManifestSchema,
|
|
489
|
+
expectedOperationRevision: positiveIntegerSchema,
|
|
490
|
+
successorOperationRevision: positiveIntegerSchema.optional(),
|
|
491
|
+
envelopeTargetDigest: digestSchema,
|
|
492
|
+
activeWriterOperationId: idSchema.nullable(),
|
|
493
|
+
activeProviderOperationId: idSchema.nullable(),
|
|
494
|
+
authorization: authorizationSchema,
|
|
495
|
+
}).strict();
|
|
496
|
+
export function initializeFrontendSplitParent(raw) {
|
|
497
|
+
const input = initializeFrontendSplitParentInputSchema.parse(raw);
|
|
498
|
+
const execution = input.execution;
|
|
499
|
+
if (execution.kind !== "single-task")
|
|
500
|
+
throw new Error("split-parent cannot be converted or initialized again");
|
|
501
|
+
const manifest = input.manifest;
|
|
502
|
+
const rejected = (reason) => ({ outcome: "rejected", reason });
|
|
503
|
+
if (execution.operationId !== manifest.parentOperationId)
|
|
504
|
+
return rejected("operation binding mismatch");
|
|
505
|
+
if (execution.sourceRunId !== manifest.sourceRunId || execution.sourceDigest !== manifest.sourceDigest)
|
|
506
|
+
return rejected("source binding mismatch");
|
|
507
|
+
if (execution.operationRevision !== input.expectedOperationRevision)
|
|
508
|
+
return rejected("operation revision CAS mismatch");
|
|
509
|
+
const successorOperationRevision = input.successorOperationRevision ?? execution.operationRevision + 1;
|
|
510
|
+
if (successorOperationRevision <= execution.operationRevision)
|
|
511
|
+
return rejected("successor operation revision must advance");
|
|
512
|
+
if (input.envelopeTargetDigest !== manifest.digest)
|
|
513
|
+
return rejected("envelope target digest mismatch");
|
|
514
|
+
if (input.activeWriterOperationId !== null || input.activeProviderOperationId !== null)
|
|
515
|
+
return rejected("active writer/provider exists");
|
|
516
|
+
if (input.authorization.mode === "human") {
|
|
517
|
+
const { envelope, receipt } = input.authorization;
|
|
518
|
+
if (envelope.kind !== "split-materialization-approval" || envelope.resumeAction !== "materialize-split" || envelope.operationId !== execution.operationId || envelope.runId !== execution.sourceRunId || envelope.operationRevision !== execution.operationRevision || envelope.targetDigest !== manifest.digest || envelope.payload.splitManifestRef.digest !== manifest.digest)
|
|
519
|
+
return rejected("split envelope binding mismatch");
|
|
520
|
+
if (receipt.kind !== "split-materialization-approval" || receipt.resumeAction !== "materialize-split" || receipt.commitState !== "committed" || receipt.consumeState !== "unconsumed")
|
|
521
|
+
return rejected("invalid split receipt state");
|
|
522
|
+
if (receipt.envelopeDigest !== envelope.digest || receipt.operationId !== execution.operationId || receipt.runId !== execution.sourceRunId || receipt.operationRevision !== execution.operationRevision || receipt.targetDigest !== manifest.digest)
|
|
523
|
+
return rejected("split receipt binding mismatch");
|
|
524
|
+
if (receipt.resolution.splitManifestRef.ref !== envelope.payload.splitManifestRef.ref || receipt.resolution.splitManifestRef.digest !== manifest.digest)
|
|
525
|
+
return rejected("split receipt manifest mismatch");
|
|
526
|
+
}
|
|
527
|
+
const parent = buildParent({ schemaVersion: 1, generation: 1, operationId: execution.operationId, operationRevision: successorOperationRevision, sourceRunId: execution.sourceRunId, sourceDigest: execution.sourceDigest, manifestDigest: manifest.digest, operationState: "orchestrating-split", phase: "materializing-children", children: manifest.children.map(({ childId, operationId }) => ({ childId, operationId })), observations: [] });
|
|
528
|
+
return { outcome: "applied", parent, execution: buildFrontendOperationExecution({ kind: "split-parent", operationId: execution.operationId, operationRevision: parent.operationRevision, sourceRunId: execution.sourceRunId, sourceDigest: execution.sourceDigest, generation: 1, parentRecordDigest: parent.digest }) };
|
|
529
|
+
}
|
|
530
|
+
export const frontendSplitChildObservationSchema = z.object({
|
|
531
|
+
observedGeneration: nonnegativeIntegerSchema,
|
|
532
|
+
childId: idSchema,
|
|
533
|
+
operationId: idSchema,
|
|
534
|
+
authoritativeRevision: positiveIntegerSchema,
|
|
535
|
+
authoritativeSequence: nonnegativeIntegerSchema,
|
|
536
|
+
state: frontendOperationStateSchema,
|
|
537
|
+
stateDigest: digestSchema,
|
|
538
|
+
stateFact: authoritativeOperationStateFactSchema,
|
|
539
|
+
envelopeRef: frontendCommittedFactRefSchema.optional(),
|
|
540
|
+
externalActionRef: frontendCommittedFactRefSchema.optional(),
|
|
541
|
+
candidateHash: digestSchema.optional(),
|
|
542
|
+
closeoutRef: frontendCommittedFactRefSchema.optional(),
|
|
543
|
+
}).strict().superRefine((value, ctx) => {
|
|
544
|
+
if (value.state === "blocked-human" && !value.envelopeRef)
|
|
545
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["envelopeRef"], message: "blocked-human requires envelopeRef" });
|
|
546
|
+
if (value.state !== "blocked-human" && value.envelopeRef)
|
|
547
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["envelopeRef"], message: "only blocked-human may carry envelopeRef" });
|
|
548
|
+
if (value.state === "blocked-external" && !value.externalActionRef)
|
|
549
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["externalActionRef"], message: "blocked-external requires externalActionRef" });
|
|
550
|
+
if (value.state !== "blocked-external" && value.externalActionRef)
|
|
551
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["externalActionRef"], message: "only blocked-external may carry externalActionRef" });
|
|
552
|
+
if (value.state === "success" && (!value.candidateHash || !value.closeoutRef))
|
|
553
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "successful child requires candidateHash and closeoutRef" });
|
|
554
|
+
if (value.state !== "success" && (value.candidateHash || value.closeoutRef))
|
|
555
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "only successful child carries candidate output" });
|
|
556
|
+
});
|
|
557
|
+
const frontendSplitStaleAuditFactStructuralSchema = z.object({
|
|
558
|
+
kind: z.literal("frontend-split-stale-observation"),
|
|
559
|
+
reason: idSchema,
|
|
560
|
+
parentDigest: digestSchema,
|
|
561
|
+
observedGeneration: nonnegativeIntegerSchema,
|
|
562
|
+
childId: idSchema,
|
|
563
|
+
operationId: idSchema,
|
|
564
|
+
authoritativeRevision: positiveIntegerSchema,
|
|
565
|
+
authoritativeSequence: nonnegativeIntegerSchema,
|
|
566
|
+
authoritativeFactRef: idSchema,
|
|
567
|
+
authoritativeFactDigest: digestSchema,
|
|
568
|
+
digest: digestSchema,
|
|
569
|
+
}).strict();
|
|
570
|
+
export const frontendSplitStaleAuditFactSchema = frontendSplitStaleAuditFactStructuralSchema.superRefine((value, ctx) => {
|
|
571
|
+
const { digest, ...payload } = value;
|
|
572
|
+
if (computeFrontendSplitDigest(payload) !== digest)
|
|
573
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "stale audit digest mismatch" });
|
|
574
|
+
});
|
|
575
|
+
export function parseFrontendSplitStaleAuditFact(value) {
|
|
576
|
+
return frontendSplitStaleAuditFactSchema.parse(value);
|
|
577
|
+
}
|
|
578
|
+
function stale(parent, observation, reason) {
|
|
579
|
+
const payload = { kind: "frontend-split-stale-observation", reason, parentDigest: parent.digest, observedGeneration: observation.observedGeneration, childId: observation.childId, operationId: observation.operationId, authoritativeRevision: observation.authoritativeRevision, authoritativeSequence: observation.authoritativeSequence, authoritativeFactRef: observation.stateFact.fact.ref, authoritativeFactDigest: observation.stateFact.fact.digest };
|
|
580
|
+
return { outcome: "ignored-stale", parent, auditFact: { ...payload, digest: computeFrontendSplitDigest(payload) } };
|
|
581
|
+
}
|
|
582
|
+
function selectedBlock(observations, manifest) {
|
|
583
|
+
const rank = new Map(manifest.topologicalOrder.map((id, index) => [id, index]));
|
|
584
|
+
return observations.filter((item) => item.blockRef).sort((a, b) => (rank.get(a.childId) - rank.get(b.childId)) || a.operationId.localeCompare(b.operationId))[0]?.blockRef;
|
|
585
|
+
}
|
|
586
|
+
const terminalPriority = ["failed-internal", "non-converging", "cancelled", "superseded"];
|
|
587
|
+
export function observeFrontendSplitChild(parentValue, manifestValue, rawValue) {
|
|
588
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
589
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
590
|
+
const raw = frontendSplitChildObservationSchema.parse(rawValue);
|
|
591
|
+
if (parent.operationTerminal !== undefined)
|
|
592
|
+
return stale(parent, raw, "parent is terminal");
|
|
593
|
+
if (parent.phase !== "materializing-children" && parent.phase !== "awaiting-children")
|
|
594
|
+
return stale(parent, raw, "parent child observations are frozen");
|
|
595
|
+
if (raw.observedGeneration !== parent.generation)
|
|
596
|
+
return stale(parent, raw, "generation mismatch");
|
|
597
|
+
if (parent.manifestDigest !== manifest.digest || parent.sourceDigest !== manifest.sourceDigest || parent.sourceRunId !== manifest.sourceRunId)
|
|
598
|
+
return stale(parent, raw, "parent binding drift");
|
|
599
|
+
const registered = parent.children.find((item) => item.childId === raw.childId);
|
|
600
|
+
if (!registered || registered.operationId !== raw.operationId)
|
|
601
|
+
return stale(parent, raw, "unknown child or operation mismatch");
|
|
602
|
+
const stateFact = authoritativeOperationStateFactSchema.safeParse(raw.stateFact);
|
|
603
|
+
if (!digestSchema.safeParse(raw.stateDigest).success || !stateFact.success)
|
|
604
|
+
return stale(parent, raw, "invalid state digest or state fact");
|
|
605
|
+
if (stateFact.data.operationId !== raw.operationId || stateFact.data.state !== raw.state || stateFact.data.stateDigest !== raw.stateDigest)
|
|
606
|
+
return stale(parent, raw, "authoritative child state fact mismatch");
|
|
607
|
+
if (stateFact.data.authoritativeRevision !== raw.authoritativeRevision || stateFact.data.authoritativeSequence !== raw.authoritativeSequence)
|
|
608
|
+
return stale(parent, raw, "authoritative fact position mismatch");
|
|
609
|
+
let blockRef;
|
|
610
|
+
if (raw.state === "blocked-human") {
|
|
611
|
+
const ref = frontendCommittedFactRefSchema.parse(raw.envelopeRef);
|
|
612
|
+
blockRef = { generation: parent.generation, childId: raw.childId, operationId: raw.operationId, kind: "human", ref: ref.ref, digest: ref.digest };
|
|
613
|
+
}
|
|
614
|
+
else if (raw.state === "blocked-external") {
|
|
615
|
+
const ref = frontendCommittedFactRefSchema.parse(raw.externalActionRef);
|
|
616
|
+
blockRef = { generation: parent.generation, childId: raw.childId, operationId: raw.operationId, kind: "external", ref: ref.ref, digest: ref.digest };
|
|
617
|
+
}
|
|
618
|
+
const observation = childObservationSchema.parse({ childId: raw.childId, operationId: raw.operationId, authoritativeRevision: stateFact.data.authoritativeRevision, authoritativeSequence: stateFact.data.authoritativeSequence, state: raw.state, stateDigest: raw.stateDigest, observationFact: stateFact.data.fact, candidateHash: raw.candidateHash, closeoutRef: raw.closeoutRef, blockRef });
|
|
619
|
+
const existing = parent.observations.find((item) => item.childId === raw.childId);
|
|
620
|
+
if (existing && canonicalFrontendSplitJson(existing) === canonicalFrontendSplitJson(observation))
|
|
621
|
+
return stale(parent, raw, "authoritative fact replay");
|
|
622
|
+
if (parent.observations.some((item) => item.observationFact.ref === observation.observationFact.ref && item.observationFact.digest === observation.observationFact.digest))
|
|
623
|
+
return stale(parent, raw, "authoritative fact identity reused");
|
|
624
|
+
if (existing) {
|
|
625
|
+
const older = raw.authoritativeRevision < existing.authoritativeRevision || (raw.authoritativeRevision === existing.authoritativeRevision && raw.authoritativeSequence < existing.authoritativeSequence);
|
|
626
|
+
const samePosition = raw.authoritativeRevision === existing.authoritativeRevision && raw.authoritativeSequence === existing.authoritativeSequence;
|
|
627
|
+
if (older || samePosition)
|
|
628
|
+
return stale(parent, raw, samePosition ? "conflicting authoritative fact position" : "older authoritative fact");
|
|
629
|
+
if (isFrontendOperationTerminalState(existing.state))
|
|
630
|
+
return stale(parent, raw, "terminal child observation is irreversible");
|
|
631
|
+
}
|
|
632
|
+
const observations = [...parent.observations.filter((item) => item.childId !== raw.childId), observation];
|
|
633
|
+
const terminal = terminalPriority.find((state) => observations.some((item) => item.state === state));
|
|
634
|
+
const pendingBlockRef = selectedBlock(observations, manifest);
|
|
635
|
+
const operationState = terminal ?? (pendingBlockRef?.kind === "human" ? "blocked-human" : pendingBlockRef ? "blocked-external" : "orchestrating-split");
|
|
636
|
+
const next = buildParent({ ...parentWithoutDigest(parent), operationRevision: parent.operationRevision + 1, observations, pendingBlockRef, operationState, operationTerminal: terminal, phase: parent.phase === "materializing-children" ? "awaiting-children" : parent.phase });
|
|
637
|
+
return { outcome: "applied", parent: next };
|
|
638
|
+
}
|
|
639
|
+
const candidateItemSchema = z.object({ childId: idSchema, operationId: idSchema, topologyRank: positiveIntegerSchema, generation: positiveIntegerSchema, candidateHash: digestSchema, closeoutRef: frontendCommittedFactRefSchema, stateDigest: digestSchema }).strict();
|
|
640
|
+
const candidateBindingStructuralSchema = z.object({ schemaVersion: z.literal(1), generation: positiveIntegerSchema, sourceDigest: digestSchema, manifestDigest: digestSchema, parentDigest: digestSchema, children: z.array(candidateItemSchema), digest: digestSchema }).strict();
|
|
641
|
+
export const frontendSplitCandidateBindingSchema = candidateBindingStructuralSchema.superRefine((value, ctx) => {
|
|
642
|
+
const { digest, ...payload } = value;
|
|
643
|
+
if (computeFrontendSplitDigest(payload) !== digest)
|
|
644
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "candidate binding digest mismatch" });
|
|
645
|
+
if (new Set(value.children.map((item) => item.childId)).size !== value.children.length)
|
|
646
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["children"], message: "duplicate candidate child" });
|
|
647
|
+
if (new Set(value.children.map((item) => item.operationId)).size !== value.children.length)
|
|
648
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["children"], message: "duplicate candidate operation" });
|
|
649
|
+
for (const [index, child] of value.children.entries()) {
|
|
650
|
+
if (child.generation !== value.generation)
|
|
651
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["children", index, "generation"], message: "candidate child generation mismatch" });
|
|
652
|
+
if (child.topologyRank !== index + 1)
|
|
653
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["children", index, "topologyRank"], message: "candidate topology ranks must be contiguous from one" });
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
export function parseFrontendSplitCandidateBinding(value) {
|
|
657
|
+
return frontendSplitCandidateBindingSchema.parse(value);
|
|
658
|
+
}
|
|
659
|
+
export function frontendSplitDependencyClosure(manifestValue, childId) {
|
|
660
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
661
|
+
if (!manifest.children.some((child) => child.childId === childId))
|
|
662
|
+
throw new Error(`unknown child: ${childId}`);
|
|
663
|
+
const dependencies = new Map();
|
|
664
|
+
for (const edge of manifest.dependencies) {
|
|
665
|
+
const prerequisites = dependencies.get(edge.childId) ?? [];
|
|
666
|
+
prerequisites.push(edge.dependsOn);
|
|
667
|
+
dependencies.set(edge.childId, prerequisites);
|
|
668
|
+
}
|
|
669
|
+
const closure = new Set();
|
|
670
|
+
const pending = [...(dependencies.get(childId) ?? [])];
|
|
671
|
+
while (pending.length > 0) {
|
|
672
|
+
const prerequisiteId = pending.pop();
|
|
673
|
+
if (closure.has(prerequisiteId))
|
|
674
|
+
continue;
|
|
675
|
+
closure.add(prerequisiteId);
|
|
676
|
+
pending.push(...(dependencies.get(prerequisiteId) ?? []));
|
|
677
|
+
}
|
|
678
|
+
return manifest.topologicalOrder.filter((id) => closure.has(id));
|
|
679
|
+
}
|
|
680
|
+
export function isFrontendSplitChildReady(parentValue, manifestValue, childId) {
|
|
681
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
682
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
683
|
+
if (parent.operationTerminal !== undefined || parent.operationState !== "orchestrating-split" || parent.generation < 1 || parent.manifestDigest !== manifest.digest || parent.sourceDigest !== manifest.sourceDigest)
|
|
684
|
+
return false;
|
|
685
|
+
return frontendSplitDependencyClosure(manifest, childId).every((dependencyId) => {
|
|
686
|
+
const child = manifest.children.find((item) => item.childId === dependencyId);
|
|
687
|
+
const observation = parent.observations.find((item) => item.childId === dependencyId);
|
|
688
|
+
return observation?.operationId === child.operationId && observation.state === "success" && observation.observationFact.commitState === "committed" && observation.closeoutRef?.commitState === "committed" && observation.candidateHash !== undefined;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
function requiredDependencyClosure(manifest) {
|
|
692
|
+
const dependencies = new Map();
|
|
693
|
+
for (const edge of manifest.dependencies) {
|
|
694
|
+
const prerequisites = dependencies.get(edge.childId) ?? [];
|
|
695
|
+
prerequisites.push(edge.dependsOn);
|
|
696
|
+
dependencies.set(edge.childId, prerequisites);
|
|
697
|
+
}
|
|
698
|
+
const closure = new Set(manifest.children.filter((child) => child.required).map((child) => child.childId));
|
|
699
|
+
const pending = [...closure];
|
|
700
|
+
while (pending.length > 0) {
|
|
701
|
+
const childId = pending.pop();
|
|
702
|
+
for (const prerequisiteId of dependencies.get(childId) ?? []) {
|
|
703
|
+
if (closure.has(prerequisiteId))
|
|
704
|
+
continue;
|
|
705
|
+
closure.add(prerequisiteId);
|
|
706
|
+
pending.push(prerequisiteId);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return closure;
|
|
710
|
+
}
|
|
711
|
+
function candidateMatchesParent(candidate, parent, manifest) {
|
|
712
|
+
const closure = requiredDependencyClosure(manifest);
|
|
713
|
+
const required = manifest.topologicalOrder
|
|
714
|
+
.filter((childId) => closure.has(childId))
|
|
715
|
+
.map((childId) => manifest.children.find((child) => child.childId === childId));
|
|
716
|
+
if (candidate.children.length !== required.length)
|
|
717
|
+
return false;
|
|
718
|
+
return required.every((child, index) => {
|
|
719
|
+
const item = candidate.children[index];
|
|
720
|
+
const observation = parent.observations.find((observed) => observed.childId === child.childId);
|
|
721
|
+
return item !== undefined && item.childId === child.childId && item.operationId === child.operationId && item.topologyRank === index + 1 && item.generation === parent.generation && observation !== undefined && observation.operationId === child.operationId && observation.state === "success" && item.stateDigest === observation.stateDigest && item.candidateHash === observation.candidateHash && item.closeoutRef.ref === observation.closeoutRef?.ref && item.closeoutRef.digest === observation.closeoutRef.digest;
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
export function prepareFrontendSplitJoin(parentValue, manifestValue) {
|
|
725
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
726
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
727
|
+
if (parent.generation < 1 || parent.sourceDigest !== manifest.sourceDigest || parent.manifestDigest !== manifest.digest || parent.operationState !== "orchestrating-split" || parent.operationTerminal !== undefined || parent.pendingBlockRef !== undefined || (parent.phase !== "materializing-children" && parent.phase !== "awaiting-children"))
|
|
728
|
+
return { outcome: "not-ready", reason: "stale or non-runnable parent binding" };
|
|
729
|
+
const closure = requiredDependencyClosure(manifest);
|
|
730
|
+
for (const childId of manifest.topologicalOrder) {
|
|
731
|
+
if (!closure.has(childId))
|
|
732
|
+
continue;
|
|
733
|
+
const child = manifest.children.find((item) => item.childId === childId);
|
|
734
|
+
const observation = parent.observations.find((item) => item.childId === childId);
|
|
735
|
+
if (!observation || observation.operationId !== child.operationId || observation.state !== "success" || !observation.candidateHash || !observation.closeoutRef || observation.observationFact.commitState !== "committed" || observation.closeoutRef.commitState !== "committed") {
|
|
736
|
+
return { outcome: "not-ready", reason: `${child.required ? "required" : "dependency"} child ${childId} is incomplete` };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const children = [];
|
|
740
|
+
for (const childId of manifest.topologicalOrder) {
|
|
741
|
+
if (!closure.has(childId))
|
|
742
|
+
continue;
|
|
743
|
+
const child = manifest.children.find((item) => item.childId === childId);
|
|
744
|
+
const observation = parent.observations.find((item) => item.childId === childId);
|
|
745
|
+
children.push({ childId, operationId: child.operationId, topologyRank: children.length + 1, generation: parent.generation, candidateHash: observation.candidateHash, closeoutRef: observation.closeoutRef, stateDigest: observation.stateDigest });
|
|
746
|
+
}
|
|
747
|
+
const nextParent = buildParent({ ...parentWithoutDigest(parent), operationRevision: parent.operationRevision + 1, phase: manifest.integration.required ? "awaiting-integration" : "join-complete" });
|
|
748
|
+
const payload = { schemaVersion: 1, generation: parent.generation, sourceDigest: parent.sourceDigest, manifestDigest: manifest.digest, parentDigest: nextParent.digest, children };
|
|
749
|
+
return { outcome: "ready", parent: nextParent, binding: candidateBindingStructuralSchema.parse({ ...payload, digest: computeFrontendSplitDigest(payload) }) };
|
|
750
|
+
}
|
|
751
|
+
const integrationBindingStructuralSchema = z.object({ schemaVersion: z.literal(1), generation: positiveIntegerSchema, sourceDigest: digestSchema, manifestDigest: digestSchema, candidateBindingsDigest: digestSchema, parentDigest: digestSchema, operationId: idSchema, state: z.literal("success"), stateDigest: digestSchema, observationFact: frontendCommittedFactRefSchema, closeoutRef: frontendCommittedFactRefSchema, digest: digestSchema }).strict();
|
|
752
|
+
export const frontendSplitIntegrationBindingSchema = integrationBindingStructuralSchema.superRefine((value, ctx) => { const { digest, ...payload } = value; if (computeFrontendSplitDigest(payload) !== digest)
|
|
753
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "integration binding digest mismatch" }); });
|
|
754
|
+
export function parseFrontendSplitIntegrationBinding(value) {
|
|
755
|
+
return frontendSplitIntegrationBindingSchema.parse(value);
|
|
756
|
+
}
|
|
757
|
+
export const completeFrontendSplitIntegrationInputSchema = z.object({
|
|
758
|
+
observedGeneration: nonnegativeIntegerSchema,
|
|
759
|
+
operationId: idSchema,
|
|
760
|
+
state: frontendOperationStateSchema,
|
|
761
|
+
stateDigest: digestSchema,
|
|
762
|
+
candidateBindingsDigest: digestSchema,
|
|
763
|
+
stateFact: committedOperationStateFactSchema,
|
|
764
|
+
closeoutRef: frontendCommittedFactRefSchema,
|
|
765
|
+
}).strict();
|
|
766
|
+
export function completeFrontendSplitIntegration(parentValue, manifestValue, candidateValue, rawInput) {
|
|
767
|
+
const input = completeFrontendSplitIntegrationInputSchema.parse(rawInput);
|
|
768
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
769
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
770
|
+
const candidate = frontendSplitCandidateBindingSchema.parse(candidateValue);
|
|
771
|
+
if (!manifest.integration.required || !manifest.integration.operationId)
|
|
772
|
+
return { outcome: "ignored-stale", reason: "integration is not required" };
|
|
773
|
+
if (input.observedGeneration !== parent.generation || candidate.generation !== parent.generation || parent.manifestDigest !== manifest.digest || candidate.manifestDigest !== manifest.digest || candidate.sourceDigest !== parent.sourceDigest || candidate.parentDigest !== parent.digest || !candidateMatchesParent(candidate, parent, manifest) || parent.phase !== "awaiting-integration" || parent.operationState !== "orchestrating-split" || input.candidateBindingsDigest !== candidate.digest || input.operationId !== manifest.integration.operationId)
|
|
774
|
+
return { outcome: "ignored-stale", reason: "integration freshness mismatch" };
|
|
775
|
+
const stateFact = committedOperationStateFactSchema.safeParse(input.stateFact);
|
|
776
|
+
if (input.state !== "success" || !stateFact.success || stateFact.data.operationId !== input.operationId || stateFact.data.state !== "success" || stateFact.data.stateDigest !== input.stateDigest || !frontendCommittedFactRefSchema.safeParse(input.closeoutRef).success || !digestSchema.safeParse(input.stateDigest).success)
|
|
777
|
+
return { outcome: "ignored-stale", reason: "integration did not commit authoritative success" };
|
|
778
|
+
const nextParent = buildParent({ ...parentWithoutDigest(parent), operationRevision: parent.operationRevision + 1, phase: "integration-complete" });
|
|
779
|
+
const payload = { schemaVersion: 1, generation: parent.generation, sourceDigest: parent.sourceDigest, manifestDigest: manifest.digest, candidateBindingsDigest: candidate.digest, parentDigest: nextParent.digest, operationId: input.operationId, state: "success", stateDigest: input.stateDigest, observationFact: stateFact.data.fact, closeoutRef: input.closeoutRef };
|
|
780
|
+
return { outcome: "accepted", parent: nextParent, binding: integrationBindingStructuralSchema.parse({ ...payload, digest: computeFrontendSplitDigest(payload) }) };
|
|
781
|
+
}
|
|
782
|
+
export const frontendSplitChildCloseoutFactsSchema = z.object({ childId: idSchema, residualRisks: z.array(z.string().min(1)), frontendTestHandoffRef: idSchema.optional() }).strict();
|
|
783
|
+
const aggregateStructuralSchema = z.object({ schemaVersion: z.literal(1), generation: positiveIntegerSchema, parentOperationId: idSchema, sourceRunId: idSchema, sourceDigest: digestSchema, manifestDigest: digestSchema, candidateBindingsDigest: digestSchema, childCloseouts: z.array(z.object({ childId: idSchema, operationId: idSchema, topologyRank: positiveIntegerSchema, closeoutRef: frontendCommittedFactRefSchema }).strict()), frontendTestHandoffRefs: z.array(idSchema), remainingRisks: z.array(z.string().min(1)), integrationBindingDigest: digestSchema.optional(), integrationCloseoutRef: frontendCommittedFactRefSchema.optional(), operationTerminal: z.literal("success"), digest: digestSchema }).strict();
|
|
784
|
+
export const frontendSplitAggregateCloseoutSchema = aggregateStructuralSchema.superRefine((value, ctx) => {
|
|
785
|
+
const { digest, ...payload } = value;
|
|
786
|
+
if (computeFrontendSplitDigest(payload) !== digest)
|
|
787
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "aggregate digest mismatch" });
|
|
788
|
+
if ((value.integrationBindingDigest === undefined) !== (value.integrationCloseoutRef === undefined))
|
|
789
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "integration binding and closeout must be present together" });
|
|
790
|
+
if (new Set(value.childCloseouts.map((item) => item.childId)).size !== value.childCloseouts.length || new Set(value.childCloseouts.map((item) => item.operationId)).size !== value.childCloseouts.length)
|
|
791
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["childCloseouts"], message: "duplicate aggregate child binding" });
|
|
792
|
+
if (value.childCloseouts.some((item, index) => item.topologyRank !== index + 1))
|
|
793
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["childCloseouts"], message: "aggregate child closeout ranks must be contiguous from one" });
|
|
794
|
+
if (new Set(value.frontendTestHandoffRefs).size !== value.frontendTestHandoffRefs.length)
|
|
795
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["frontendTestHandoffRefs"], message: "duplicate aggregate frontend-test handoff ref" });
|
|
796
|
+
});
|
|
797
|
+
export function buildFrontendSplitAggregateCloseout(parentValue, manifestValue, candidateValue, integrationValue, childCloseoutFacts = []) {
|
|
798
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
799
|
+
const manifest = parseFrontendSplitManifest(manifestValue);
|
|
800
|
+
const candidate = frontendSplitCandidateBindingSchema.parse(candidateValue);
|
|
801
|
+
if (candidate.generation !== parent.generation || candidate.sourceDigest !== parent.sourceDigest || candidate.manifestDigest !== manifest.digest || parent.manifestDigest !== manifest.digest || parent.operationState !== "orchestrating-split" || parent.operationTerminal !== undefined || parent.pendingBlockRef !== undefined)
|
|
802
|
+
throw new Error("aggregate freshness mismatch");
|
|
803
|
+
if (!candidateMatchesParent(candidate, parent, manifest))
|
|
804
|
+
throw new Error("aggregate child binding mismatch");
|
|
805
|
+
let integration;
|
|
806
|
+
if (manifest.integration.required) {
|
|
807
|
+
integration = frontendSplitIntegrationBindingSchema.parse(integrationValue);
|
|
808
|
+
if (parent.phase !== "integration-complete" || integration.generation !== parent.generation || integration.sourceDigest !== parent.sourceDigest || integration.manifestDigest !== manifest.digest || integration.candidateBindingsDigest !== candidate.digest || integration.operationId !== manifest.integration.operationId || integration.state !== "success" || integration.parentDigest !== parent.digest)
|
|
809
|
+
throw new Error("aggregate integration freshness mismatch");
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
if (parent.phase !== "join-complete" || candidate.parentDigest !== parent.digest)
|
|
813
|
+
throw new Error("aggregate join freshness mismatch");
|
|
814
|
+
if (integrationValue !== undefined)
|
|
815
|
+
throw new Error("unexpected integration binding");
|
|
816
|
+
}
|
|
817
|
+
const childCloseouts = candidate.children.map((item) => ({ childId: item.childId, operationId: item.operationId, topologyRank: item.topologyRank, closeoutRef: item.closeoutRef }));
|
|
818
|
+
const facts = childCloseoutFacts.map((item) => frontendSplitChildCloseoutFactsSchema.parse(item));
|
|
819
|
+
if (facts.length > 0 && (facts.length !== candidate.children.length || facts.some((item, index) => item.childId !== candidate.children[index]?.childId)))
|
|
820
|
+
throw new Error("aggregate child closeout facts childId mismatch");
|
|
821
|
+
const frontendTestHandoffRefs = facts.flatMap((item) => (item.frontendTestHandoffRef === undefined ? [] : [item.frontendTestHandoffRef]));
|
|
822
|
+
const remainingRisks = facts.flatMap((item) => item.residualRisks);
|
|
823
|
+
const payload = { schemaVersion: 1, generation: parent.generation, parentOperationId: parent.operationId, sourceRunId: parent.sourceRunId, sourceDigest: parent.sourceDigest, manifestDigest: manifest.digest, candidateBindingsDigest: candidate.digest, childCloseouts, frontendTestHandoffRefs, remainingRisks, integrationBindingDigest: integration?.digest, integrationCloseoutRef: integration?.closeoutRef, operationTerminal: "success" };
|
|
824
|
+
return aggregateStructuralSchema.parse({ ...payload, digest: computeFrontendSplitDigest(payload) });
|
|
825
|
+
}
|
|
826
|
+
export function parseFrontendSplitAggregateCloseout(value) { return frontendSplitAggregateCloseoutSchema.parse(value); }
|
|
827
|
+
function rebuildFrontendSplitParent(parent, patch) {
|
|
828
|
+
const nextParent = buildParent({ ...parentWithoutDigest(parent), ...patch, operationRevision: parent.operationRevision + 1 });
|
|
829
|
+
return {
|
|
830
|
+
parent: nextParent,
|
|
831
|
+
execution: buildFrontendOperationExecution({ kind: "split-parent", operationId: nextParent.operationId, operationRevision: nextParent.operationRevision, sourceRunId: nextParent.sourceRunId, sourceDigest: nextParent.sourceDigest, generation: nextParent.generation, parentRecordDigest: nextParent.digest }),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
export function advanceFrontendSplitParent(parentValue, patch) {
|
|
835
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
836
|
+
if (patch.phase === "closed" || patch.operationState === "success" || patch.operationTerminal === "success")
|
|
837
|
+
throw new Error("aggregate finalization is the only success transition");
|
|
838
|
+
return rebuildFrontendSplitParent(parent, patch);
|
|
839
|
+
}
|
|
840
|
+
export function finalizeFrontendSplitAggregate(parentValue, aggregateValue, committedRefValue) {
|
|
841
|
+
const parent = parseFrontendSplitParentRecord(parentValue);
|
|
842
|
+
const aggregate = frontendSplitAggregateCloseoutSchema.parse(aggregateValue);
|
|
843
|
+
const committedRef = frontendCommittedFactRefSchema.parse(committedRefValue);
|
|
844
|
+
if (committedRef.digest !== aggregate.digest)
|
|
845
|
+
throw new Error("committed aggregate ref digest mismatch");
|
|
846
|
+
if (aggregate.parentOperationId !== parent.operationId || aggregate.generation !== parent.generation || aggregate.sourceRunId !== parent.sourceRunId || aggregate.sourceDigest !== parent.sourceDigest || aggregate.manifestDigest !== parent.manifestDigest || aggregate.operationTerminal !== "success")
|
|
847
|
+
throw new Error("aggregate finalization freshness mismatch");
|
|
848
|
+
if (parent.operationTerminal !== undefined || (parent.phase !== "join-complete" && parent.phase !== "integration-complete"))
|
|
849
|
+
throw new Error("parent is not ready for aggregate finalization");
|
|
850
|
+
return rebuildFrontendSplitParent(parent, { phase: "closed", operationState: "success", operationTerminal: "success", pendingBlockRef: undefined });
|
|
851
|
+
}
|
|
852
|
+
export function assertFrontendSplitBindingConsistency(input) {
|
|
853
|
+
const { parent, manifest, candidateBinding: candidate, integrationBinding: integration, aggregateCloseout } = input;
|
|
854
|
+
const early = parent.phase === "materializing-children" || parent.phase === "awaiting-children";
|
|
855
|
+
if (early && (candidate || integration || aggregateCloseout))
|
|
856
|
+
throw new Error("early split projection may not carry join bindings");
|
|
857
|
+
if (parent.phase !== "closed" && aggregateCloseout)
|
|
858
|
+
throw new Error("only closed success may carry aggregate closeout");
|
|
859
|
+
if (parent.phase === "closed") {
|
|
860
|
+
if (parent.operationState !== "success" || parent.operationTerminal !== "success" || !candidate || !aggregateCloseout)
|
|
861
|
+
throw new Error("closed split projection requires committed aggregate success");
|
|
862
|
+
}
|
|
863
|
+
else if (parent.operationState === "success" || parent.operationTerminal === "success")
|
|
864
|
+
throw new Error("split success requires closed aggregate projection");
|
|
865
|
+
if (!candidate) {
|
|
866
|
+
if (integration)
|
|
867
|
+
throw new Error("integration binding requires candidate binding");
|
|
868
|
+
if (!early && parent.phase !== "closed")
|
|
869
|
+
throw new Error("join phase requires candidate binding");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (candidate.generation !== parent.generation || candidate.sourceDigest !== parent.sourceDigest || candidate.manifestDigest !== manifest.digest || !candidateMatchesParent(candidate, parent, manifest))
|
|
873
|
+
throw new Error("candidate projection freshness mismatch");
|
|
874
|
+
if ((parent.phase === "awaiting-integration" || parent.phase === "join-complete") && candidate.parentDigest !== parent.digest)
|
|
875
|
+
throw new Error("candidate parent digest mismatch");
|
|
876
|
+
if (manifest.integration.required) {
|
|
877
|
+
if ((parent.phase === "integration-complete" || parent.phase === "closed") && !integration)
|
|
878
|
+
throw new Error("integration projection is missing");
|
|
879
|
+
if (parent.phase === "awaiting-integration" && integration)
|
|
880
|
+
throw new Error("integration completed before callback");
|
|
881
|
+
}
|
|
882
|
+
else if (integration)
|
|
883
|
+
throw new Error("unexpected integration projection");
|
|
884
|
+
if (integration && (integration.generation !== parent.generation || integration.sourceDigest !== parent.sourceDigest || integration.manifestDigest !== manifest.digest || integration.candidateBindingsDigest !== candidate.digest || integration.operationId !== manifest.integration.operationId || (parent.phase === "integration-complete" && integration.parentDigest !== parent.digest)))
|
|
885
|
+
throw new Error("integration projection freshness mismatch");
|
|
886
|
+
if (!aggregateCloseout)
|
|
887
|
+
return;
|
|
888
|
+
const aggregate = aggregateCloseout.value;
|
|
889
|
+
if (aggregateCloseout.committedRef.digest !== aggregate.digest || aggregate.parentOperationId !== parent.operationId || aggregate.generation !== parent.generation || aggregate.sourceRunId !== parent.sourceRunId || aggregate.sourceDigest !== parent.sourceDigest || aggregate.manifestDigest !== manifest.digest || aggregate.candidateBindingsDigest !== candidate.digest)
|
|
890
|
+
throw new Error("aggregate projection freshness mismatch");
|
|
891
|
+
if (aggregate.childCloseouts.length !== candidate.children.length || aggregate.childCloseouts.some((item, index) => item.childId !== candidate.children[index]?.childId || item.operationId !== candidate.children[index]?.operationId || item.topologyRank !== candidate.children[index]?.topologyRank || item.closeoutRef.ref !== candidate.children[index]?.closeoutRef.ref || item.closeoutRef.digest !== candidate.children[index]?.closeoutRef.digest))
|
|
892
|
+
throw new Error("aggregate child projection mismatch");
|
|
893
|
+
if (manifest.integration.required) {
|
|
894
|
+
if (!integration || aggregate.integrationBindingDigest !== integration.digest || aggregate.integrationCloseoutRef?.ref !== integration.closeoutRef.ref || aggregate.integrationCloseoutRef?.digest !== integration.closeoutRef.digest)
|
|
895
|
+
throw new Error("aggregate integration projection mismatch");
|
|
896
|
+
}
|
|
897
|
+
else if (aggregate.integrationBindingDigest !== undefined || aggregate.integrationCloseoutRef !== undefined)
|
|
898
|
+
throw new Error("unexpected aggregate integration projection");
|
|
899
|
+
}
|