@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.10
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 +129 -9
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1214 -584
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +78 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +213 -1181
- package/dist/worker/console/chat/routes.js +212 -71
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-D_fNbWc_.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_HuqLW6m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-Djy2qY1m.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-bB-1TSyY.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-C5Hmghrz.js} +1 -1
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-Duyl39oy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-x6mdnSyf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-WlBuFj7W.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CTF429HX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-Dwd6r-SF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-C-Br82D1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CuGp0DeE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-CJhFX6Lh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-DpGOa_jT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-DXJaTynd.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-CRBZWjE4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BdXcxtlb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-07UKsOts.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-D38p9__S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-nSiBNNjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BFOTot5D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-DypKaZ6k.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-B6yoOQeW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-BHYS6viX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-D2VD5Snj.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-E98mWxN1.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-CT1zxhqP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-6GcngNcy.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-DCL6NvKe.js} +1 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +468 -0
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-C0627e-9.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DPDPv9HL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-D3OHdWiO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DI88zD7n.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-p2l8IrWY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-B1bAX-oS.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-DwOnWxdD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-xRkTXnJC.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-_9UC5Zio.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CgEUirWJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-Czbpwiyz.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-_j7_KiqY.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-BNJa6Hyi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-DedpKtOc.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BBvthr7c.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-Ds7umgEM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-CKa-p1nF.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-LoUNR56o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Bqbl6ym7.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B8dUg4lf.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +263 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +742 -268
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +49 -11
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +38 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +6 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +9 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
2
|
+
/** An execution packing target, not a claim about any model's context limit. */
|
|
3
|
+
export const FRONTEND_SCOPE_TARGET_BYTES = 24_000;
|
|
4
|
+
export function packFrontendInputUnits(units, options = {}) {
|
|
5
|
+
const target = options.targetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
6
|
+
const maxUnits = options.maxUnits ?? 4;
|
|
7
|
+
if (!Number.isFinite(target) || target <= 0 || !Number.isInteger(maxUnits) || maxUnits < 1)
|
|
8
|
+
throw Error("FRONTEND_INPUT_POLICY_INVALID");
|
|
9
|
+
const batches = [];
|
|
10
|
+
let batch = [];
|
|
11
|
+
let bytes = 2;
|
|
12
|
+
let cost = 0;
|
|
13
|
+
const ids = new Set();
|
|
14
|
+
for (const unit of units) {
|
|
15
|
+
if (!unit.id || ids.has(unit.id))
|
|
16
|
+
throw Error(`FRONTEND_INPUT_ID_CONFLICT: ${unit.id}`);
|
|
17
|
+
ids.add(unit.id);
|
|
18
|
+
const size = Buffer.byteLength(JSON.stringify(unit), "utf8") + 1;
|
|
19
|
+
if (options.maxUnitBytes !== undefined && size > options.maxUnitBytes)
|
|
20
|
+
throw Error(`FRONTEND_INPUT_UNIT_TOO_LARGE: ${unit.id} requires ${size} bytes; split the source obligation without dropping conditions`);
|
|
21
|
+
const unitCost = options.cost?.(unit) ?? 1;
|
|
22
|
+
if (batch.length && (batch.length >= maxUnits || bytes + size > target || cost + unitCost > (options.maxCost ?? Infinity))) {
|
|
23
|
+
batches.push(batch);
|
|
24
|
+
batch = [];
|
|
25
|
+
bytes = 2;
|
|
26
|
+
cost = 0;
|
|
27
|
+
}
|
|
28
|
+
batch.push(unit);
|
|
29
|
+
bytes += size;
|
|
30
|
+
cost += unitCost;
|
|
31
|
+
}
|
|
32
|
+
if (batch.length)
|
|
33
|
+
batches.push(batch);
|
|
34
|
+
return batches;
|
|
35
|
+
}
|
|
36
|
+
/** Select whole obligations before serialization; never clip identities or text. */
|
|
37
|
+
export function projectFrontendInputScope(payload, scopeIds) {
|
|
38
|
+
const byId = new Map(payload.requirements.map(r => [r.id, r]));
|
|
39
|
+
const requirements = [...new Set(scopeIds)].map(id => {
|
|
40
|
+
const unit = byId.get(id);
|
|
41
|
+
if (!unit)
|
|
42
|
+
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${id}`);
|
|
43
|
+
return unit;
|
|
44
|
+
});
|
|
45
|
+
const fragments = new Set(requirements.flatMap(r => Array.isArray(r.sourceFragmentIds) ? r.sourceFragmentIds : []));
|
|
46
|
+
const inScope = (fact) => {
|
|
47
|
+
const ids = typeof fact.requirementId === "string" ? [fact.requirementId] : Array.isArray(fact.requirementIds) ? fact.requirementIds : [];
|
|
48
|
+
return !ids.length || ids.some(id => scopeIds.includes(String(id)));
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
...payload, requirements,
|
|
52
|
+
...(Array.isArray(payload.sharedFacts) ? { sharedFacts: payload.sharedFacts.filter(f => f && typeof f === "object" && inScope(f)).map(f => f.kind === "required-deliverables" && Array.isArray(f.items) ? { ...f, items: f.items.filter((item) => inScope(item)) } : f) } : {}),
|
|
53
|
+
...(Array.isArray(payload.fragments) ? { fragments: payload.fragments.filter(f => f && typeof f === "object" && fragments.has(f.id)) } : {}),
|
|
54
|
+
inputManifest: { schemaVersion: 1, semantics: "full", sourceSha256: sha256OfCanonicalJson(payload), scopeIds: requirements.map(r => r.id), excludedIds: payload.requirements.filter(r => !scopeIds.includes(r.id)).map(r => r.id) },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function parseFrontendInputBlock(prompt, phase) {
|
|
58
|
+
const block = new RegExp(`<frontend_${phase}_input>[\\s\\S]*?<\\/frontend_${phase}_input>`).exec(prompt)?.[0];
|
|
59
|
+
if (!block)
|
|
60
|
+
return undefined;
|
|
61
|
+
for (const line of block.split(/\r?\n/)) {
|
|
62
|
+
try {
|
|
63
|
+
const payload = JSON.parse(line);
|
|
64
|
+
if (Array.isArray(payload.requirements))
|
|
65
|
+
return { block, payload };
|
|
66
|
+
}
|
|
67
|
+
catch { /* wrapper prose is not JSON */ }
|
|
68
|
+
}
|
|
69
|
+
throw Error(`FRONTEND_INPUT_INVALID: ${phase} compiled inventory is not parseable`);
|
|
70
|
+
}
|
|
71
|
+
export function projectFrontendContractPrompt(prompt, scopeIds) {
|
|
72
|
+
const input = parseFrontendInputBlock(prompt, "contract");
|
|
73
|
+
if (!input)
|
|
74
|
+
throw Error("FRONTEND_INPUT_MISSING: contract compiled inventory unavailable");
|
|
75
|
+
return prompt.replace(input.block, ["<frontend_contract_input>", "Complete canonical obligations for this session only. Confirm each id with record_requirement; preserve all independent conditions.", JSON.stringify(projectFrontendInputScope(input.payload, scopeIds)), "</frontend_contract_input>"].join("\n"));
|
|
76
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { collectCanonicalStateFlowNames } from "./frontend-contract-facts.js";
|
|
2
|
+
export function committedFactFromPlanRecord(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
|
+
return undefined;
|
|
5
|
+
const record = value;
|
|
6
|
+
if (record.phase !== undefined && record.phase !== "committed")
|
|
7
|
+
return undefined;
|
|
8
|
+
const fact = record.fact;
|
|
9
|
+
return fact && typeof fact === "object" && !Array.isArray(fact)
|
|
10
|
+
? fact
|
|
11
|
+
: typeof record.kind === "string"
|
|
12
|
+
? record
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
export function planFactStringList(value) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return [];
|
|
18
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
19
|
+
}
|
|
20
|
+
export function planFactScopeIntersects(fact, requirementIds) {
|
|
21
|
+
return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
|
|
22
|
+
}
|
|
23
|
+
/** Compute the authoritative coverage queue from the committed plan ledger. */
|
|
24
|
+
export function collectFrontendPlanMissingFacts(input) {
|
|
25
|
+
const requirements = new Map();
|
|
26
|
+
const standaloneEvidenceGaps = new Set();
|
|
27
|
+
const verificationTargetIds = new Set();
|
|
28
|
+
const verificationTargetRequirements = new Map();
|
|
29
|
+
for (const value of input.committedFacts) {
|
|
30
|
+
const fact = committedFactFromPlanRecord(value);
|
|
31
|
+
if (!fact || fact.origin !== "plan")
|
|
32
|
+
continue;
|
|
33
|
+
if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
|
|
34
|
+
const entry = fact.entry;
|
|
35
|
+
if (typeof entry.id === "string" && entry.id.trim())
|
|
36
|
+
requirements.set(entry.id, entry);
|
|
37
|
+
}
|
|
38
|
+
if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
|
|
39
|
+
const entry = fact.entry;
|
|
40
|
+
const id = entry.id;
|
|
41
|
+
if (typeof id === "string" && id.trim()) {
|
|
42
|
+
verificationTargetIds.add(id);
|
|
43
|
+
verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
|
|
44
|
+
? entry.requirementIds.filter((value) => typeof value === "string")
|
|
45
|
+
: []));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
|
|
49
|
+
const entry = fact.entry;
|
|
50
|
+
const requirementId = entry.requirementId;
|
|
51
|
+
const description = entry.description;
|
|
52
|
+
if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
|
|
53
|
+
standaloneEvidenceGaps.add(requirementId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const missing = [];
|
|
58
|
+
for (const id of input.requirementIds) {
|
|
59
|
+
const entry = requirements.get(id);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
missing.push({
|
|
62
|
+
kind: "plan-requirement",
|
|
63
|
+
id,
|
|
64
|
+
requirementIds: [id],
|
|
65
|
+
reason: `requirement ${id} has no committed plan-requirement fact`,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const targetIds = Array.isArray(entry.verificationTargetIds)
|
|
70
|
+
? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
71
|
+
: [];
|
|
72
|
+
const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
|
|
73
|
+
? entry.evidenceGap
|
|
74
|
+
: undefined;
|
|
75
|
+
const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
|
|
76
|
+
standaloneEvidenceGaps.has(id);
|
|
77
|
+
if (targetIds.length === 0 && !hasEvidenceGap) {
|
|
78
|
+
missing.push({
|
|
79
|
+
kind: "plan-verification-target",
|
|
80
|
+
requirementIds: [id],
|
|
81
|
+
reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const targetId of targetIds) {
|
|
86
|
+
if (!verificationTargetIds.has(targetId) ||
|
|
87
|
+
!verificationTargetRequirements.get(targetId)?.has(id)) {
|
|
88
|
+
missing.push({
|
|
89
|
+
kind: "plan-verification-target",
|
|
90
|
+
id: targetId,
|
|
91
|
+
requirementIds: [id],
|
|
92
|
+
reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return missing;
|
|
98
|
+
}
|
|
99
|
+
/** Completeness checks for phases whose facts are committed incrementally. */
|
|
100
|
+
export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
101
|
+
const facts = input.committedFacts
|
|
102
|
+
.map(committedFactFromPlanRecord)
|
|
103
|
+
.filter((fact) => Boolean(fact && fact.origin === "plan"));
|
|
104
|
+
if (input.phase === "ux-registry") {
|
|
105
|
+
return facts.some((fact) => fact.kind === "state-registry")
|
|
106
|
+
? []
|
|
107
|
+
: [
|
|
108
|
+
{
|
|
109
|
+
kind: "state-registry",
|
|
110
|
+
requirementIds: [...input.requirementIds],
|
|
111
|
+
reason: "global UX vocabulary phase has no committed state-registry fact",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
if (input.phase === "ux-local") {
|
|
116
|
+
// One snapshot per check: no cross-revision cache to invalidate.
|
|
117
|
+
const required = new Set(input.behaviorRequiredRequirementIds ?? []);
|
|
118
|
+
const requested = new Set(input.requirementIds.filter(id => required.has(id)));
|
|
119
|
+
if (!requested.size)
|
|
120
|
+
return [];
|
|
121
|
+
const byRequirement = new Map();
|
|
122
|
+
for (const fact of facts) {
|
|
123
|
+
for (const id of new Set(planFactStringList(fact.scopeRequirementIds))) {
|
|
124
|
+
if (!requested.has(id))
|
|
125
|
+
continue;
|
|
126
|
+
const entries = byRequirement.get(id) ?? [];
|
|
127
|
+
entries.push(fact);
|
|
128
|
+
byRequirement.set(id, entries);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const liveStateFlow = collectCanonicalStateFlowNames(facts);
|
|
132
|
+
const missing = [];
|
|
133
|
+
for (const id of input.requirementIds) {
|
|
134
|
+
if (!required.has(id))
|
|
135
|
+
continue;
|
|
136
|
+
const scopedFacts = byRequirement.get(id) ?? [];
|
|
137
|
+
const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
|
|
138
|
+
Array.isArray(fact.uiComponentChoices) &&
|
|
139
|
+
fact.uiComponentChoices.length > 0);
|
|
140
|
+
const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
|
|
141
|
+
const hasStateFlow = [...canonicalStateFlow.uiStateNames].some(name => liveStateFlow.uiStateNames.has(name)) ||
|
|
142
|
+
[...canonicalStateFlow.interactionNames].some(name => liveStateFlow.interactionNames.has(name));
|
|
143
|
+
if (!hasChoice) {
|
|
144
|
+
missing.push({
|
|
145
|
+
kind: "component-choice",
|
|
146
|
+
requirementIds: [id],
|
|
147
|
+
reason: "behaviour-required UX slice has no committed component-choice fact",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (!hasStateFlow) {
|
|
151
|
+
missing.push({
|
|
152
|
+
kind: "state-flow",
|
|
153
|
+
requirementIds: [id],
|
|
154
|
+
reason: "behaviour-required UX slice has no committed state-flow fact",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return missing;
|
|
159
|
+
}
|
|
160
|
+
const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
|
|
161
|
+
const allInteractions = collectCanonicalStateFlowNames(facts).interactionNames;
|
|
162
|
+
const requested = new Set(input.requirementIds);
|
|
163
|
+
const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, requested))).interactionNames].filter(name => allInteractions.has(name)));
|
|
164
|
+
const coveredInteractions = new Set(facts
|
|
165
|
+
.filter((fact) => fact.kind === "data-flow")
|
|
166
|
+
.flatMap((fact) => planFactStringList(fact.interactions)));
|
|
167
|
+
const missing = [];
|
|
168
|
+
if (!hasMockApi) {
|
|
169
|
+
missing.push({
|
|
170
|
+
kind: "mock-api",
|
|
171
|
+
requirementIds: [...input.requirementIds],
|
|
172
|
+
reason: "global Mock/data phase has no committed mock-api fact",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
for (const interaction of liveInteractions) {
|
|
176
|
+
if (coveredInteractions.has(interaction))
|
|
177
|
+
continue;
|
|
178
|
+
missing.push({
|
|
179
|
+
kind: "data-flow",
|
|
180
|
+
id: interaction,
|
|
181
|
+
requirementIds: [...input.requirementIds],
|
|
182
|
+
reason: `interaction ${interaction} has no committed data-flow fact`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return missing;
|
|
186
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Precise failures own their recovery. A stale transport length marker must
|
|
2
|
+
* never turn cancellation, provider rejection or policy failure into progress. */
|
|
3
|
+
export function classifyFrontendPlanRecovery(result) {
|
|
4
|
+
const category = result.failureCategory ?? "";
|
|
5
|
+
if (result.timedOut || result.stopReason === "aborted" ||
|
|
6
|
+
/interrupt|termination-unconfirmed|budget_breach/.test(category) ||
|
|
7
|
+
["cancelled", "auth", "missing-api-key", "rate-limit", "quota", "timeout", "tool-policy", "write-guard", "path-violation", "governance-blocked", "human-rejected"].includes(category))
|
|
8
|
+
return "stop";
|
|
9
|
+
if (result.ok)
|
|
10
|
+
return "none";
|
|
11
|
+
if (category === "output-limit")
|
|
12
|
+
return "output";
|
|
13
|
+
if (["context-overflow", "context-budget-exhausted"].includes(category))
|
|
14
|
+
return "context";
|
|
15
|
+
if (result.stopReason === "length" && ["", "empty-output", "network", "nonzero-exit", "unknown"].includes(category))
|
|
16
|
+
return "output";
|
|
17
|
+
return "none";
|
|
18
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
1
2
|
/**
|
|
2
3
|
* Deterministic Markdown renderer for a canonical frontend implementation
|
|
3
4
|
* contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
|
|
@@ -17,13 +18,19 @@ function bulletList(items) {
|
|
|
17
18
|
* prose — the renderer must never present it as requirement content. */
|
|
18
19
|
const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
|
|
19
20
|
function requirementsCoverage(contract) {
|
|
20
|
-
const
|
|
21
|
+
const renderRequirement = (requirement, includeOutcome = true) => {
|
|
21
22
|
const targets = requirement.implementationTargets.join(", ");
|
|
22
23
|
const verificationTargets = requirement.verificationTargetIds.join(", ");
|
|
23
|
-
const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
|
+
const authoredOutcome = !includeOutcome || requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
25
|
? ""
|
|
25
26
|
: ` — ${requirement.expectedOutcome}`;
|
|
26
27
|
return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
|
|
28
|
+
};
|
|
29
|
+
const lines = collectFrontendExecutionGroups(contract.requirements).flatMap(group => {
|
|
30
|
+
const members = contract.requirements.filter(r => group.requirementIds.includes(r.id));
|
|
31
|
+
if (group.kind === "unclassified")
|
|
32
|
+
return members.map(r => renderRequirement(r));
|
|
33
|
+
return [`### ${group.id} (${group.kind})`, group.summary, ...members.map(r => renderRequirement(r, false)), "Full independent acceptance conditions remain in contracts/frontend-implementation-contract.json."];
|
|
27
34
|
});
|
|
28
35
|
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
29
36
|
}
|
|
@@ -72,7 +79,7 @@ function sourceFidelityLedger(contract) {
|
|
|
72
79
|
`- ledger: ${binding.ledgerPath}`,
|
|
73
80
|
`- ledgerSha256: ${binding.ledgerSha256 ?? "(missing)"}`,
|
|
74
81
|
`- inputDigest: ${binding.inputDigest ?? "(missing)"}`,
|
|
75
|
-
"- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json
|
|
82
|
+
"- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json;本计划按执行组展示共享方案;独立验收全文保留在 canonical contract,执行输入按完整义务分批。",
|
|
76
83
|
];
|
|
77
84
|
const boundCount = Object.keys(binding.requirementToFragments ?? {}).length;
|
|
78
85
|
if (boundCount > 0) {
|
|
@@ -96,13 +96,13 @@ export async function runRecoveryOperationController(input) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
// Total budget fold + hard stop (resource protection, fail-closed).
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
ledgers,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
99
|
+
try {
|
|
100
|
+
const ledgers = await readLedgers();
|
|
101
|
+
if (deps.budget)
|
|
102
|
+
budgetAggregate = foldOperationBudgets({ ledgers, budget: deps.budget, now });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { terminalReason: "needs-attention", attempts: attempt, budgetAggregate, fingerprints, detail: `Recovery budget evidence unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
106
106
|
}
|
|
107
107
|
if (budgetAggregate?.status === "breached") {
|
|
108
108
|
return {
|
|
@@ -87,6 +87,7 @@ export function foldOperationBudgets(input) {
|
|
|
87
87
|
limits: { ...input.budget.limits },
|
|
88
88
|
consumed: {
|
|
89
89
|
executorCalls: 0,
|
|
90
|
+
...(input.budget.limits.maxProviderRequests !== undefined ? { providerRequests: 0 } : {}),
|
|
90
91
|
wallTimeMs: 0,
|
|
91
92
|
tokens: null,
|
|
92
93
|
repairPasses: 0,
|
|
@@ -100,6 +101,15 @@ export function foldOperationBudgets(input) {
|
|
|
100
101
|
let knownTokenSum = 0;
|
|
101
102
|
const missing = new Set();
|
|
102
103
|
for (const ledger of input.ledgers) {
|
|
104
|
+
if (input.budget.limits.maxProviderRequests !== undefined) {
|
|
105
|
+
if (ledger.consumed.providerRequests === undefined) {
|
|
106
|
+
if (input.budget.mode === "hard")
|
|
107
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: missing operation request consumption");
|
|
108
|
+
delete aggregate.consumed.providerRequests;
|
|
109
|
+
}
|
|
110
|
+
else if (aggregate.consumed.providerRequests !== undefined)
|
|
111
|
+
aggregate.consumed.providerRequests += ledger.consumed.providerRequests;
|
|
112
|
+
}
|
|
103
113
|
aggregate.consumed.executorCalls += ledger.consumed.executorCalls;
|
|
104
114
|
aggregate.consumed.wallTimeMs += ledger.consumed.wallTimeMs;
|
|
105
115
|
aggregate.consumed.repairPasses += ledger.consumed.repairPasses;
|
|
@@ -119,6 +129,9 @@ export function foldOperationBudgets(input) {
|
|
|
119
129
|
aggregate.consumed.missingTokenNodeIds = [...missing].sort((a, b) => a.localeCompare(b));
|
|
120
130
|
if (aggregate.mode === "hard") {
|
|
121
131
|
const limits = aggregate.limits;
|
|
132
|
+
pushAggregateBreach(aggregate, "providerRequests", aggregate.consumed.providerRequests ?? 0, limits.maxProviderRequests, now);
|
|
133
|
+
if (aggregate.breaches.length)
|
|
134
|
+
aggregate.status = "breached";
|
|
122
135
|
pushAggregateBreach(aggregate, "executorCalls", aggregate.consumed.executorCalls, limits.maxExecutorCalls, now);
|
|
123
136
|
pushAggregateBreach(aggregate, "wallTimeMs", aggregate.consumed.wallTimeMs, limits.maxWallTimeMs, now);
|
|
124
137
|
pushAggregateBreach(aggregate, "repairPasses", aggregate.consumed.repairPasses, limits.maxRepairPasses, now);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFile, mkdir, readdir, readFile, rm, access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
3
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
5
|
import { sha256Hex } from "./frontend-implementation-contract.js";
|
|
5
6
|
import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_REL_DIR, FRONTEND_RECOVERY_STAGING_PREFIX, computeFrontendRecoveryPlan, computeFrontendRecoveryPlanForSource, } from "./frontend-recovery-plan.js";
|
|
@@ -413,6 +414,8 @@ async function materializeChildStaging(input) {
|
|
|
413
414
|
}
|
|
414
415
|
}
|
|
415
416
|
const childState = {
|
|
417
|
+
...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
|
|
418
|
+
...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
|
|
416
419
|
version: 1,
|
|
417
420
|
title: parentSpec.title,
|
|
418
421
|
runId: childRunId,
|
|
@@ -434,6 +437,7 @@ async function materializeChildStaging(input) {
|
|
|
434
437
|
}
|
|
435
438
|
: {}),
|
|
436
439
|
frontendRecoveryState: {
|
|
440
|
+
...(parentState.budgetLedger ? { budgetLineage: { parentLedgerSha256: sha256OfCanonicalJson(parentState.budgetLedger), inheritedAt: createdAt, inheritedConsumed: structuredClone(parentState.budgetLedger.consumed) } } : {}),
|
|
437
441
|
schemaVersion: 1,
|
|
438
442
|
phase: "child-running",
|
|
439
443
|
requestId,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, lstat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
5
|
+
import { readCommittedEvents } from "./frontend-typed-event-store.js";
|
|
6
|
+
import { stageTypedEventFact, adoptTypedEventFact } from "./frontend-typed-event-transaction.js";
|
|
7
|
+
const invalidScope = (message) => Object.assign(new Error(`FRONTEND_REVIEW_SCOPE_INVALID: ${message}`), { code: "FRONTEND_REVIEW_SCOPE_INVALID" });
|
|
8
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const isArtifactReference = (value) => isRecord(value)
|
|
10
|
+
&& typeof value.path === "string" && value.path.length > 0
|
|
11
|
+
&& typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256);
|
|
12
|
+
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
13
|
+
/** The section index is navigation; reconstruct coverage against the canonical source before using it. */
|
|
14
|
+
export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
|
|
15
|
+
const root = await realpath(runDir);
|
|
16
|
+
const files = new Map();
|
|
17
|
+
async function read(relative, expected) {
|
|
18
|
+
const absolute = path.resolve(root, relative);
|
|
19
|
+
if (!absolute.startsWith(root + path.sep) || (await lstat(absolute)).isSymbolicLink() || !(await realpath(absolute)).startsWith(root + path.sep))
|
|
20
|
+
throw invalidScope("artifact escaped run");
|
|
21
|
+
const text = await readFile(absolute, "utf8");
|
|
22
|
+
const sha256 = hash(text);
|
|
23
|
+
if (expected && sha256 !== expected)
|
|
24
|
+
throw Error(`FRONTEND_REVIEW_SCOPE_DRIFT: ${relative}`);
|
|
25
|
+
files.set(relative, sha256);
|
|
26
|
+
return { id: relative, text, sha256 };
|
|
27
|
+
}
|
|
28
|
+
let indexArtifact;
|
|
29
|
+
try {
|
|
30
|
+
indexArtifact = await read("contracts/frontend-implementation-contract.index.json");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === "ENOENT")
|
|
34
|
+
return undefined;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const index = JSON.parse(indexArtifact.text);
|
|
38
|
+
if (!isRecord(index) || !isArtifactReference(index.contract) || !Array.isArray(index.sections))
|
|
39
|
+
throw invalidScope("malformed contract index");
|
|
40
|
+
const canonicalArtifact = await read(index.contract.path, index.contract.sha256);
|
|
41
|
+
const canonical = JSON.parse(canonicalArtifact.text);
|
|
42
|
+
if (!isRecord(canonical))
|
|
43
|
+
throw invalidScope("canonical contract must be an object");
|
|
44
|
+
const covered = new Map();
|
|
45
|
+
const sections = [];
|
|
46
|
+
const ids = new Set();
|
|
47
|
+
for (const entry of index.sections) {
|
|
48
|
+
if (!isRecord(entry) || !isArtifactReference(entry) || !Array.isArray(entry.slices))
|
|
49
|
+
throw invalidScope("malformed section index entry");
|
|
50
|
+
if (ids.has(entry.path))
|
|
51
|
+
throw invalidScope("duplicate section identity");
|
|
52
|
+
ids.add(entry.path);
|
|
53
|
+
const section = await read(entry.path, entry.sha256);
|
|
54
|
+
const payload = JSON.parse(section.text);
|
|
55
|
+
if (!isRecord(payload) || !Array.isArray(payload.slices) || !isRecord(payload.data))
|
|
56
|
+
throw invalidScope(`malformed section: ${entry.path}`);
|
|
57
|
+
if (payload.contractSha256 !== canonicalArtifact.sha256 || sha256OfCanonicalJson(payload.slices) !== sha256OfCanonicalJson(entry.slices))
|
|
58
|
+
throw invalidScope("unbound section");
|
|
59
|
+
for (const slice of payload.slices) {
|
|
60
|
+
if (!isRecord(slice) || typeof slice.field !== "string" || !Object.hasOwn(canonical, slice.field) || !Object.hasOwn(payload.data, slice.field))
|
|
61
|
+
throw invalidScope(`missing or invalid field reference in ${entry.path}: ${String(slice?.field)}`);
|
|
62
|
+
const value = canonical[slice.field];
|
|
63
|
+
const positions = covered.get(slice.field) ?? new Set();
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
if (!Number.isInteger(slice.start) || !Number.isInteger(slice.end) || slice.start < 0 || slice.end < slice.start || slice.end > value.length || slice.total !== value.length)
|
|
66
|
+
throw invalidScope("invalid array coverage");
|
|
67
|
+
if ((value.length === 0 && covered.has(slice.field)) || (value.length > 0 && slice.start === slice.end))
|
|
68
|
+
throw invalidScope(`empty or duplicate array coverage: ${slice.field}`);
|
|
69
|
+
for (let i = slice.start; i < slice.end; i++) {
|
|
70
|
+
if (positions.has(i))
|
|
71
|
+
throw invalidScope("overlapping coverage");
|
|
72
|
+
positions.add(i);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (["start", "end", "total"].some(key => Object.hasOwn(slice, key)))
|
|
77
|
+
throw invalidScope(`scalar field has array range: ${slice.field}`);
|
|
78
|
+
if (positions.size)
|
|
79
|
+
throw invalidScope("duplicate field");
|
|
80
|
+
positions.add(0);
|
|
81
|
+
}
|
|
82
|
+
const range = Array.isArray(value) ? value.slice(slice.start, slice.end) : value;
|
|
83
|
+
if (sha256OfCanonicalJson(range) !== sha256OfCanonicalJson(payload.data[slice.field]))
|
|
84
|
+
throw invalidScope(`section differs from canonical obligation: ${slice.field}`);
|
|
85
|
+
covered.set(slice.field, positions);
|
|
86
|
+
}
|
|
87
|
+
const sectionFields = new Set(payload.slices.map(slice => slice.field));
|
|
88
|
+
if (Object.keys(payload.data).some(field => !sectionFields.has(field)))
|
|
89
|
+
throw invalidScope(`unindexed section data: ${entry.path}`);
|
|
90
|
+
sections.push(section);
|
|
91
|
+
}
|
|
92
|
+
if (Object.keys(canonical).some(field => !covered.has(field) || covered.get(field).size !== (Array.isArray(canonical[field]) ? canonical[field].length : 1)))
|
|
93
|
+
throw invalidScope("canonical field coverage incomplete");
|
|
94
|
+
const scopes = Buffer.byteLength(canonicalArtifact.text) <= targetBytes ? [canonicalArtifact] : sections;
|
|
95
|
+
if (phase === "review") {
|
|
96
|
+
const context = await read("contracts/frontend-review-context.json");
|
|
97
|
+
const payload = JSON.parse(context.text);
|
|
98
|
+
if (payload.contractRef.sha256 !== canonicalArtifact.sha256 || payload.contractRef.indexSha256 !== indexArtifact.sha256)
|
|
99
|
+
throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review context contract binding");
|
|
100
|
+
const diff = await read(payload.diff.reviewIndexPath);
|
|
101
|
+
const diffIndex = JSON.parse(diff.text);
|
|
102
|
+
await read(payload.diff.patchPath, payload.diff.patchSha256);
|
|
103
|
+
if (diffIndex.patchSha256 !== payload.diff.patchSha256)
|
|
104
|
+
throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review diff binding");
|
|
105
|
+
scopes.push(context);
|
|
106
|
+
for (const hunk of diffIndex.hunks)
|
|
107
|
+
scopes.push(await read(hunk.patchPath, hunk.sha256));
|
|
108
|
+
}
|
|
109
|
+
return { scopes, digest: sha256OfCanonicalJson([...files]), validate: async () => { for (const [relative, expected] of files)
|
|
110
|
+
await read(relative, expected); } };
|
|
111
|
+
}
|
|
112
|
+
/** Checkpoints attest explicit model completion of already supplied, hash-bound scopes. */
|
|
113
|
+
export function createFrontendReviewScopeProtocol(input) {
|
|
114
|
+
const kind = input.phase === "design" ? "design-scope-completed" : "review-scope-completed";
|
|
115
|
+
let active = new Set();
|
|
116
|
+
const completedScopeIds = () => new Set(readCommittedEvents(input.getStore(), input.attemptId).filter(r => r.fact.kind === kind).map(r => String(r.fact.id)));
|
|
117
|
+
const receipt = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
|
|
118
|
+
return {
|
|
119
|
+
setActiveScope(ids) { active = new Set(ids); },
|
|
120
|
+
completedScopeIds,
|
|
121
|
+
committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
|
|
122
|
+
assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
|
|
123
|
+
throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
|
|
124
|
+
customTools: !input.inventory ? [] : [{ name: "complete_review_scope", label: "complete_review_scope", description: "After reviewing the supplied complete scope and saving its findings, checkpoint its exact runtime id. This does not approve the implementation or design.", parameters: { type: "object", properties: { id: { type: "string", minLength: 1 } }, required: ["id"], additionalProperties: false },
|
|
125
|
+
async execute(callId, params) {
|
|
126
|
+
const scope = input.inventory.scopes.find(s => s.id === params.id);
|
|
127
|
+
if (!scope || !active.has(params.id))
|
|
128
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Only the supplied active review scope may be completed" });
|
|
129
|
+
await input.inventory.validate();
|
|
130
|
+
const store = input.getStore();
|
|
131
|
+
const requestId = `${input.attemptId}:${callId}:${randomUUID()}`;
|
|
132
|
+
const fact = { kind, id: scope.id, sha256: scope.sha256 };
|
|
133
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId: input.attemptId, fact });
|
|
134
|
+
const committed = await adoptTypedEventFact({ store, requestId, attemptId: input.attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
135
|
+
return receipt({ ok: true, kind, revision: committed.revision });
|
|
136
|
+
},
|
|
137
|
+
}],
|
|
138
|
+
};
|
|
139
|
+
}
|