@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3
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 +1645 -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 +1150 -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 +42 -16
- 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 +347 -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 +258 -393
- 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 +438 -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,347 @@
|
|
|
1
|
+
import { deterministicSha256, } from "./frontend-implementation-contract.js";
|
|
2
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
4
|
+
const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
5
|
+
function normalizeRelativePath(value) {
|
|
6
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7
|
+
}
|
|
8
|
+
function sortFindings(findings) {
|
|
9
|
+
return [...findings].sort((a, b) => {
|
|
10
|
+
if (a.code !== b.code)
|
|
11
|
+
return a.code < b.code ? -1 : 1;
|
|
12
|
+
if (a.message !== b.message)
|
|
13
|
+
return a.message < b.message ? -1 : 1;
|
|
14
|
+
const pathA = a.path ?? "";
|
|
15
|
+
const pathB = b.path ?? "";
|
|
16
|
+
if (pathA !== pathB)
|
|
17
|
+
return pathA < pathB ? -1 : 1;
|
|
18
|
+
return 0;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** 1. UI-visible requirements must map to a design decision. Every applicable
|
|
22
|
+
* UI state and every interaction must be covered by an exact `purpose` match in
|
|
23
|
+
* `uiComponentChoices`; a present `stylingStrategy` additionally covers the
|
|
24
|
+
* visual UI-state dimension (it cannot cover behavioural interactions). */
|
|
25
|
+
export function checkUiDesignCoverage(contract) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
|
|
28
|
+
const hasStyling = Boolean(contract.stylingStrategy?.trim());
|
|
29
|
+
for (const state of contract.uiStates) {
|
|
30
|
+
if (!state.applicable)
|
|
31
|
+
continue;
|
|
32
|
+
const covered = purposes.has(state.name) || hasStyling;
|
|
33
|
+
if (!covered) {
|
|
34
|
+
findings.push({
|
|
35
|
+
code: "ui-design-coverage-missing",
|
|
36
|
+
message: `applicable UI state "${state.name}" has no matching uiComponentChoices.purpose or stylingStrategy`,
|
|
37
|
+
path: state.name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const interaction of contract.interactions) {
|
|
42
|
+
if (!purposes.has(interaction.name)) {
|
|
43
|
+
findings.push({
|
|
44
|
+
code: "ui-design-coverage-missing",
|
|
45
|
+
message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose`,
|
|
46
|
+
path: interaction.name,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return findings;
|
|
51
|
+
}
|
|
52
|
+
/** 2. Source freshness: every injected entry must be fresh with a valid hash;
|
|
53
|
+
* every design-evidence path must appear in the freshness set with a single,
|
|
54
|
+
* self-consistent sha256; any declared conflict is blocking. */
|
|
55
|
+
export function checkSourceFreshness(contract, sourceFreshness) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const hashesByPath = new Map();
|
|
58
|
+
for (const entry of sourceFreshness) {
|
|
59
|
+
if (!entry.fresh) {
|
|
60
|
+
findings.push({
|
|
61
|
+
code: "source-stale",
|
|
62
|
+
message: `source is stale: ${entry.path}`,
|
|
63
|
+
path: entry.path,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!SHA256_HEX.test(entry.sha256)) {
|
|
67
|
+
findings.push({
|
|
68
|
+
code: "source-hash-invalid",
|
|
69
|
+
message: `source sha256 is not a valid 64-hex digest: ${entry.path}`,
|
|
70
|
+
path: entry.path,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const hashes = hashesByPath.get(entry.path) ?? [];
|
|
74
|
+
hashes.push(entry.sha256);
|
|
75
|
+
hashesByPath.set(entry.path, hashes);
|
|
76
|
+
}
|
|
77
|
+
for (const [path, hashes] of hashesByPath) {
|
|
78
|
+
if (new Set(hashes).size > 1) {
|
|
79
|
+
findings.push({
|
|
80
|
+
code: "source-hash-conflict",
|
|
81
|
+
message: `source freshness entries disagree on sha256: ${path}`,
|
|
82
|
+
path,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const evidencePath of contract.designEvidence.paths) {
|
|
87
|
+
if (!hashesByPath.has(evidencePath)) {
|
|
88
|
+
findings.push({
|
|
89
|
+
code: "design-evidence-path-missing-freshness",
|
|
90
|
+
message: `design evidence path is absent from the freshness set: ${evidencePath}`,
|
|
91
|
+
path: evidencePath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// `designEvidence.conflicts` is the plan's honest disclosure of design
|
|
96
|
+
// deviations (e.g. task text demands a test form the frozen constraints
|
|
97
|
+
// make infeasible). Whether a disclosed conflict is acceptable is decided
|
|
98
|
+
// by the design reviewer via `approve_design` / `request_design_changes`,
|
|
99
|
+
// which `checkDesignApproval` enforces separately. Treating any conflict
|
|
100
|
+
// as blocking here would strand every honest plan that reports one — the
|
|
101
|
+
// model cannot "fix" a conflict it correctly identified. Conflicts must be
|
|
102
|
+
// disclosed (schema-required array), never silently dropped, but disclosure
|
|
103
|
+
// itself is not a policy violation.
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
/** 3. Mock/API strategy must be allowed; non-`not-needed` strategies require
|
|
107
|
+
* at least one endpoint and every endpoint must carry fixture + consumer. */
|
|
108
|
+
export function checkMockStrategy(contract, allowedMockStrategies) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
if (!allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
111
|
+
findings.push({
|
|
112
|
+
code: "mock-strategy-not-allowed",
|
|
113
|
+
message: `mockApi.strategy "${contract.mockApi.strategy}" is not in allowedMockStrategies`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (contract.mockApi.strategy !== "not-needed") {
|
|
117
|
+
if (contract.mockApi.endpoints.length === 0) {
|
|
118
|
+
findings.push({
|
|
119
|
+
code: "mock-endpoints-empty",
|
|
120
|
+
message: `mockApi.strategy "${contract.mockApi.strategy}" requires at least one endpoint`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
contract.mockApi.endpoints.forEach((endpoint, index) => {
|
|
124
|
+
if (!endpoint.fixture) {
|
|
125
|
+
findings.push({
|
|
126
|
+
code: "mock-endpoint-missing-fixture",
|
|
127
|
+
message: `mock endpoint ${index} is missing a fixture path`,
|
|
128
|
+
path: endpoint.path,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (!endpoint.consumer) {
|
|
132
|
+
findings.push({
|
|
133
|
+
code: "mock-endpoint-missing-consumer",
|
|
134
|
+
message: `mock endpoint ${index} is missing a consumer path`,
|
|
135
|
+
path: endpoint.path,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return findings;
|
|
141
|
+
}
|
|
142
|
+
/** 4. Component choice must be precisely spec-referenced: `specified` requires
|
|
143
|
+
* a specReference whose normalized path is a candidate and a non-empty section;
|
|
144
|
+
* `new` must not carry a specReference; `reuse-existing` may omit it. */
|
|
145
|
+
export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
146
|
+
const findings = [];
|
|
147
|
+
const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
|
|
148
|
+
(contract.uiComponentChoices ?? []).forEach((choice, index) => {
|
|
149
|
+
if (choice.decision === "specified") {
|
|
150
|
+
if (!choice.specReference) {
|
|
151
|
+
findings.push({
|
|
152
|
+
code: "component-spec-reference-missing",
|
|
153
|
+
message: `specified component choice #${index} is missing specReference`,
|
|
154
|
+
path: choice.purpose,
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!choice.specReference.section.trim()) {
|
|
159
|
+
findings.push({
|
|
160
|
+
code: "component-spec-reference-missing",
|
|
161
|
+
message: `specified component choice #${index} has an empty section`,
|
|
162
|
+
path: choice.purpose,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const normalized = normalizeRelativePath(choice.specReference.path);
|
|
166
|
+
if (!candidates.has(normalized)) {
|
|
167
|
+
findings.push({
|
|
168
|
+
code: "component-spec-reference-path-outside-candidates",
|
|
169
|
+
message: `specified component choice #${index} specReference.path "${choice.specReference.path}" is not a candidate spec path`,
|
|
170
|
+
path: choice.specReference.path,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (choice.decision === "new" && choice.specReference) {
|
|
175
|
+
findings.push({
|
|
176
|
+
code: "component-new-with-spec-reference",
|
|
177
|
+
message: `new component choice #${index} must not carry a specReference`,
|
|
178
|
+
path: choice.purpose,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return findings;
|
|
183
|
+
}
|
|
184
|
+
/** 5. Every implementation target and every non-static verification target
|
|
185
|
+
* must fall inside the declared writeSet patterns. */
|
|
186
|
+
export function checkTargetPaths(contract, writeSetPatterns) {
|
|
187
|
+
const findings = [];
|
|
188
|
+
const matches = (file) => writeSetPatterns.some((pattern) => pathMatchesPattern(file, pattern));
|
|
189
|
+
for (const file of contract.targets.files) {
|
|
190
|
+
if (!matches(file)) {
|
|
191
|
+
findings.push({
|
|
192
|
+
code: "target-path-outside-write-set",
|
|
193
|
+
message: `implementation target is outside the writeSet patterns: ${file}`,
|
|
194
|
+
path: file,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const target of contract.verificationTargets) {
|
|
199
|
+
if (target.type === "static")
|
|
200
|
+
continue;
|
|
201
|
+
if (!matches(target.file)) {
|
|
202
|
+
findings.push({
|
|
203
|
+
code: "target-path-outside-write-set",
|
|
204
|
+
message: `verification target "${target.id}" file is outside the writeSet patterns: ${target.file}`,
|
|
205
|
+
path: target.file,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return findings;
|
|
210
|
+
}
|
|
211
|
+
/** 6. Any declared dependency identifier must be authorized. A declarative
|
|
212
|
+
* "no new dependencies" policy declares nothing; package-name-like tokens are
|
|
213
|
+
* matched exactly against `allowedDependencies`.
|
|
214
|
+
*
|
|
215
|
+
* The dependency policy is a natural-language statement, not a machine list.
|
|
216
|
+
* Three rules keep this check deterministic and aligned with the plan facts:
|
|
217
|
+
*
|
|
218
|
+
* 1. An empty allowedDependencies set skips the check entirely — with no
|
|
219
|
+
* allowlist there is no ground truth to reject against. The DAG generator
|
|
220
|
+
* normally fills this set from the project package.json, so an empty set
|
|
221
|
+
* means the manifest could not be read (no authorization is configured).
|
|
222
|
+
* 2. A declarative "no new dependencies" intent (零新增/不新增/无新增/不引入/
|
|
223
|
+
* no new…) satisfies the check: the model explicitly claims it adds
|
|
224
|
+
* nothing, so prose mentioning the existing toolchain ("DOM",
|
|
225
|
+
* "devDependencies", already-declared deps) must not be misread as new
|
|
226
|
+
* dependency claims.
|
|
227
|
+
* 3. Otherwise, every package-name-like token must match the allowlist —
|
|
228
|
+
* an unauthorized new dependency (a library absent from the project
|
|
229
|
+
* manifest) still fails closed.
|
|
230
|
+
*/
|
|
231
|
+
export function checkDependencies(contract, allowedDependencies) {
|
|
232
|
+
const findings = [];
|
|
233
|
+
const policy = contract.dependencyPolicy?.trim();
|
|
234
|
+
if (!policy)
|
|
235
|
+
return findings;
|
|
236
|
+
// No explicit allowlist: nothing can be judged unauthorized.
|
|
237
|
+
if (allowedDependencies.length === 0)
|
|
238
|
+
return findings;
|
|
239
|
+
// Declarative no-new-dependencies intent: trust the claim, do not parse
|
|
240
|
+
// prose tokens (they mention the already-declared toolchain). The allowlist
|
|
241
|
+
// below already passes any already-declared dependency by exact match, so
|
|
242
|
+
// this exemption is deliberately narrow: only explicit negatives escape the
|
|
243
|
+
// token scan, never neutral descriptions that could conceal a new dep.
|
|
244
|
+
if (/零新增|不新增|无新增|不引入|不添加|不依赖|no\s*new\s*deps?|no\s*new\s*dependencies/i.test(policy)) {
|
|
245
|
+
return findings;
|
|
246
|
+
}
|
|
247
|
+
const allowed = new Set(allowedDependencies);
|
|
248
|
+
const tokens = policy
|
|
249
|
+
.split(/[,;\s]+/)
|
|
250
|
+
.filter((token) => token.length > 0);
|
|
251
|
+
for (const token of tokens) {
|
|
252
|
+
if (!NPM_PACKAGE_NAME.test(token))
|
|
253
|
+
continue;
|
|
254
|
+
// Already-declared dependencies are authorized by definition.
|
|
255
|
+
if (allowed.has(token))
|
|
256
|
+
continue;
|
|
257
|
+
// Versioned/scoped variants: pkg@1.2.3 or @scope/pkg@1 → match bare name.
|
|
258
|
+
const bareName = token.startsWith("@")
|
|
259
|
+
? token.split("@").slice(0, 2).join("@")
|
|
260
|
+
: token.split("@")[0];
|
|
261
|
+
if (allowed.has(bareName))
|
|
262
|
+
continue;
|
|
263
|
+
findings.push({
|
|
264
|
+
code: "dependency-not-allowed",
|
|
265
|
+
message: `declared dependency "${token}" is not in allowedDependencies`,
|
|
266
|
+
path: token,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return findings;
|
|
270
|
+
}
|
|
271
|
+
/** 7. Every declared UI state must carry a complete attribution:
|
|
272
|
+
* applicable → expectedBehavior, non-applicable → notApplicableReason.
|
|
273
|
+
*
|
|
274
|
+
* The check validates the states the contract DECLARES, not a hardcoded list
|
|
275
|
+
* of canonical names: the plan prompts say "Do not invent UI states", so a
|
|
276
|
+
* contract that reasonably covers loading/empty/error/permission without a
|
|
277
|
+
* `disabled` entry must not be blocked for missing a state the model had no
|
|
278
|
+
* reason to declare — the model cannot fix that without inventing one, which
|
|
279
|
+
* the ladder then retries to exhaustion.
|
|
280
|
+
*
|
|
281
|
+
* Pure-logic contracts (no user-visible UI states AND no interactions) are
|
|
282
|
+
* exempt entirely: the schema and prompts explicitly allow `uiStates: []`.
|
|
283
|
+
* Once any state or interaction is declared, every declared state must carry
|
|
284
|
+
* complete attribution.
|
|
285
|
+
*/
|
|
286
|
+
export function checkUiStateAttribution(contract) {
|
|
287
|
+
if (contract.uiStates.length === 0 && contract.interactions.length === 0)
|
|
288
|
+
return [];
|
|
289
|
+
const findings = [];
|
|
290
|
+
for (const state of contract.uiStates) {
|
|
291
|
+
if (state.applicable && !state.expectedBehavior) {
|
|
292
|
+
findings.push({
|
|
293
|
+
code: "ui-state-attribution-incomplete",
|
|
294
|
+
message: `applicable UI state "${state.name}" is missing expectedBehavior`,
|
|
295
|
+
path: state.name,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (!state.applicable && !state.notApplicableReason) {
|
|
299
|
+
findings.push({
|
|
300
|
+
code: "ui-state-attribution-incomplete",
|
|
301
|
+
message: `non-applicable UI state "${state.name}" is missing notApplicableReason`,
|
|
302
|
+
path: state.name,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return findings;
|
|
307
|
+
}
|
|
308
|
+
/** Design approval: a `request-revision` verdict from the design reviewer is
|
|
309
|
+
* blocking. This is injected as structured input, never parsed from model text. */
|
|
310
|
+
export function checkDesignApproval(designApproval) {
|
|
311
|
+
if (designApproval.verdict === "request-revision") {
|
|
312
|
+
return [
|
|
313
|
+
{
|
|
314
|
+
code: "design-approval-request-revision",
|
|
315
|
+
message: "design approval verdict is request-revision",
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
/** Aggregate the deterministic design-policy checks into a single
|
|
322
|
+
* deterministic result. Any finding blocks; an empty finding list approves. */
|
|
323
|
+
export function evaluateFrontendDesignPolicy(input) {
|
|
324
|
+
const findings = sortFindings([
|
|
325
|
+
...(input.designApproval ? checkDesignApproval(input.designApproval) : []),
|
|
326
|
+
...checkUiDesignCoverage(input.contract),
|
|
327
|
+
...checkSourceFreshness(input.contract, input.sourceFreshness),
|
|
328
|
+
...checkMockStrategy(input.contract, input.allowedMockStrategies),
|
|
329
|
+
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths),
|
|
330
|
+
...checkTargetPaths(input.contract, input.writeSetPatterns),
|
|
331
|
+
...checkDependencies(input.contract, input.allowedDependencies),
|
|
332
|
+
...checkUiStateAttribution(input.contract),
|
|
333
|
+
]);
|
|
334
|
+
const classification = findings.length > 0 ? "blocked" : "approved";
|
|
335
|
+
const result = {
|
|
336
|
+
schemaId: "frontend-design-policy-v1",
|
|
337
|
+
classification,
|
|
338
|
+
findings,
|
|
339
|
+
digest: "",
|
|
340
|
+
};
|
|
341
|
+
result.digest = deterministicSha256({
|
|
342
|
+
schemaId: result.schemaId,
|
|
343
|
+
classification: result.classification,
|
|
344
|
+
findings: result.findings,
|
|
345
|
+
});
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const id = z.string().trim().min(1);
|
|
4
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
|
+
const revision = z.number().int().positive();
|
|
6
|
+
const instant = z.string().datetime();
|
|
7
|
+
const factRef = z.object({ ref: id, digest }).strict();
|
|
8
|
+
function canonicalValue(value) {
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return value.map(canonicalValue);
|
|
11
|
+
if (value !== null && typeof value === "object")
|
|
12
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonicalValue(item)]));
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function canonicalFrontendHumanDecisionJson(value) {
|
|
16
|
+
return JSON.stringify(canonicalValue(value));
|
|
17
|
+
}
|
|
18
|
+
export function computeFrontendHumanDecisionDigest(value) {
|
|
19
|
+
return createHash("sha256").update(canonicalFrontendHumanDecisionJson(value), "utf8").digest("hex");
|
|
20
|
+
}
|
|
21
|
+
const baseEnvelope = {
|
|
22
|
+
operationId: id,
|
|
23
|
+
runId: id,
|
|
24
|
+
operationRevision: revision,
|
|
25
|
+
targetDigest: digest,
|
|
26
|
+
staleOnTargetDigestChange: z.literal(true),
|
|
27
|
+
expiresAt: instant.optional(),
|
|
28
|
+
};
|
|
29
|
+
const contractEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("contract-decision"), resumeAction: z.literal("resume-contract"), payload: z.object({ questionFactIds: z.array(id).min(1) }).strict() }).strict();
|
|
30
|
+
const highEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("high-admission"), resumeAction: z.literal("admit-high"), payload: z.object({ admissionCandidateRef: factRef }).strict() }).strict();
|
|
31
|
+
const splitEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("split-materialization-approval"), resumeAction: z.literal("materialize-split"), payload: z.object({ splitManifestRef: factRef }).strict() }).strict();
|
|
32
|
+
const baselineEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("baseline-debt-acceptance"), resumeAction: z.literal("accept-baseline-debt"), payload: z.object({ baselineEvidenceRefs: z.array(factRef).min(1), candidateDigest: digest, commandDigest: digest, resultingPolicy: z.literal("allow-non-regression") }).strict() }).strict();
|
|
33
|
+
const verificationEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("verification-config"), resumeAction: z.literal("select-verification-config"), payload: z.object({ unresolvedEntrypointRefs: z.array(factRef).min(1), allowlistedResolvers: z.array(id).min(1) }).strict() }).strict();
|
|
34
|
+
export const frontendHumanDecisionEnvelopeInputSchema = z.discriminatedUnion("kind", [contractEnvelopeInput, highEnvelopeInput, splitEnvelopeInput, baselineEnvelopeInput, verificationEnvelopeInput]);
|
|
35
|
+
const envelopeSchemas = [contractEnvelopeInput.extend({ digest }).strict(), highEnvelopeInput.extend({ digest }).strict(), splitEnvelopeInput.extend({ digest }).strict(), baselineEnvelopeInput.extend({ digest }).strict(), verificationEnvelopeInput.extend({ digest }).strict()];
|
|
36
|
+
const envelopeStructuralSchema = z.discriminatedUnion("kind", envelopeSchemas);
|
|
37
|
+
export const frontendHumanDecisionEnvelopeSchema = envelopeStructuralSchema.superRefine((value, ctx) => {
|
|
38
|
+
const { digest: actual, ...payload } = value;
|
|
39
|
+
if (computeFrontendHumanDecisionDigest(payload) !== actual)
|
|
40
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "envelope digest mismatch" });
|
|
41
|
+
});
|
|
42
|
+
export function buildFrontendHumanDecisionEnvelope(raw) {
|
|
43
|
+
const payload = frontendHumanDecisionEnvelopeInputSchema.parse(raw);
|
|
44
|
+
return envelopeStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) });
|
|
45
|
+
}
|
|
46
|
+
export function parseFrontendHumanDecisionEnvelope(value) { return frontendHumanDecisionEnvelopeSchema.parse(value); }
|
|
47
|
+
const receiptBase = { operationId: id, runId: id, operationRevision: revision, targetDigest: digest, receiptId: id, envelopeDigest: digest, commitState: z.literal("committed"), consumeState: z.enum(["unconsumed", "consumed"]) };
|
|
48
|
+
const resolutionFact = { resolutionFactRef: factRef };
|
|
49
|
+
const contractReceiptInput = z.object({ ...receiptBase, kind: z.literal("contract-decision"), resumeAction: z.literal("resume-contract"), resolution: z.object({ questionFactIds: z.array(id).min(1), outcome: z.literal("resume-contract"), ...resolutionFact }).strict() }).strict();
|
|
50
|
+
const highReceiptInput = z.object({ ...receiptBase, kind: z.literal("high-admission"), resumeAction: z.literal("admit-high"), resolution: z.object({ admissionCandidateRef: factRef, outcome: z.literal("admitted-high"), ...resolutionFact }).strict() }).strict();
|
|
51
|
+
const splitReceiptInput = z.object({ ...receiptBase, kind: z.literal("split-materialization-approval"), resumeAction: z.literal("materialize-split"), resolution: z.object({ splitManifestRef: factRef, ...resolutionFact }).strict() }).strict();
|
|
52
|
+
const baselineReceiptInput = z.object({ ...receiptBase, kind: z.literal("baseline-debt-acceptance"), resumeAction: z.literal("accept-baseline-debt"), resolution: z.object({ baselineEvidenceRefs: z.array(factRef).min(1), candidateDigest: digest, commandDigest: digest, resultingPolicy: z.literal("allow-non-regression"), outcome: z.literal("accepted-baseline-debt"), remediated: z.literal(false), ...resolutionFact }).strict() }).strict();
|
|
53
|
+
const verificationReceiptInput = z.object({ ...receiptBase, kind: z.literal("verification-config"), resumeAction: z.literal("select-verification-config"), resolution: z.object({ entrypointRef: factRef, resolver: id, sourceDigest: digest, commandDigest: digest, ...resolutionFact }).strict() }).strict();
|
|
54
|
+
export const frontendHumanDecisionReceiptInputSchema = z.discriminatedUnion("kind", [contractReceiptInput, highReceiptInput, splitReceiptInput, baselineReceiptInput, verificationReceiptInput]);
|
|
55
|
+
const receiptStructuralSchema = z.discriminatedUnion("kind", [contractReceiptInput.extend({ digest }).strict(), highReceiptInput.extend({ digest }).strict(), splitReceiptInput.extend({ digest }).strict(), baselineReceiptInput.extend({ digest }).strict(), verificationReceiptInput.extend({ digest }).strict()]);
|
|
56
|
+
export const frontendHumanDecisionReceiptSchema = receiptStructuralSchema.superRefine((value, ctx) => {
|
|
57
|
+
const { digest: actual, ...payload } = value;
|
|
58
|
+
if (computeFrontendHumanDecisionDigest(payload) !== actual)
|
|
59
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "receipt digest mismatch" });
|
|
60
|
+
});
|
|
61
|
+
export function buildFrontendHumanDecisionReceipt(raw) {
|
|
62
|
+
const payload = frontendHumanDecisionReceiptInputSchema.parse(raw);
|
|
63
|
+
return receiptStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) });
|
|
64
|
+
}
|
|
65
|
+
export function parseFrontendHumanDecisionReceipt(value) { return frontendHumanDecisionReceiptSchema.parse(value); }
|
|
66
|
+
const activeInput = z.object({ status: z.literal("active"), operationId: id, runId: id, revision }).strict();
|
|
67
|
+
const blockedInput = z.object({ status: z.literal("blocked-human"), operationId: id, runId: id, revision, envelope: frontendHumanDecisionEnvelopeSchema }).strict();
|
|
68
|
+
const resolvedInput = z.object({ status: z.literal("resolved"), operationId: id, runId: id, revision, receipt: frontendHumanDecisionReceiptSchema }).strict();
|
|
69
|
+
export const frontendHumanDecisionProjectionInputSchema = z.discriminatedUnion("status", [activeInput, blockedInput, resolvedInput]);
|
|
70
|
+
const projectionStructuralSchema = z.discriminatedUnion("status", [activeInput.extend({ digest }).strict(), blockedInput.extend({ digest }).strict(), resolvedInput.extend({ digest }).strict()]);
|
|
71
|
+
export const frontendHumanDecisionProjectionSchema = projectionStructuralSchema.superRefine((value, ctx) => { const { digest: actual, ...payload } = value; if (computeFrontendHumanDecisionDigest(payload) !== actual)
|
|
72
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "projection digest mismatch" }); });
|
|
73
|
+
export function buildFrontendHumanDecisionProjection(raw) { const payload = frontendHumanDecisionProjectionInputSchema.parse(raw); return projectionStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) }); }
|
|
74
|
+
export function parseFrontendHumanDecisionProjection(value) { return frontendHumanDecisionProjectionSchema.parse(value); }
|
|
75
|
+
const reject = (projection, reason) => ({ outcome: "rejected", reason, projection });
|
|
76
|
+
export function materializeFrontendHumanDecision(input) {
|
|
77
|
+
const projection = parseFrontendHumanDecisionProjection(input.projection);
|
|
78
|
+
const envelope = parseFrontendHumanDecisionEnvelope(input.envelope);
|
|
79
|
+
if (projection.status !== "active")
|
|
80
|
+
return reject(projection, "projection is not active");
|
|
81
|
+
if (projection.revision !== input.expectedRecordRevision || projection.digest !== input.expectedProjectionDigest)
|
|
82
|
+
return reject(projection, "revision or projection digest conflict");
|
|
83
|
+
if (envelope.operationId !== projection.operationId || envelope.runId !== projection.runId || envelope.operationRevision !== projection.revision)
|
|
84
|
+
return reject(projection, "envelope binding mismatch");
|
|
85
|
+
return { outcome: "applied", projection: buildFrontendHumanDecisionProjection({ status: "blocked-human", operationId: projection.operationId, runId: projection.runId, revision: projection.revision + 1, envelope }) };
|
|
86
|
+
}
|
|
87
|
+
function receiptMatchesEnvelope(receipt, envelope) {
|
|
88
|
+
if (receipt.kind !== envelope.kind || receipt.resumeAction !== envelope.resumeAction || receipt.operationId !== envelope.operationId || receipt.runId !== envelope.runId || receipt.operationRevision !== envelope.operationRevision || receipt.targetDigest !== envelope.targetDigest || receipt.envelopeDigest !== envelope.digest)
|
|
89
|
+
return false;
|
|
90
|
+
switch (envelope.kind) {
|
|
91
|
+
case "contract-decision": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.questionFactIds) === canonicalFrontendHumanDecisionJson(envelope.payload.questionFactIds);
|
|
92
|
+
case "high-admission": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.admissionCandidateRef) === canonicalFrontendHumanDecisionJson(envelope.payload.admissionCandidateRef);
|
|
93
|
+
case "split-materialization-approval": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.splitManifestRef) === canonicalFrontendHumanDecisionJson(envelope.payload.splitManifestRef);
|
|
94
|
+
case "baseline-debt-acceptance": return receipt.kind === envelope.kind && receipt.resolution.candidateDigest === envelope.payload.candidateDigest && receipt.resolution.commandDigest === envelope.payload.commandDigest && canonicalFrontendHumanDecisionJson(receipt.resolution.baselineEvidenceRefs) === canonicalFrontendHumanDecisionJson(envelope.payload.baselineEvidenceRefs);
|
|
95
|
+
case "verification-config": return receipt.kind === envelope.kind && envelope.payload.allowlistedResolvers.includes(receipt.resolution.resolver) && envelope.payload.unresolvedEntrypointRefs.some((item) => canonicalFrontendHumanDecisionJson(item) === canonicalFrontendHumanDecisionJson(receipt.resolution.entrypointRef));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function consumeFrontendHumanDecision(input) {
|
|
99
|
+
const projection = parseFrontendHumanDecisionProjection(input.projection);
|
|
100
|
+
if (projection.status !== "blocked-human")
|
|
101
|
+
return reject(projection, "projection has no pending envelope");
|
|
102
|
+
const consumeInput = z.object({ expectedRecordRevision: revision, expectedProjectionDigest: digest, currentTargetDigest: digest, now: instant }).strict().safeParse({
|
|
103
|
+
expectedRecordRevision: input.expectedRecordRevision,
|
|
104
|
+
expectedProjectionDigest: input.expectedProjectionDigest,
|
|
105
|
+
currentTargetDigest: input.currentTargetDigest,
|
|
106
|
+
now: input.now,
|
|
107
|
+
});
|
|
108
|
+
if (!consumeInput.success)
|
|
109
|
+
return reject(projection, "invalid consume input");
|
|
110
|
+
const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
|
|
111
|
+
if (projection.revision !== input.expectedRecordRevision || projection.digest !== input.expectedProjectionDigest)
|
|
112
|
+
return reject(projection, "revision or projection digest conflict");
|
|
113
|
+
if (input.currentTargetDigest !== projection.envelope.targetDigest) {
|
|
114
|
+
const payload = { kind: "frontend-human-decision-stale", operationId: projection.operationId, envelopeDigest: projection.envelope.digest, previousTargetDigest: projection.envelope.targetDigest, currentTargetDigest: input.currentTargetDigest };
|
|
115
|
+
return { outcome: "stale", reason: "target digest changed", projection, auditFact: { ...payload, digest: computeFrontendHumanDecisionDigest(payload) } };
|
|
116
|
+
}
|
|
117
|
+
if (projection.envelope.expiresAt && Date.parse(input.now) >= Date.parse(projection.envelope.expiresAt))
|
|
118
|
+
return reject(projection, "envelope expired");
|
|
119
|
+
if (receipt.commitState !== "committed" || receipt.consumeState !== "unconsumed" || !receiptMatchesEnvelope(receipt, projection.envelope))
|
|
120
|
+
return reject(projection, "receipt freshness or payload mismatch");
|
|
121
|
+
const { digest: _digest, ...receiptPayload } = receipt;
|
|
122
|
+
const consumed = buildFrontendHumanDecisionReceipt({ ...receiptPayload, consumeState: "consumed" });
|
|
123
|
+
return { outcome: "applied", projection: buildFrontendHumanDecisionProjection({ status: "resolved", operationId: projection.operationId, runId: projection.runId, revision: projection.revision + 1, receipt: consumed }) };
|
|
124
|
+
}
|
|
125
|
+
/* ------------------------------------------------------------------ *
|
|
126
|
+
* A+B producer/consumer wiring (no new envelope schema in the frozen *
|
|
127
|
+
* five-kind union). *
|
|
128
|
+
* ------------------------------------------------------------------ */
|
|
129
|
+
/**
|
|
130
|
+
* A+B (AC-002): contract `blocked` uniquely maps to `blocked-human`
|
|
131
|
+
* (contract-decision envelope) or `blocked-external` (wait-external), reusing
|
|
132
|
+
* the existing five-envelope schema without widening it. Any other disposition
|
|
133
|
+
* is `not-blocked`.
|
|
134
|
+
*/
|
|
135
|
+
export function mapContractBlockedOwner(input) {
|
|
136
|
+
if (input.disposition !== "blocked")
|
|
137
|
+
return "not-blocked";
|
|
138
|
+
return input.blockingOwner === "blocked-external"
|
|
139
|
+
? "blocked-external"
|
|
140
|
+
: "blocked-human";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A+B (AC-007): High-topology `high-admission-receipt`. The receipt is a
|
|
144
|
+
* runtime-materialized catalog (not a sixth human-decision envelope): a receipt
|
|
145
|
+
* whose candidate digest matches the admission digest resolves to `accepted`;
|
|
146
|
+
* any digest drift resolves to `stale`.
|
|
147
|
+
*/
|
|
148
|
+
export const highAdmissionReceiptSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
schemaVersion: z.literal(1),
|
|
151
|
+
schemaId: z.literal("high-admission-receipt-v1"),
|
|
152
|
+
candidateDigest: digest,
|
|
153
|
+
admissionDigest: digest,
|
|
154
|
+
status: z.enum(["accepted", "stale"]),
|
|
155
|
+
})
|
|
156
|
+
.strict();
|
|
157
|
+
export function evaluateHighAdmissionReceipt(input) {
|
|
158
|
+
return highAdmissionReceiptSchema.parse({
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
schemaId: "high-admission-receipt-v1",
|
|
161
|
+
candidateDigest: input.candidateDigest,
|
|
162
|
+
admissionDigest: input.admissionDigest,
|
|
163
|
+
status: input.candidateDigest === input.admissionDigest
|
|
164
|
+
? "accepted"
|
|
165
|
+
: "stale",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A+B (AC-008): verification-config routing. A verification-config signal
|
|
170
|
+
* either restarts the contract or produces a verification-config-decision
|
|
171
|
+
* (independent of the frozen `verification-config` envelope). Fail-closed to
|
|
172
|
+
* contract restart when the signal is untyped.
|
|
173
|
+
*/
|
|
174
|
+
export function routeVerificationConfig(input) {
|
|
175
|
+
const entrypointRefs = [...new Set(input.unresolvedEntrypointRefs)].filter((item) => item.length > 0);
|
|
176
|
+
return {
|
|
177
|
+
decision: entrypointRefs.length > 0
|
|
178
|
+
? "verification-config-decision"
|
|
179
|
+
: "contract-restart",
|
|
180
|
+
entrypointRefs,
|
|
181
|
+
};
|
|
182
|
+
}
|