@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +198 -118
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +76 -11
- package/dist/executors/shell-executor.js +88 -12
- package/dist/infrastructure/console/operation-store.js +2 -0
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/task/config-types.js +22 -9
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -8
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/operator-actions.js +0 -1
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/prd-intake-bridge.js +5 -15
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +2 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -67
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/backend-test-dag.json +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frontendPlanSemanticDigest } from "./frontend-plan-progress.js";
|
|
1
2
|
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
@@ -12,7 +13,7 @@ export function readFrontendToolProgress(tools) {
|
|
|
12
13
|
const counters = [...new Set((tools ?? []).flatMap(tool => tool && typeof tool === "object" && progressByTool.has(tool) ? [progressByTool.get(tool)] : []))];
|
|
13
14
|
if (!counters.length)
|
|
14
15
|
return undefined;
|
|
15
|
-
return counters.reduce((sum, item) => ({ failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
|
|
16
|
+
return counters.reduce((sum, item) => ({ finalizeRejections: sum.finalizeRejections + item.finalizeRejections, failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { finalizeRejections: 0, failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
|
|
16
17
|
}
|
|
17
18
|
async function exclusive(key, work) {
|
|
18
19
|
const previous = queues.get(key) ?? Promise.resolve();
|
|
@@ -61,7 +62,8 @@ export async function createDurableFrontendTools(input) {
|
|
|
61
62
|
return { ...tool, parameters: { ...schema, properties: { ...schema.properties, replace: { type: "boolean", description: "Explicitly revise the current fact with the same identity. Omit for new facts or identical replay." } } } };
|
|
62
63
|
});
|
|
63
64
|
const bindingSha256 = hash({ version: 1, attemptId: input.attemptId, binding: input.binding, schemas: tools.map(t => ({ name: t.name, parameters: t.parameters })) });
|
|
64
|
-
const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store);
|
|
65
|
+
const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); if (hasCommittedTerminalEvent(store, input.attemptId))
|
|
66
|
+
input.planControl?.acceptTerminal(); };
|
|
65
67
|
const validate = async (records) => {
|
|
66
68
|
let revision = 0;
|
|
67
69
|
let terminal = false;
|
|
@@ -89,81 +91,99 @@ export async function createDurableFrontendTools(input) {
|
|
|
89
91
|
throw ledgerError("FRONTEND_LEDGER_MISSING", "frontend durable ledger disappeared or became empty; refusing an unproven acknowledgement");
|
|
90
92
|
};
|
|
91
93
|
await exclusive(file, restore);
|
|
92
|
-
const progress = { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
|
|
94
|
+
const progress = { finalizeRejections: 0, failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
|
|
93
95
|
const replay = (receipt) => { progress.duplicateRecords++; return receipt; };
|
|
94
96
|
const wrapped = tools.map(tool => ({ ...tool, execute: (...args) => exclusive(file, async () => {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
|
|
107
|
-
const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
|
|
108
|
-
const previousCall = submissions.find(s => s.callId === callId);
|
|
109
|
-
if (previousCall)
|
|
110
|
-
return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
|
|
111
|
-
const previousFact = submissions.filter(s => s.identity === identity).at(-1);
|
|
112
|
-
if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
|
|
113
|
-
return replay(previousFact.receipt);
|
|
114
|
-
// A committed finalize terminal carries no domain payload in its params
|
|
115
|
-
// (r15: the model acknowledged with {replace:false}, the executor's
|
|
116
|
-
// auto-finalize sends {}) — both are acknowledgements of the same durable
|
|
117
|
-
// terminal, so any later finalize call replays the receipt instead of
|
|
118
|
-
// demanding a params match no caller can reproduce. Other singletons
|
|
119
|
-
// (state registry, data flows, ...) carry domain payloads and keep the
|
|
120
|
-
// replace:true correction path.
|
|
121
|
-
if (FINALIZE_TERMINAL_TOOLS.has(tool.name) && previousFact?.receipt)
|
|
122
|
-
return replay(previousFact.receipt);
|
|
123
|
-
if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
|
|
124
|
-
return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
|
|
125
|
-
if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "finalize_decision", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
|
|
126
|
-
return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
|
|
127
|
-
const draft = structuredClone(input.store);
|
|
128
|
-
input.setWorkingStore(draft);
|
|
129
|
-
let result;
|
|
97
|
+
const beforeRevision = input.store.revision;
|
|
98
|
+
const execute = async () => {
|
|
99
|
+
const stopped = input.planControl?.readStop();
|
|
100
|
+
if (stopped)
|
|
101
|
+
return failure(stopped.code, stopped.error);
|
|
102
|
+
const [callId, params] = args;
|
|
103
|
+
const valid = Check(tool.parameters, params);
|
|
104
|
+
if (!valid) {
|
|
105
|
+
const errors = [...Errors(tool.parameters, params)];
|
|
106
|
+
return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
|
|
107
|
+
}
|
|
130
108
|
try {
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
109
|
+
await restore();
|
|
110
|
+
const isReviewFinding = ["record_review_finding", "record_design_finding"].includes(tool.name);
|
|
111
|
+
const paramsSha256 = hash(isReviewFinding ? { ...params, replace: undefined } : params);
|
|
112
|
+
const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan", "finalize_decision"]);
|
|
113
|
+
const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? params?.entry?.purpose ?? params?.entry?.state ?? params?.entry?.interaction ?? params?.entry?.boundary ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
|
|
114
|
+
const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
|
|
115
|
+
const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
|
|
116
|
+
const previousCall = submissions.find(s => s.callId === callId);
|
|
117
|
+
if (previousCall)
|
|
118
|
+
return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
|
|
119
|
+
const previousFact = submissions.filter(s => s.identity === identity).at(-1);
|
|
120
|
+
if (isReviewFinding && !previousFact && params?.replace === true)
|
|
121
|
+
return failure("FACT_REPLACEMENT_NOT_FOUND", `${identity}: replacement requires an existing finding; omit replace to create it`);
|
|
122
|
+
if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
|
|
123
|
+
return replay(previousFact.receipt);
|
|
124
|
+
// A committed finalize terminal carries no domain payload in its params
|
|
125
|
+
// (r15: the model acknowledged with {replace:false}, the executor's
|
|
126
|
+
// auto-finalize sends {}) — both are acknowledgements of the same durable
|
|
127
|
+
// terminal, so any later finalize call replays the receipt instead of
|
|
128
|
+
// demanding a params match no caller can reproduce. Other singletons
|
|
129
|
+
// (state registry, data flows, ...) carry domain payloads and keep the
|
|
130
|
+
// replace:true correction path.
|
|
131
|
+
if (FINALIZE_TERMINAL_TOOLS.has(tool.name) && previousFact?.receipt)
|
|
132
|
+
return replay(previousFact.receipt);
|
|
133
|
+
if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
|
|
134
|
+
return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
|
|
135
|
+
if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "finalize_decision", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
|
|
136
|
+
return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
|
|
137
|
+
const draft = structuredClone(input.store);
|
|
138
|
+
input.setWorkingStore(draft);
|
|
139
|
+
let result;
|
|
140
|
+
try {
|
|
141
|
+
// Protocol controls must not be spread into domain facts by permissive callers.
|
|
142
|
+
const original = input.tools.find(t => t.name === tool.name);
|
|
143
|
+
const domainParams = { ...params };
|
|
144
|
+
if (!original.parameters.properties?.replace)
|
|
145
|
+
delete domainParams.replace;
|
|
146
|
+
result = await tool.execute(args[0], domainParams, ...args.slice(2));
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
input.setWorkingStore(input.store);
|
|
150
|
+
}
|
|
151
|
+
if (result.details?.ok !== true)
|
|
152
|
+
return result;
|
|
153
|
+
const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
|
|
154
|
+
if (!newRecords.length)
|
|
155
|
+
return replay(result);
|
|
156
|
+
for (const record of draft.records) {
|
|
157
|
+
record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
|
|
158
|
+
}
|
|
159
|
+
for (const record of newRecords)
|
|
160
|
+
record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
|
|
161
|
+
newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
|
|
162
|
+
const committed = draft.records.filter(r => r.phase === "committed");
|
|
163
|
+
await validate(committed);
|
|
164
|
+
await persist(file, committed);
|
|
165
|
+
publish(draft);
|
|
166
|
+
progress.durableSubmissions++;
|
|
167
|
+
if (previousFact)
|
|
168
|
+
progress.durableReplacements++;
|
|
169
|
+
return result;
|
|
137
170
|
}
|
|
138
|
-
|
|
171
|
+
catch (error) {
|
|
139
172
|
input.setWorkingStore(input.store);
|
|
173
|
+
return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
|
|
140
174
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
|
|
148
|
-
}
|
|
149
|
-
for (const record of newRecords)
|
|
150
|
-
record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
|
|
151
|
-
newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
|
|
152
|
-
const committed = draft.records.filter(r => r.phase === "committed");
|
|
153
|
-
await validate(committed);
|
|
154
|
-
await persist(file, committed);
|
|
155
|
-
publish(draft);
|
|
156
|
-
progress.durableSubmissions++;
|
|
157
|
-
if (previousFact)
|
|
158
|
-
progress.durableReplacements++;
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
input.setWorkingStore(input.store);
|
|
163
|
-
return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
|
|
175
|
+
};
|
|
176
|
+
const result = await execute();
|
|
177
|
+
if (result.details?.ok !== true) {
|
|
178
|
+
progress.failedRecords++;
|
|
179
|
+
if (FINALIZE_TERMINAL_TOOLS.has(tool.name))
|
|
180
|
+
progress.finalizeRejections++;
|
|
164
181
|
}
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
const note = input.planControl?.observe(tool.name, args[1], result.details, input.store.revision > beforeRevision, input.planControl ? frontendPlanSemanticDigest(input.store.records) : undefined);
|
|
183
|
+
if (note)
|
|
184
|
+
return { ...result, content: [...(Array.isArray(result.content) ? result.content : []), { type: "text", text: JSON.stringify(note) }], details: { ...result.details, planProgress: note } };
|
|
185
|
+
return result;
|
|
186
|
+
}) }));
|
|
167
187
|
for (const tool of wrapped)
|
|
168
188
|
progressByTool.set(tool, progress);
|
|
169
189
|
return {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
2
|
+
import { capabilityBoundarySchema } from "../../task/config-types.js";
|
|
1
3
|
import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { readFileSync } from "node:fs";
|
|
@@ -195,6 +197,20 @@ export function isRecognizedFrontendVerifyCommandText(command) {
|
|
|
195
197
|
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
196
198
|
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
|
|
197
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Repo-level shell scripts whose name marks them as governance/static checks
|
|
202
|
+
* rather than test runners: `scripts/check-*.sh`, `scripts/ci.sh`,
|
|
203
|
+
* `scripts/ci-governance.sh` and friends. The behavior lane is test-observed
|
|
204
|
+
* at writer admission, so these must not be admitted there — they cannot emit
|
|
205
|
+
* a test report.
|
|
206
|
+
*
|
|
207
|
+
* A test word in the name still wins, because the test-semantics check runs
|
|
208
|
+
* earlier in `classifyFrontendVerifyCommandText`, so a real test script such as
|
|
209
|
+
* `scripts/run-tests.sh` or `scripts/check-tests.sh` is unaffected.
|
|
210
|
+
*/
|
|
211
|
+
const VERIFY_STATIC_SCRIPT_PATH_RE = /\bscripts[\\/]+(?:ci(?:-governance|-tests)?|check-[a-z0-9-]+)\.sh\b/i;
|
|
212
|
+
/** A shell script whose own name marks it as a test runner. */
|
|
213
|
+
const VERIFY_BEHAVIOR_SCRIPT_PATH_RE = /\bscripts[\\/]+[a-z0-9-]*(?:tests?|specs?|e2e)[a-z0-9-]*\.sh\b/i;
|
|
198
214
|
/**
|
|
199
215
|
* Classify a raw verification command text into the runtime-owned mode.
|
|
200
216
|
* Shared single implementation used by DAG generation (lane split) and
|
|
@@ -207,6 +223,14 @@ export function isRecognizedFrontendVerifyCommandText(command) {
|
|
|
207
223
|
* static scripts (`npm run check`, `check:types`) and leading binaries
|
|
208
224
|
* (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
|
|
209
225
|
* website run build`) are skipped when locating the script.
|
|
226
|
+
*
|
|
227
|
+
* Returns `undefined` for a command whose text carries no recognizable
|
|
228
|
+
* verification semantics. The previous fallback returned `"behavior"`, which
|
|
229
|
+
* silently pushed unknown commands into the strictest lane: the behavior lane
|
|
230
|
+
* is test-observed at writer admission, so a command that cannot emit a test
|
|
231
|
+
* report blocked admission deterministically with no way for the model to
|
|
232
|
+
* clear it. Callers must now decide explicitly instead of inheriting a lane
|
|
233
|
+
* from an unrecognized string.
|
|
210
234
|
*/
|
|
211
235
|
export function classifyFrontendVerifyCommandText(command) {
|
|
212
236
|
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
|
|
@@ -220,16 +244,24 @@ export function classifyFrontendVerifyCommandText(command) {
|
|
|
220
244
|
}
|
|
221
245
|
return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
|
|
222
246
|
? "static"
|
|
223
|
-
:
|
|
247
|
+
: undefined;
|
|
224
248
|
}
|
|
225
249
|
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
226
250
|
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
251
|
+
// A test word in the invoked script's own name wins over the static
|
|
252
|
+
// governance pattern: `scripts/check-tests.sh` runs tests even though it
|
|
253
|
+
// starts with `check-`, so it must stay in the test-observed lane.
|
|
254
|
+
if (VERIFY_BEHAVIOR_SCRIPT_PATH_RE.test(command)) {
|
|
255
|
+
return "behavior";
|
|
256
|
+
}
|
|
227
257
|
if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
228
258
|
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
229
|
-
|
|
259
|
+
VERIFY_STATIC_SCRIPT_PATH_RE.test(command)) {
|
|
230
260
|
return "static";
|
|
231
261
|
}
|
|
232
|
-
|
|
262
|
+
// Unrecognized: no lane is inferred. Callers must require an operator
|
|
263
|
+
// declaration rather than defaulting into the test-observed behavior lane.
|
|
264
|
+
return undefined;
|
|
233
265
|
}
|
|
234
266
|
export function slugFrontendVerifyCommandId(label, taken) {
|
|
235
267
|
const slug = label
|
|
@@ -591,23 +623,22 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
|
|
|
591
623
|
try {
|
|
592
624
|
raw = await readFile(specPath, "utf8");
|
|
593
625
|
}
|
|
594
|
-
catch {
|
|
595
|
-
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (error.code === "ENOENT")
|
|
628
|
+
return undefined;
|
|
629
|
+
throw error;
|
|
596
630
|
}
|
|
597
631
|
let spec;
|
|
598
632
|
try {
|
|
599
633
|
spec = JSON.parse(raw);
|
|
600
634
|
}
|
|
601
635
|
catch {
|
|
602
|
-
|
|
636
|
+
throw new Error("capability-boundary-source-invalid: run.json is malformed");
|
|
603
637
|
}
|
|
604
638
|
for (const task of spec.tasks ?? []) {
|
|
605
639
|
const boundary = task.shell?.frontendDesignPolicy?.capabilityBoundary;
|
|
606
|
-
if (boundary
|
|
607
|
-
(
|
|
608
|
-
(boundary.forbidden?.length ?? 0) > 0)) {
|
|
609
|
-
return boundary;
|
|
610
|
-
}
|
|
640
|
+
if (boundary !== undefined)
|
|
641
|
+
return capabilityBoundarySchema.parse(boundary);
|
|
611
642
|
}
|
|
612
643
|
return undefined;
|
|
613
644
|
}
|
|
@@ -617,46 +648,67 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
|
|
|
617
648
|
export function composeDependencyPolicyWithBoundary(policy, boundary) {
|
|
618
649
|
if (!boundary)
|
|
619
650
|
return policy;
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
"
|
|
632
|
-
|
|
651
|
+
const frozen = capabilityBoundarySchema.parse(boundary);
|
|
652
|
+
// Replace only our explicit complete block; a prose mention never grants
|
|
653
|
+
// ownership of the author's following constraints.
|
|
654
|
+
const begin = "<!-- frontend-capability-boundary:start -->";
|
|
655
|
+
const end = "<!-- frontend-capability-boundary:end -->";
|
|
656
|
+
const source = policy ?? "";
|
|
657
|
+
const start = source.indexOf(begin);
|
|
658
|
+
const finish = start < 0 ? -1 : source.indexOf(end, start + begin.length);
|
|
659
|
+
const prefix = (finish < 0 ? source : source.slice(0, start) + source.slice(finish + end.length)).trim();
|
|
660
|
+
const parts = [
|
|
661
|
+
frozen.allowed?.length ? `仅可使用 ${frozen.allowed.join("、")}` : undefined,
|
|
662
|
+
frozen.forbidden?.length ? `禁止 ${frozen.forbidden.join("、")}` : undefined,
|
|
663
|
+
frozen.requiredClauses?.length ? `必须满足:${frozen.requiredClauses.join(";")}` : undefined,
|
|
664
|
+
].filter(Boolean);
|
|
665
|
+
const sentence = `${CAPABILITY_BOUNDARY_MARKER}:${parts.join(";")}。`;
|
|
666
|
+
const block = `${begin}\n${sentence}\n${end}`;
|
|
667
|
+
return prefix ? `${prefix}\n${block}` : block;
|
|
633
668
|
}
|
|
634
669
|
/** Read the generation-frozen requirement→file ownership from run.json
|
|
635
|
-
* (frontendDesignPolicy spec)
|
|
636
|
-
export async function deriveRequirementOwnershipFromRun(runDir) {
|
|
670
|
+
* (frontendDesignPolicy spec), bound to the canonical source identity. */
|
|
671
|
+
export async function deriveRequirementOwnershipFromRun(runDir, expectedBinding) {
|
|
637
672
|
const specPath = path.join(runDir, "run.json");
|
|
638
673
|
let raw;
|
|
639
674
|
try {
|
|
640
675
|
raw = await readFile(specPath, "utf8");
|
|
641
676
|
}
|
|
642
|
-
catch {
|
|
643
|
-
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (error.code === "ENOENT" && (!expectedBinding || !isDagSourceBindingV2(expectedBinding)))
|
|
679
|
+
return [];
|
|
680
|
+
if (error.code === "ENOENT")
|
|
681
|
+
throw new Error("requirement-ownership-source-invalid: frozen run.json is missing");
|
|
682
|
+
throw error;
|
|
644
683
|
}
|
|
645
684
|
let spec;
|
|
646
685
|
try {
|
|
647
686
|
spec = JSON.parse(raw);
|
|
648
687
|
}
|
|
649
688
|
catch {
|
|
650
|
-
|
|
689
|
+
throw new Error("requirement-ownership-source-invalid: run.json is malformed");
|
|
651
690
|
}
|
|
652
691
|
const merged = new Map();
|
|
653
692
|
for (const task of spec.tasks ?? []) {
|
|
654
|
-
|
|
693
|
+
const raw = task.shell?.frontendDesignPolicy?.requirementOwnership;
|
|
694
|
+
if (raw === undefined)
|
|
695
|
+
continue;
|
|
696
|
+
const parsed = z.array(z.object({ requirementIds: z.array(z.string().min(1)).min(1), implementationFiles: z.array(z.string().min(1)).optional(), verificationFiles: z.array(z.string().min(1)).optional() }).strict()).safeParse(raw);
|
|
697
|
+
if (!parsed.success)
|
|
698
|
+
throw new Error("requirement-ownership-source-invalid: frozen requirementOwnership is malformed");
|
|
699
|
+
for (const entry of parsed.data) {
|
|
655
700
|
const key = [...(entry.requirementIds ?? [])].sort().join("|");
|
|
656
|
-
|
|
701
|
+
const previous = merged.get(key);
|
|
702
|
+
if (previous && serializeDeterministicJson(previous) !== serializeDeterministicJson(entry))
|
|
703
|
+
throw new Error("requirement-ownership-source-invalid: conflicting ownership declarations");
|
|
704
|
+
if (!previous)
|
|
657
705
|
merged.set(key, entry);
|
|
658
706
|
}
|
|
659
707
|
}
|
|
708
|
+
if (merged.size && expectedBinding) {
|
|
709
|
+
if (!spec.sourceBinding || serializeDeterministicJson(spec.sourceBinding) !== serializeDeterministicJson(expectedBinding))
|
|
710
|
+
throw new Error("requirement-ownership-source-invalid: frozen source binding mismatch or missing");
|
|
711
|
+
}
|
|
660
712
|
return [...merged.values()];
|
|
661
713
|
}
|
|
662
714
|
/** Deterministic requirement→file ownership check. A requirement's
|
|
@@ -702,27 +754,15 @@ function checkRequirementOwnership(canonical, ownership) {
|
|
|
702
754
|
continue;
|
|
703
755
|
if (implementationFiles.length > 0) {
|
|
704
756
|
const allTargets = requirement.implementationTargets ?? [];
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// claims: a contract may require a mounting file to exist for a
|
|
715
|
-
// requirement implemented elsewhere (smoke r55 — a deliverable
|
|
716
|
-
// otherwise made plan finalize permanently unfixable).
|
|
717
|
-
const deliverables = new Set(requirement.deliverableTargets ?? []);
|
|
718
|
-
const ownedTargets = targets.filter((file) => !deliverables.has(file));
|
|
719
|
-
// Only files OUTSIDE the declared set violate. An empty owned set
|
|
720
|
-
// means every mapped file is a deliverable/test-shaped obligation —
|
|
721
|
-
// unplaced requirements are rejected upstream, not here (r56:
|
|
722
|
-
// "maps [X] expects [X]" still violated when deliverables covered
|
|
723
|
-
// the whole mapping).
|
|
724
|
-
const outside = ownedTargets.filter((file) => !implementationFiles.includes(file));
|
|
725
|
-
if (outside.length > 0) {
|
|
757
|
+
// A verification placement must have explicit verification
|
|
758
|
+
// ownership AND a bound verification target. File names alone
|
|
759
|
+
// never exempt implementation responsibility.
|
|
760
|
+
const verificationPlacements = new Set([...(targetsByRequirement.get(id) ?? [])].filter(file => verificationFiles.includes(file)));
|
|
761
|
+
const targets = allTargets.filter(file => !verificationPlacements.has(file) || implementationFiles.includes(file));
|
|
762
|
+
// implementationTargets retains actual placements. Existence-only
|
|
763
|
+
// deliverableTargets never subtract from this ownership claim.
|
|
764
|
+
const outside = targets.filter(file => !implementationFiles.includes(file));
|
|
765
|
+
if (outside.length > 0 || !targets.some(file => implementationFiles.includes(file))) {
|
|
726
766
|
findings.push({
|
|
727
767
|
code: "requirement-ownership-violated",
|
|
728
768
|
message: `requirement ${id} expects implementation within [${implementationFiles.join(", ")}] but plan maps [${targets.join(", ") || "none"}] — re-map the owning behavior group's module placement`,
|
|
@@ -761,7 +801,7 @@ const FRONTEND_CONTRACT_REVIEW_GROUPS = [
|
|
|
761
801
|
},
|
|
762
802
|
{
|
|
763
803
|
id: "data-design",
|
|
764
|
-
fields: ["mockApi", "designEvidence", "dependencyPolicy", "realIntegrationGap"],
|
|
804
|
+
fields: ["mockApi", "designEvidence", "capabilityBoundary", "dependencyPolicy", "realIntegrationGap"],
|
|
765
805
|
},
|
|
766
806
|
{
|
|
767
807
|
id: "verification",
|
|
@@ -1103,11 +1143,15 @@ export const frontendImplementationContractSchema = z
|
|
|
1103
1143
|
uiStates: z.array(z.string().min(1)),
|
|
1104
1144
|
// Display-only test scope; never read by control flow.
|
|
1105
1145
|
scope: frontendVerificationScopeSchema.optional(),
|
|
1146
|
+
// Planned evidence strength, never proof of observed behavior.
|
|
1147
|
+
evidenceLevel: z.enum(["unit", "mounted", "real-integration"]).optional(),
|
|
1148
|
+
boundary: z.string().min(1).optional(),
|
|
1106
1149
|
})
|
|
1107
1150
|
.strict()).min(1),
|
|
1108
1151
|
evidenceGaps: z.array(gap).optional(),
|
|
1109
1152
|
stylingStrategy: z.string().min(1).optional(),
|
|
1110
1153
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
1154
|
+
capabilityBoundary: capabilityBoundarySchema.optional(),
|
|
1111
1155
|
dependencyPolicy: z.string().min(1).optional(),
|
|
1112
1156
|
residualRisks: z.array(z.string().min(1)).optional(),
|
|
1113
1157
|
realIntegrationGap: z.string().min(1).optional(),
|
|
@@ -2034,7 +2078,7 @@ function assertFrontendContractPathsSafe(value) {
|
|
|
2034
2078
|
const record = asRecord(value);
|
|
2035
2079
|
if (!record)
|
|
2036
2080
|
return;
|
|
2037
|
-
const pathFields = ["files", "file", "implementationTargets", "fixture", "consumer"];
|
|
2081
|
+
const pathFields = ["files", "file", "implementationTargets", "deliverableTargets", "fixture", "consumer"];
|
|
2038
2082
|
for (const [key, child] of Object.entries(record)) {
|
|
2039
2083
|
if (pathFields.includes(key) && Array.isArray(child)) {
|
|
2040
2084
|
for (const item of child) {
|
|
@@ -2328,6 +2372,8 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
2328
2372
|
file,
|
|
2329
2373
|
symbol: asString(vt.symbol) || undefined,
|
|
2330
2374
|
...(scope ? { scope } : {}),
|
|
2375
|
+
...(vt.evidenceLevel !== undefined ? { evidenceLevel: vt.evidenceLevel } : {}),
|
|
2376
|
+
...(vt.boundary !== undefined ? { boundary: vt.boundary } : {}),
|
|
2331
2377
|
requirementIds: requirementIds.length > 0
|
|
2332
2378
|
? requirementIds
|
|
2333
2379
|
: [...canonicalBinding.requirementIds],
|
|
@@ -2757,7 +2803,10 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2757
2803
|
// field (r10/r11 design-review findings).
|
|
2758
2804
|
const referencedConcretePaths = [
|
|
2759
2805
|
...new Set([
|
|
2760
|
-
...result.data.requirements.flatMap(
|
|
2806
|
+
...result.data.requirements.flatMap(requirement => [
|
|
2807
|
+
...requirement.implementationTargets,
|
|
2808
|
+
...(requirement.deliverableTargets ?? []),
|
|
2809
|
+
]),
|
|
2761
2810
|
// Mock endpoint paths stay in the set: the zod refine validated
|
|
2762
2811
|
// them against the declared targets.files patterns, so the
|
|
2763
2812
|
// narrowing must keep covering them. Verification-target files
|
|
@@ -2767,6 +2816,9 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2767
2816
|
...(result.data.mockApi?.endpoints ?? []).flatMap((endpoint) => [endpoint.fixture, endpoint.consumer].filter((value) => typeof value === "string" && value.length > 0)),
|
|
2768
2817
|
].filter((value) => value.length > 0)),
|
|
2769
2818
|
].sort();
|
|
2819
|
+
const outsideDelivery = result.data.requirements.flatMap(requirement => requirement.deliverableTargets ?? []).filter(file => !parsedTargetFiles.some(pattern => pathMatchesPattern(file, pattern)));
|
|
2820
|
+
if (outsideDelivery.length)
|
|
2821
|
+
fail("blocked", `target-path-outside-write-set: deliverable paths [${outsideDelivery.join(", ")}] exceed frozen targets`, candidateJsonSha256);
|
|
2770
2822
|
if (referencedConcretePaths.length > 0) {
|
|
2771
2823
|
result.data.targets = {
|
|
2772
2824
|
...result.data.targets,
|
|
@@ -2795,7 +2847,26 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2795
2847
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
2796
2848
|
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
2797
2849
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
2798
|
-
|
|
2850
|
+
// Both finalize prechecks and the final materializer enter this function.
|
|
2851
|
+
// Check the submitted placement before normalization can invent a fallback.
|
|
2852
|
+
const ownership = await deriveRequirementOwnershipFromRun(input.runDir, input.sourceBinding);
|
|
2853
|
+
for (const entry of ownership) {
|
|
2854
|
+
if (!entry.implementationFiles?.length)
|
|
2855
|
+
continue;
|
|
2856
|
+
for (const id of entry.requirementIds) {
|
|
2857
|
+
const original = asRecord(parsed)?.requirements?.find(item => asRecord(item)?.id === id);
|
|
2858
|
+
if (!original || asStringArray(asRecord(original)?.implementationTargets).length === 0)
|
|
2859
|
+
fail("retryable-invalid", `requirement-ownership-placement-missing: ${id} requires an explicit placement from its Plan facts`, candidateJsonSha256);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
const ownershipFindings = checkRequirementOwnership(result.data, ownership);
|
|
2863
|
+
if (ownershipFindings.length)
|
|
2864
|
+
throw new PlanPolicyPrecheckFailure(ownershipFindings.map(f => `${f.code}: ${f.message}`).join("; "), ownershipFindings);
|
|
2865
|
+
const frozenBoundary = await deriveFrontendCapabilityBoundaryFromRun(input.runDir);
|
|
2866
|
+
delete result.data.capabilityBoundary;
|
|
2867
|
+
if (frozenBoundary)
|
|
2868
|
+
result.data.capabilityBoundary = frozenBoundary;
|
|
2869
|
+
result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, frozenBoundary);
|
|
2799
2870
|
return {
|
|
2800
2871
|
canonical: result.data,
|
|
2801
2872
|
candidateRawSha256,
|
|
@@ -2992,6 +3063,7 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
2992
3063
|
};
|
|
2993
3064
|
}
|
|
2994
3065
|
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
3066
|
+
"/capabilityBoundary",
|
|
2995
3067
|
"/schemaVersion",
|
|
2996
3068
|
"/sourceBinding",
|
|
2997
3069
|
"/riskLevel",
|
|
@@ -3038,7 +3110,7 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
3038
3110
|
if (!record)
|
|
3039
3111
|
return ["/"];
|
|
3040
3112
|
const violations = [];
|
|
3041
|
-
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
3113
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel", "capabilityBoundary"]) {
|
|
3042
3114
|
if (Object.hasOwn(record, key))
|
|
3043
3115
|
violations.push(`/${key}`);
|
|
3044
3116
|
}
|
|
@@ -3052,6 +3124,22 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
3052
3124
|
violations.push("/mockApi/productionDefaultOff");
|
|
3053
3125
|
return violations;
|
|
3054
3126
|
}
|
|
3127
|
+
/** Read-only verification references do not grant writer permissions. */
|
|
3128
|
+
export async function collectExistingFrontendVerificationPaths(workspaceRoot, contract) {
|
|
3129
|
+
if (!workspaceRoot)
|
|
3130
|
+
return [];
|
|
3131
|
+
const existing = [];
|
|
3132
|
+
for (const target of contract.verificationTargets) {
|
|
3133
|
+
if (target.mode === "static" || existing.includes(target.file))
|
|
3134
|
+
continue;
|
|
3135
|
+
try {
|
|
3136
|
+
await readFileBoundarySafe({ boundaryRoot: workspaceRoot, targetPath: path.resolve(workspaceRoot, target.file), label: "frontend existing verification" });
|
|
3137
|
+
existing.push(target.file);
|
|
3138
|
+
}
|
|
3139
|
+
catch { /* Missing or unsafe files cannot bypass the write-set check. */ }
|
|
3140
|
+
}
|
|
3141
|
+
return existing;
|
|
3142
|
+
}
|
|
3055
3143
|
function workspaceRootFromDagRunDir(runDir) {
|
|
3056
3144
|
const absolute = path.resolve(runDir);
|
|
3057
3145
|
const segments = absolute.split(path.sep);
|
|
@@ -3255,9 +3343,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
3255
3343
|
...checkRequiredDeliverables(analysis.canonical),
|
|
3256
3344
|
...checkUxNameCollisions(analysis.canonical),
|
|
3257
3345
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
3258
|
-
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
3346
|
+
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir), await collectExistingFrontendVerificationPaths(workspaceRootFromDagRunDir(input.runDir), analysis.canonical)),
|
|
3259
3347
|
...checkComponentChoiceSourceReferences(analysis.canonical, await deriveFrontendSourceBindingPathsFromRun(input.runDir)),
|
|
3260
|
-
...checkRequirementOwnership(analysis.canonical, await deriveRequirementOwnershipFromRun(input.runDir)),
|
|
3261
3348
|
...checkInteractionIdVocabulary(analysis.canonical, await deriveFrontendInteractionIdsFromRun(input.runDir)),
|
|
3262
3349
|
...checkMockApiStrategyAllowed(analysis.canonical, await deriveFrontendAllowedMockStrategiesFromRun(input.runDir)),
|
|
3263
3350
|
];
|