@tea-agent/loop-agent 0.42.0-next.9 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- 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/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- 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/parse-intent.js +7 -0
- 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/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- 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 +78 -11
- 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 +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- 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 +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -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 +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -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/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- 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/index.html +4 -4
- 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/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- 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 +431 -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 +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- 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 +64 -22
- 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 +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- 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 +423 -41
- 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 +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- 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 +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- 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-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFrontendToolProgress } from "./frontend-durable-tools.js";
|
|
5
|
+
import { frontendModelCapabilities } from "../../shared/frontend-execution-policy.js";
|
|
6
|
+
/** Count returned text only after a successful native read. No filesystem reads or raw content retention. */
|
|
7
|
+
export function createFrontendReadObservation() {
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const finished = new Set();
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
let bytes = 0, duplicates = 0, complete = 0, rejected = 0, unknown = false;
|
|
12
|
+
return {
|
|
13
|
+
observe(raw) {
|
|
14
|
+
const event = record(raw);
|
|
15
|
+
const id = event.toolCallId;
|
|
16
|
+
if (typeof id !== "string")
|
|
17
|
+
return;
|
|
18
|
+
if (event.type === "tool_execution_start" && event.toolName === "read")
|
|
19
|
+
pending.set(id, record(event.args ?? event.input));
|
|
20
|
+
if (event.type !== "tool_execution_end" || !pending.has(id) || finished.has(id))
|
|
21
|
+
return;
|
|
22
|
+
const args = pending.get(id);
|
|
23
|
+
pending.delete(id);
|
|
24
|
+
finished.add(id);
|
|
25
|
+
const result = record(event.result);
|
|
26
|
+
if (event.isError === true || result.isError === true || result.ok === false || result.error || record(result.details).ok === false) {
|
|
27
|
+
rejected++;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const content = result.content;
|
|
31
|
+
if (!Array.isArray(content) || content.some(item => record(item).type !== "text" || typeof record(item).text !== "string") || typeof args.path !== "string") {
|
|
32
|
+
unknown = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const text = content.map(item => record(item).text).join("\n");
|
|
36
|
+
const size = Buffer.byteLength(text);
|
|
37
|
+
const key = createHash("sha256").update(JSON.stringify([args.path.replaceAll("\\", "/").replace(/^\.\//, ""), args.offset ?? null, args.limit ?? null, text])).digest("hex");
|
|
38
|
+
bytes += size;
|
|
39
|
+
complete++;
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
duplicates += size;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
},
|
|
44
|
+
snapshot() { return { successfulReadBytes: unknown ? null : bytes, duplicateReadBytes: unknown ? null : duplicates, completedReads: complete, rejectedReads: rejected, incompleteReads: pending.size }; },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function classifyFrontendSessionFailure(result) {
|
|
48
|
+
const errorCode = /\b(?:FRONTEND_[A-Z_]+|TOOL_SCHEMA_INVALID|FACT_IDENTITY_CONFLICT|REQUEST_ID_REUSE_CONFLICT|TERMINAL_CONFLICT|[A-Z_]+_INCOMPLETE|[A-Z_]+_EXHAUSTED)\b/.exec(result.stderr)?.[0] ?? null;
|
|
49
|
+
const category = result.failureCategory;
|
|
50
|
+
const failureLayer = result.stopReason === "length" || /context|budget-exhausted/.test(category) ? "capacity"
|
|
51
|
+
: result.ok ? "none"
|
|
52
|
+
: errorCode?.includes("LEDGER") || errorCode === "FRONTEND_COMMIT_FAILED" ? "persistence"
|
|
53
|
+
: errorCode === "TOOL_SCHEMA_INVALID" ? "schema"
|
|
54
|
+
: /binding|source|permission|write-guard|tool-policy/.test(category) ? "governance"
|
|
55
|
+
: /timeout|rate-limit|provider|transport|network|auth/.test(category) ? "provider"
|
|
56
|
+
: /invalid-output|structured-output/.test(category) ? "protocol-or-coverage"
|
|
57
|
+
: "unknown";
|
|
58
|
+
return { failureLayer, errorCode };
|
|
59
|
+
}
|
|
60
|
+
const fields = ["inputTokens", "outputTokens", "reasoningTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens", "peakRequestContextTokens"];
|
|
61
|
+
const record = (v) => v !== null && typeof v === "object" ? v : {};
|
|
62
|
+
const number = (r, ...keys) => {
|
|
63
|
+
for (const key of keys)
|
|
64
|
+
if (typeof r[key] === "number" && Number.isFinite(r[key]) && r[key] >= 0)
|
|
65
|
+
return r[key];
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
const sumKnown = (values) => values.length && values.every(v => v !== null) ? values.reduce((s, v) => s + v, 0) : null;
|
|
69
|
+
/** Observation only. Never feed this projection into the SDK execution guard. */
|
|
70
|
+
export function collectFrontendUsage(events) {
|
|
71
|
+
const samples = new Map();
|
|
72
|
+
const aliases = new Map();
|
|
73
|
+
let anonymous = false;
|
|
74
|
+
let observed = false;
|
|
75
|
+
for (const raw of events) {
|
|
76
|
+
const event = record(raw);
|
|
77
|
+
const message = record(event.message);
|
|
78
|
+
if (typeof message.role === "string" && message.role !== "assistant")
|
|
79
|
+
continue;
|
|
80
|
+
const usage = record(event.usage ?? message.usage ?? event.tokenUsage);
|
|
81
|
+
if (!Object.keys(usage).length)
|
|
82
|
+
continue;
|
|
83
|
+
observed = true;
|
|
84
|
+
const ids = [event.responseId, event.messageId, message.responseId, message.id].filter((v) => typeof v === "string" && v.length > 0);
|
|
85
|
+
const roots = [...new Set(ids.map(id => aliases.get(id) ?? id))];
|
|
86
|
+
const key = roots[0];
|
|
87
|
+
if (!key) {
|
|
88
|
+
anonymous = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const input = number(usage, "input", "input_tokens", "inputTokens", "prompt_tokens", "promptTokens");
|
|
92
|
+
const output = number(usage, "output", "output_tokens", "outputTokens", "completion_tokens", "completionTokens");
|
|
93
|
+
const cacheRead = number(usage, "cacheRead", "cache_read_input_tokens");
|
|
94
|
+
const cacheWrite = number(usage, "cacheWrite", "cache_creation_input_tokens");
|
|
95
|
+
// Pi's native input excludes cache tokens; OpenAI-compatible input includes them.
|
|
96
|
+
const context = input === null ? null : "input" in usage ? sumKnown([input, cacheRead, cacheWrite]) : input;
|
|
97
|
+
const sample = {
|
|
98
|
+
inputTokens: input, outputTokens: output,
|
|
99
|
+
reasoningTokens: number(usage, "reasoning", "reasoning_tokens", "reasoningTokens") ?? number(record(usage.completion_tokens_details ?? usage.output_tokens_details), "reasoning_tokens"),
|
|
100
|
+
cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite,
|
|
101
|
+
totalTokens: number(usage, "total_tokens", "totalTokens") ?? sumKnown([context, output]),
|
|
102
|
+
peakRequestContextTokens: context,
|
|
103
|
+
};
|
|
104
|
+
for (const root of roots) {
|
|
105
|
+
const previous = samples.get(root);
|
|
106
|
+
if (previous)
|
|
107
|
+
for (const field of fields)
|
|
108
|
+
sample[field] = previous[field] === null ? sample[field] : sample[field] === null ? previous[field] : Math.max(previous[field], sample[field]);
|
|
109
|
+
samples.delete(root);
|
|
110
|
+
}
|
|
111
|
+
for (const [alias, root] of aliases)
|
|
112
|
+
if (roots.includes(root))
|
|
113
|
+
aliases.set(alias, key);
|
|
114
|
+
for (const id of ids)
|
|
115
|
+
aliases.set(id, key);
|
|
116
|
+
samples.set(key, sample);
|
|
117
|
+
}
|
|
118
|
+
const result = Object.fromEntries(fields.map(field => [field, anonymous ? null : field === "peakRequestContextTokens" ? (samples.size && [...samples.values()].every(s => s[field] !== null) ? Math.max(...[...samples.values()].map(s => s[field])) : null) : sumKnown([...samples.values()].map(s => s[field]))]));
|
|
119
|
+
return { ...result, requestCount: anonymous || !observed ? null : samples.size,
|
|
120
|
+
completeness: !observed ? "unknown" : !anonymous && result.inputTokens !== null && result.outputTokens !== null && result.totalTokens !== null ? "complete" : "partial" };
|
|
121
|
+
}
|
|
122
|
+
function aggregateAttempts(attempts) {
|
|
123
|
+
const usages = attempts.map(a => a.actual ?? collectFrontendUsage([]));
|
|
124
|
+
const totals = Object.fromEntries(fields.map(field => [field, field === "peakRequestContextTokens"
|
|
125
|
+
? usages.every(u => u[field] !== null) ? Math.max(...usages.map(u => u[field])) : null
|
|
126
|
+
: sumKnown(usages.map(u => u[field]))]));
|
|
127
|
+
return { ...totals, requestCount: sumKnown(usages.map(u => u.requestCount)), completeness: usages.every(u => u.completeness === "complete") ? "complete" : usages.every(u => u.completeness === "unknown") ? "unknown" : "partial" };
|
|
128
|
+
}
|
|
129
|
+
/** Captures every transport attempt, including an SDK failure before CLI fallback. */
|
|
130
|
+
export async function observeFrontendAttempt(input, execute) {
|
|
131
|
+
const event = { id: randomUUID(), backend: input.backend, model: input.model, startedAt: new Date().toISOString(), completion: "incomplete" };
|
|
132
|
+
const notify = () => { try {
|
|
133
|
+
input.observer?.(structuredClone(event));
|
|
134
|
+
}
|
|
135
|
+
catch { /* observation cannot change execution */ } };
|
|
136
|
+
notify();
|
|
137
|
+
const result = await execute();
|
|
138
|
+
event.completion = "complete";
|
|
139
|
+
event.actual = result.usageObservation ?? collectFrontendUsage([]);
|
|
140
|
+
event.reads = result.readObservation;
|
|
141
|
+
event.capabilities = result.capabilitiesObservation;
|
|
142
|
+
event.requestEnvelope = result.requestEnvelopeObservation;
|
|
143
|
+
event.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: result.durationMs };
|
|
144
|
+
notify();
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
const measure = (text) => ({ sha256: createHash("sha256").update(text).digest("hex"), chars: text.length, bytes: Buffer.byteLength(text), estimatedTokens: Math.ceil(Buffer.byteLength(text) / 3) });
|
|
148
|
+
function safeObservation(read, fallback) { try {
|
|
149
|
+
return read();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return fallback;
|
|
153
|
+
} }
|
|
154
|
+
/** One logical dispatch and its real transport attempts; no raw prompt or tool output is stored. */
|
|
155
|
+
export async function observeFrontendSession(input, execute) {
|
|
156
|
+
const durableBefore = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
157
|
+
const before = safeObservation(() => input.committedCount?.(), undefined);
|
|
158
|
+
const toolsBefore = readFrontendToolProgress(input.customTools);
|
|
159
|
+
const receipt = {
|
|
160
|
+
schemaVersion: 1, policyVersion: "frontend-session-observation-v1",
|
|
161
|
+
identity: { sessionId: randomUUID(), phase: input.phase, scopeIds: input.scopeIds ?? [], taskId: input.taskId ?? null, runId: input.runId ?? null, nodeId: input.nodeId ?? null, attempt: input.attempt ?? null, sourceDigest: input.sourceDigest ?? null, contractDigest: input.contractDigest ?? null },
|
|
162
|
+
startedAt: new Date().toISOString(), finishedAt: null, completion: "incomplete",
|
|
163
|
+
planned: { prompt: measure(input.prompt), userMessage: measure(input.userMessage ?? ""), toolSchemas: input.customTools ? safeObservation(() => measure(JSON.stringify(input.customTools.map(t => { const tool = record(t); return { name: tool.name, description: tool.description, parameters: tool.parameters }; }))), null) : null,
|
|
164
|
+
estimatorVersion: "utf8-bytes-div3-v1-estimate", capabilities: { ...frontendModelCapabilities(undefined), model: input.model ?? null }, unmeasuredComponents: ["sdk-system-overhead", "built-in-tools", "history", "tool-results", "attachments"] },
|
|
165
|
+
actual: collectFrontendUsage([]), attempts: [],
|
|
166
|
+
progress: { memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
|
|
167
|
+
};
|
|
168
|
+
let writes = Promise.resolve();
|
|
169
|
+
let abandoned = false;
|
|
170
|
+
const writeObservation = async (file, snapshot) => {
|
|
171
|
+
// Never recreate a run that the runner has already archived.
|
|
172
|
+
await mkdir(path.dirname(file)).catch(error => { if (error.code !== "EEXIST")
|
|
173
|
+
throw error; });
|
|
174
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
await writeFile(temporary, JSON.stringify(snapshot), { flag: "wx" });
|
|
177
|
+
await rename(temporary, file);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const save = () => {
|
|
184
|
+
const snapshot = structuredClone(receipt);
|
|
185
|
+
writes = writes.then(async () => {
|
|
186
|
+
if (abandoned)
|
|
187
|
+
return;
|
|
188
|
+
if (input.sink)
|
|
189
|
+
await input.sink(snapshot);
|
|
190
|
+
else if (input.artifactPath)
|
|
191
|
+
await writeObservation(input.artifactPath, snapshot);
|
|
192
|
+
}).catch(() => { });
|
|
193
|
+
return writes;
|
|
194
|
+
};
|
|
195
|
+
const waitForWrite = async (pending) => {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
await Promise.race([pending, new Promise(resolve => { timer = setTimeout(() => { abandoned = true; resolve(); }, 100); })]);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (timer)
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
await waitForWrite(save());
|
|
206
|
+
const result = await execute(event => {
|
|
207
|
+
const index = receipt.attempts.findIndex(a => a.id === event.id);
|
|
208
|
+
if (index < 0)
|
|
209
|
+
receipt.attempts.push(event);
|
|
210
|
+
else
|
|
211
|
+
receipt.attempts[index] = event;
|
|
212
|
+
void save();
|
|
213
|
+
});
|
|
214
|
+
receipt.finishedAt = new Date().toISOString();
|
|
215
|
+
receipt.completion = "complete";
|
|
216
|
+
receipt.actual = receipt.attempts.length ? aggregateAttempts(receipt.attempts) : result.usageObservation ?? collectFrontendUsage([]);
|
|
217
|
+
receipt.planned.capabilities = receipt.attempts[0]?.capabilities ?? result.capabilitiesObservation ?? receipt.planned.capabilities;
|
|
218
|
+
receipt.progress.memoryCommittedDelta = before !== undefined && input.committedCount ? safeObservation(() => Math.max(0, input.committedCount() - before), null) : null;
|
|
219
|
+
const durableAfter = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
220
|
+
receipt.progress.durableCommittedDelta = durableBefore !== undefined && durableAfter !== undefined ? Math.max(0, durableAfter - durableBefore) : null;
|
|
221
|
+
const toolsAfter = readFrontendToolProgress(input.customTools);
|
|
222
|
+
if (toolsBefore && toolsAfter)
|
|
223
|
+
for (const key of ["failedRecords", "duplicateRecords", "durableSubmissions", "durableReplacements"])
|
|
224
|
+
receipt.progress[key] = Math.max(0, toolsAfter[key] - toolsBefore[key]);
|
|
225
|
+
const reads = receipt.attempts.length ? receipt.attempts.map(a => a.reads) : [result.readObservation];
|
|
226
|
+
for (const key of ["successfulReadBytes", "duplicateReadBytes"])
|
|
227
|
+
receipt.progress[key] = sumKnown(reads.map(r => r?.[key] ?? null));
|
|
228
|
+
receipt.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: Date.parse(receipt.finishedAt) - Date.parse(receipt.startedAt), ...classifyFrontendSessionFailure(result) };
|
|
229
|
+
await waitForWrite(save());
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
export function summarizeFrontendSessions(receipts) {
|
|
233
|
+
const byId = new Map();
|
|
234
|
+
for (const receipt of receipts) {
|
|
235
|
+
const previous = byId.get(receipt.identity.sessionId);
|
|
236
|
+
const rank = (r) => [r.completion === "complete" ? 1 : 0, Date.parse(r.finishedAt ?? r.startedAt), r.attempts.filter(a => a.completion === "complete").length, r.attempts.length];
|
|
237
|
+
const candidateRank = rank(receipt);
|
|
238
|
+
const previousRank = previous ? rank(previous) : [];
|
|
239
|
+
const difference = candidateRank.map((v, i) => v - (previousRank[i] ?? 0)).find(v => v !== 0) ?? 0;
|
|
240
|
+
if (!previous || difference > 0)
|
|
241
|
+
byId.set(receipt.identity.sessionId, receipt);
|
|
242
|
+
}
|
|
243
|
+
const unique = [...byId.values()];
|
|
244
|
+
return { sessions: unique.length, failedSessions: unique.filter(r => r.terminal?.ok === false).length, incompleteSessions: unique.filter(r => r.completion === "incomplete").length,
|
|
245
|
+
failureLayers: Object.fromEntries([...new Set(unique.flatMap(r => r.terminal && r.terminal.failureLayer !== "none" ? [r.terminal.failureLayer] : []))].sort().map(layer => [layer, unique.filter(r => r.terminal?.failureLayer === layer).length])),
|
|
246
|
+
progress: Object.fromEntries(["durableCommittedDelta", "durableSubmissions", "durableReplacements", "failedRecords", "duplicateRecords", "successfulReadBytes", "duplicateReadBytes"].map(key => [key, sumKnown(unique.map(r => r.progress[key]))])),
|
|
247
|
+
totalTokens: sumKnown(unique.flatMap(r => r.attempts.length ? r.attempts.map(a => a.actual?.totalTokens ?? null) : [r.actual.totalTokens])),
|
|
248
|
+
sessionDurationSumMs: sumKnown(unique.map(r => r.terminal?.durationMs ?? null)) };
|
|
249
|
+
}
|
|
@@ -226,6 +226,7 @@ export function parseTargetSurfaceFacts(text) {
|
|
|
226
226
|
}
|
|
227
227
|
export const PLAN_LEDGER_FACT_KINDS = [
|
|
228
228
|
"target-surface",
|
|
229
|
+
"state-registry",
|
|
229
230
|
"component-choice",
|
|
230
231
|
"state-flow",
|
|
231
232
|
"data-flow",
|
|
@@ -235,6 +236,7 @@ export const PLAN_LEDGER_FACT_KINDS = [
|
|
|
235
236
|
"plan-requirement",
|
|
236
237
|
"plan-verification-target",
|
|
237
238
|
"plan-evidence-gap",
|
|
239
|
+
"mock-endpoint",
|
|
238
240
|
];
|
|
239
241
|
function skeletonTargetFiles(skeleton) {
|
|
240
242
|
if (!isRecord(skeleton))
|
|
@@ -385,8 +387,25 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
385
387
|
patch.interactions = [...interactionByName.values(), ...unnamedInteractions];
|
|
386
388
|
}
|
|
387
389
|
const mockApi = lastByKind("mock-api");
|
|
390
|
+
if (!mockApi && facts.some(f => f.kind === "mock-endpoint" && f.removed !== true))
|
|
391
|
+
throw Error("MOCK_POLICY_MISSING: endpoint has no policy");
|
|
388
392
|
if (mockApi && isRecord(mockApi.mockApi)) {
|
|
389
|
-
|
|
393
|
+
const endpoints = new Map();
|
|
394
|
+
for (const endpoint of Array.isArray(mockApi.mockApi.endpoints) ? mockApi.mockApi.endpoints : []) {
|
|
395
|
+
if (isRecord(endpoint))
|
|
396
|
+
endpoints.set(`${endpoint.method}:${endpoint.path}`, endpoint);
|
|
397
|
+
}
|
|
398
|
+
for (const fact of facts)
|
|
399
|
+
if (fact.kind === "mock-endpoint" && isRecord(fact.endpoint)) {
|
|
400
|
+
const key = `${fact.endpoint.method}:${fact.endpoint.path}`;
|
|
401
|
+
if (fact.removed === true)
|
|
402
|
+
endpoints.delete(key);
|
|
403
|
+
else
|
|
404
|
+
endpoints.set(key, fact.endpoint);
|
|
405
|
+
}
|
|
406
|
+
if (mockApi.mockApi.strategy === "not-needed" && endpoints.size)
|
|
407
|
+
throw Error("MOCK_POLICY_CONFLICT: withdraw endpoints before declaring Mock/API not needed");
|
|
408
|
+
patch.mockApi = { ...mockApi.mockApi, endpoints: [...endpoints.values()] };
|
|
390
409
|
}
|
|
391
410
|
const deviation = lastByKind("design-deviation");
|
|
392
411
|
if (deviation) {
|
|
@@ -432,7 +451,7 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
432
451
|
const inherited = contractRequirements.get(id);
|
|
433
452
|
if (!inherited)
|
|
434
453
|
return entry;
|
|
435
|
-
const merged = { ...entry };
|
|
454
|
+
const merged = { ...entry, ...(inherited.text ? { expectedOutcome: inherited.text } : {}), ...(inherited.execution ? { execution: inherited.execution } : {}) };
|
|
436
455
|
// Inherit the contract-declared provenance only when the plan did
|
|
437
456
|
// not already declare it (model override wins; missing inherits).
|
|
438
457
|
if (!Array.isArray(entry.sourceFragmentIds)) {
|
|
@@ -484,7 +503,22 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
484
503
|
}
|
|
485
504
|
const verificationTargets = verificationTargetOrder
|
|
486
505
|
.map((id) => verificationTargetById.get(id))
|
|
487
|
-
.filter((entry) => Boolean(entry))
|
|
506
|
+
.filter((entry) => Boolean(entry))
|
|
507
|
+
.map((entry) => {
|
|
508
|
+
// Coverage precedes UX. A later state-flow fact explicitly naming
|
|
509
|
+
// this VT establishes the reverse binding without another model call.
|
|
510
|
+
// No path/name heuristics: unbound or ambiguous decisions stay subject
|
|
511
|
+
// to canonical validation. Recompute from live states after removals.
|
|
512
|
+
const boundStates = [...stateByName.values()]
|
|
513
|
+
.filter((state) => state.applicable === true &&
|
|
514
|
+
asStringArray(state.verificationTargetIds).includes(asString(entry.id)))
|
|
515
|
+
.map((state) => asString(state.name));
|
|
516
|
+
if (boundStates.length === 0)
|
|
517
|
+
return entry;
|
|
518
|
+
return { ...entry, uiStates: [...new Set([
|
|
519
|
+
...asStringArray(entry.uiStates), ...boundStates,
|
|
520
|
+
])].sort() };
|
|
521
|
+
});
|
|
488
522
|
if (verificationTargets.length > 0) {
|
|
489
523
|
patch.verificationTargets = verificationTargets;
|
|
490
524
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
2
|
+
import { FRONTEND_MAX_IMPLEMENTATION_FILES, frontendConcreteDeliveryFiles } from "./frontend-capacity.js";
|
|
2
3
|
import { buildFrontendShapeTransitionCapsule, deriveFrontendRuntimeShapeFacts, evaluateFrontendShapeEscalation, parseFrontendShapeTransitionCapsule, } from "./frontend-shape.js";
|
|
3
4
|
import { createTypedEventStore, readCommittedEvents, } from "./frontend-typed-event-store.js";
|
|
4
5
|
import { adoptTypedEventFact, stageTypedEventFact, } from "./frontend-typed-event-transaction.js";
|
|
@@ -222,7 +223,13 @@ function extractRuntimeShapeEvidence(input) {
|
|
|
222
223
|
!sourceText.includes(path) &&
|
|
223
224
|
/(?:\.[A-Za-z0-9]+|\*\*)$/.test(path));
|
|
224
225
|
const routes = canonicalStrings(text.match(/\/(?:[A-Za-z0-9_:[\]-]+\/?)+/g) ?? []);
|
|
225
|
-
|
|
226
|
+
// The frozen delivery inventory is available before Plan. Do not spend
|
|
227
|
+
// coverage/UX sessions building a contract the same capacity gate will reject.
|
|
228
|
+
// allowedPaths and text mentions are permissions/evidence, never this count.
|
|
229
|
+
const skeleton = input.tasksById.get("frontend-plan-pi")?.structuredContractOutput?.skeleton;
|
|
230
|
+
const deliveryFiles = frontendConcreteDeliveryFiles(skeleton?.targets?.files);
|
|
231
|
+
const capacitySplit = deliveryFiles.length > FRONTEND_MAX_IMPLEMENTATION_FILES;
|
|
232
|
+
const splitRequired = capacitySplit || /["']splitRequired["']\s*:\s*true|\bsplit-required\s+shape\b/i.test(text);
|
|
226
233
|
return {
|
|
227
234
|
sourceNodeId: input.sourceNodeId,
|
|
228
235
|
sourceNodeDigest: computeFrontendShapeNodeDigest(node),
|
|
@@ -232,7 +239,10 @@ function extractRuntimeShapeEvidence(input) {
|
|
|
232
239
|
targetPaths,
|
|
233
240
|
routes,
|
|
234
241
|
splitRequired,
|
|
235
|
-
evidenceRefs: [
|
|
242
|
+
evidenceRefs: [
|
|
243
|
+
...(capacitySplit ? ["dag:frontend-plan-pi:structuredContractOutput.skeleton.targets.files"] : []),
|
|
244
|
+
`node:${input.sourceNodeId}:stdout`,
|
|
245
|
+
],
|
|
236
246
|
};
|
|
237
247
|
}
|
|
238
248
|
/** Commit a typed, structured runtime fact immediately after a canonical source
|
|
@@ -46,15 +46,22 @@ const EXECUTABLE_SHAPE_RANK = {
|
|
|
46
46
|
high: 3,
|
|
47
47
|
};
|
|
48
48
|
function pathSpreadScore(paths) {
|
|
49
|
+
// Verification-only roots are routinely authorized together with the
|
|
50
|
+
// implementation root (for example `src/**` + `test/**`). They do not make
|
|
51
|
+
// a UI change cross-domain, so exclude them from the topology spread signal.
|
|
52
|
+
// Keep product packages and source roots intact: `apps/**` + `packages/**`
|
|
53
|
+
// must still count as a real spread and prevent an unsafe small topology.
|
|
54
|
+
const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
|
|
49
55
|
const tops = new Set(paths
|
|
50
56
|
.map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
57
|
+
.filter((root) => !auxiliaryRoot.test(root))
|
|
51
58
|
.filter(Boolean));
|
|
52
59
|
return tops.size;
|
|
53
60
|
}
|
|
54
61
|
function collectEscalationSignals(blob, supervised) {
|
|
55
62
|
const signals = [];
|
|
56
63
|
for (const pattern of SHAPE_ESCALATION_PATTERNS) {
|
|
57
|
-
if (
|
|
64
|
+
if (hasAffirmativeShapeSignal(blob, pattern))
|
|
58
65
|
signals.push(pattern.id);
|
|
59
66
|
}
|
|
60
67
|
if (supervised)
|
|
@@ -65,6 +72,42 @@ function collectEscalationSignals(blob, supervised) {
|
|
|
65
72
|
}
|
|
66
73
|
return [...new Set(signals)];
|
|
67
74
|
}
|
|
75
|
+
function isExplicitShapeExclusion(clause, pattern) {
|
|
76
|
+
const match = new RegExp(pattern.re.source, "i").exec(clause);
|
|
77
|
+
if (!match)
|
|
78
|
+
return false;
|
|
79
|
+
const prefix = clause.slice(0, match.index).trim();
|
|
80
|
+
const suffix = clause.slice(match.index + match[0].length).trim();
|
|
81
|
+
const actionInSignal = ["new-route", "new-dependency"].includes(pattern.id);
|
|
82
|
+
if (suffix && !(actionInSignal && /^no$/i.test(prefix) && /^is\s+(?:required|needed)$/i.test(suffix)))
|
|
83
|
+
return false;
|
|
84
|
+
if (/^(?:不涉及|不(?:修改|变更|调整|改变|新增|添加)|无需(?:修改|变更|调整|改变|新增|添加)|no\s+changes?\s+to|(?:do\s+not|don't)\s+(?:change|modify))$/i.test(prefix))
|
|
85
|
+
return true;
|
|
86
|
+
return actionInSignal && /^(?:不|禁止|无需|无须|不需要|no|without|do\s+not|don't)$/i.test(prefix);
|
|
87
|
+
}
|
|
88
|
+
/** Only a complete, independent scope exclusion can suppress a risk signal.
|
|
89
|
+
* Nested negation, conditions and trailing consequences remain risk evidence;
|
|
90
|
+
* an excluded occurrence never cancels another occurrence elsewhere. */
|
|
91
|
+
function hasAffirmativeShapeSignal(blob, pattern) {
|
|
92
|
+
for (const sentence of blob.split(/[\n。.;;!?!?]/)) {
|
|
93
|
+
if (!pattern.re.test(sentence))
|
|
94
|
+
continue;
|
|
95
|
+
// Commas can attach a condition to an exclusion. A comma-separated list
|
|
96
|
+
// is independent only when every item is itself an explicit exclusion.
|
|
97
|
+
const clauses = sentence.split(/[,,、]/).map(part => part.trim().replace(/^(?:[-*+]|\d+\))\s+/, ""));
|
|
98
|
+
if (clauses.every(clause => SHAPE_ESCALATION_PATTERNS.some(candidate => isExplicitShapeExclusion(clause, candidate))))
|
|
99
|
+
continue;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/** Negation applies to the API mention itself, never to unrelated Mock policy. */
|
|
105
|
+
function hasFrontendRemoteSignal(blob) {
|
|
106
|
+
const remaining = blob
|
|
107
|
+
.replace(/\bno\s+api\b/gi, "")
|
|
108
|
+
.replace(/无\s*(?:api\b|接口)/gi, "");
|
|
109
|
+
return /\b(api|fetch|axios|graphql|endpoint)\b|远程|接口/i.test(remaining);
|
|
110
|
+
}
|
|
68
111
|
function resolveBaselineFrontendTaskShape(input) {
|
|
69
112
|
const blob = [
|
|
70
113
|
input.requirementMarkdown ?? "",
|
|
@@ -84,9 +127,7 @@ function resolveBaselineFrontendTaskShape(input) {
|
|
|
84
127
|
const paths = input.allowedPaths ?? [];
|
|
85
128
|
const spread = pathSpreadScore(paths);
|
|
86
129
|
const concentrated = spread <= 1 && paths.length <= 4;
|
|
87
|
-
const hasApiSignal =
|
|
88
|
-
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
89
|
-
/无\s*api|无\s*接口/i.test(blob));
|
|
130
|
+
const hasApiSignal = hasFrontendRemoteSignal(blob);
|
|
90
131
|
const complexity = (input.complexity ?? "").toLowerCase();
|
|
91
132
|
// split-required wins over everything: it is a termination/escalation
|
|
92
133
|
// signal, not an executable topology.
|
|
@@ -266,9 +307,7 @@ export function deriveFrontendRuntimeShapeFacts(input) {
|
|
|
266
307
|
const supervised = /\bsupervised\b/i.test(blob);
|
|
267
308
|
const escalationSignals = collectEscalationSignals(blob, supervised);
|
|
268
309
|
const paths = input.allowedPaths ?? [];
|
|
269
|
-
const hasApiSignal =
|
|
270
|
-
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
271
|
-
/无\s*api|无\s*接口/i.test(blob));
|
|
310
|
+
const hasApiSignal = hasFrontendRemoteSignal(blob);
|
|
272
311
|
const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
|
|
273
312
|
return {
|
|
274
313
|
splitRequired: input.splitRequired,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/** Observe supported frozen Vitest commands without changing their selection. */
|
|
5
|
+
export async function withFrontendTestReporter(input) {
|
|
6
|
+
// Compound commands/wrappers need their own adapter: never append flags to
|
|
7
|
+
// a different shell command, change selection, or override a user reporter.
|
|
8
|
+
if (/[;&|<>`\n\r$]/.test(input.command) || /--(?:reporter|outputFile)/.test(input.command))
|
|
9
|
+
return undefined;
|
|
10
|
+
const direct = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?vitest\s+run(?:\s|$)/.test(input.command.trim());
|
|
11
|
+
const npm = /^npm\s+(?:run\s+([\w:-]+)|(test))(?=\s|$)(.*)$/.exec(input.command.trim());
|
|
12
|
+
let separator = "";
|
|
13
|
+
if (!direct) {
|
|
14
|
+
if (!npm)
|
|
15
|
+
return undefined;
|
|
16
|
+
let manifest;
|
|
17
|
+
try {
|
|
18
|
+
manifest = JSON.parse(await readFile(path.join(input.cwd, "package.json"), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const script = manifest.scripts?.[npm[1] ?? "test"] ?? "";
|
|
24
|
+
if (!/^vitest\s+run(?:\s|$)/.test(script) || /[;&|<>`\n\r$]|--(?:reporter|outputFile)/.test(script))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (npm[3]?.trim() && !/^--(?:\s|$)/.test(npm[3].trim()))
|
|
27
|
+
return undefined;
|
|
28
|
+
separator = npm[3]?.trim() ? "" : " --";
|
|
29
|
+
}
|
|
30
|
+
const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
31
|
+
return `${input.command}${separator} --reporter=json --outputFile=${quote(input.reportPath)}`;
|
|
32
|
+
}
|
|
33
|
+
const resultSchema = z.object({
|
|
34
|
+
testResults: z.array(z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
assertionResults: z.array(z.object({
|
|
37
|
+
title: z.string(), fullName: z.string().optional(),
|
|
38
|
+
ancestorTitles: z.array(z.string()).optional(), status: z.string(),
|
|
39
|
+
})),
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
/** Resolve runner paths against its cwd, then use the contract workspace coordinates. */
|
|
43
|
+
export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd) {
|
|
44
|
+
return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
45
|
+
file: path.relative(workspaceRoot, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
|
|
46
|
+
title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
47
|
+
status: test.status,
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
@@ -95,6 +95,8 @@ export const CONTRACT_FACT_KINDS = [
|
|
|
95
95
|
"open-question",
|
|
96
96
|
"split-proposal",
|
|
97
97
|
"openspec-selection",
|
|
98
|
+
"ui-state-declaration",
|
|
99
|
+
"required-deliverables",
|
|
98
100
|
"contract-finalized",
|
|
99
101
|
];
|
|
100
102
|
export const CONTRACT_TERMINAL_FACT_KINDS = ["contract-finalized"];
|
|
@@ -124,7 +126,9 @@ export const PLAN_RECORD_FACT_KINDS = [
|
|
|
124
126
|
"plan-requirement",
|
|
125
127
|
"plan-verification-target",
|
|
126
128
|
"plan-evidence-gap",
|
|
129
|
+
"state-registry",
|
|
127
130
|
];
|
|
131
|
+
export const INCREMENTAL_DETAIL_FACT_KINDS = ["mock-endpoint", "review-finding", "design-finding", "contract-scope-completed", "plan-group-coverage", "scout-scope", "review-scope-completed", "design-scope-completed"];
|
|
128
132
|
/**
|
|
129
133
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
130
134
|
* requests. The five-value enum replaces free-form issueCategory strings.
|
|
@@ -156,6 +160,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
|
|
|
156
160
|
...CONTRACT_FACT_KINDS,
|
|
157
161
|
...PLAN_TERMINAL_FACT_KINDS,
|
|
158
162
|
...PLAN_RECORD_FACT_KINDS,
|
|
163
|
+
...INCREMENTAL_DETAIL_FACT_KINDS,
|
|
159
164
|
]),
|
|
160
165
|
];
|
|
161
166
|
export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
|
|
@@ -258,6 +263,15 @@ export const typedEventRecordSchema = z
|
|
|
258
263
|
revision: z.number().int().nonnegative(),
|
|
259
264
|
recordedAt: z.string().min(1),
|
|
260
265
|
quarantineReason: z.string().min(1).optional(),
|
|
266
|
+
submission: z.object({
|
|
267
|
+
version: z.literal(1),
|
|
268
|
+
bindingSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
269
|
+
tool: z.string(),
|
|
270
|
+
callId: z.string(),
|
|
271
|
+
identity: z.string(),
|
|
272
|
+
paramsSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
273
|
+
receipt: z.record(z.unknown()).optional(),
|
|
274
|
+
}).strict().optional(),
|
|
261
275
|
})
|
|
262
276
|
.strict()
|
|
263
277
|
.superRefine((value, ctx) => {
|
|
@@ -152,6 +152,7 @@ export function extractFrontendTestTitles(content) {
|
|
|
152
152
|
const tokens = lexFrontendTestSource(content);
|
|
153
153
|
const titles = [];
|
|
154
154
|
const configurationModifiers = new Set(["each", "runIf", "skipIf"]);
|
|
155
|
+
const nonExecutingModifiers = new Set(["skip", "todo", "fails"]);
|
|
155
156
|
function closingParenIndex(openIndex) {
|
|
156
157
|
let depth = 0;
|
|
157
158
|
for (let cursor = openIndex; cursor < tokens.length; cursor += 1) {
|
|
@@ -177,12 +178,21 @@ export function extractFrontendTestTitles(content) {
|
|
|
177
178
|
}
|
|
178
179
|
let cursor = index + 1;
|
|
179
180
|
let hasConfigurationModifier = false;
|
|
181
|
+
let hasNonExecutingModifier = false;
|
|
182
|
+
let conditionalModifier;
|
|
180
183
|
while (tokens[cursor]?.type === "punctuation" &&
|
|
181
184
|
tokens[cursor]?.value === "." &&
|
|
182
185
|
tokens[cursor + 1]?.type === "identifier") {
|
|
183
186
|
if (tokens[cursor + 1]?.type === "identifier" &&
|
|
184
187
|
configurationModifiers.has(tokens[cursor + 1].value)) {
|
|
185
188
|
hasConfigurationModifier = true;
|
|
189
|
+
if (["runIf", "skipIf"].includes(tokens[cursor + 1].value)) {
|
|
190
|
+
conditionalModifier = tokens[cursor + 1].value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (tokens[cursor + 1]?.type === "identifier" &&
|
|
194
|
+
nonExecutingModifiers.has(tokens[cursor + 1].value)) {
|
|
195
|
+
hasNonExecutingModifier = true;
|
|
186
196
|
}
|
|
187
197
|
cursor += 2;
|
|
188
198
|
}
|
|
@@ -191,6 +201,13 @@ export function extractFrontendTestTitles(content) {
|
|
|
191
201
|
continue;
|
|
192
202
|
}
|
|
193
203
|
if (hasConfigurationModifier) {
|
|
204
|
+
if (conditionalModifier && tokens[cursor + 1]?.type === "identifier" &&
|
|
205
|
+
tokens[cursor + 2]?.value === ")") {
|
|
206
|
+
const condition = tokens[cursor + 1].value;
|
|
207
|
+
if ((conditionalModifier === "runIf" && condition === "false") ||
|
|
208
|
+
(conditionalModifier === "skipIf" && condition === "true"))
|
|
209
|
+
hasNonExecutingModifier = true;
|
|
210
|
+
}
|
|
194
211
|
const close = closingParenIndex(cursor);
|
|
195
212
|
if (close === null ||
|
|
196
213
|
tokens[close + 1]?.type !== "punctuation" ||
|
|
@@ -199,6 +216,11 @@ export function extractFrontendTestTitles(content) {
|
|
|
199
216
|
}
|
|
200
217
|
cursor = close + 1;
|
|
201
218
|
}
|
|
219
|
+
if (hasNonExecutingModifier) {
|
|
220
|
+
// A skipped suite also skips every nested test title in its body.
|
|
221
|
+
index = closingParenIndex(cursor) ?? tokens.length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
202
224
|
if (tokens[cursor + 1]?.type !== "string")
|
|
203
225
|
continue;
|
|
204
226
|
titles.push(tokens[cursor + 1].value);
|
|
@@ -226,7 +248,7 @@ async function evaluateTargetFileTrace(input) {
|
|
|
226
248
|
workspaceRoot: input.workspaceRoot,
|
|
227
249
|
file: input.target.file,
|
|
228
250
|
});
|
|
229
|
-
if (fileIssues.length || input.target.
|
|
251
|
+
if (fileIssues.length || input.target.mode === "static") {
|
|
230
252
|
return { issues: fileIssues };
|
|
231
253
|
}
|
|
232
254
|
const absolute = path.resolve(input.workspaceRoot, input.target.file);
|
|
@@ -261,10 +283,11 @@ export async function evaluateFrontendWriterTracePreflight(input) {
|
|
|
261
283
|
}
|
|
262
284
|
targets.push({
|
|
263
285
|
id: target.id,
|
|
286
|
+
commandId: target.commandId,
|
|
264
287
|
commandLabel: target.commandLabel,
|
|
265
288
|
file: target.file,
|
|
266
289
|
...(target.symbol ? { symbol: target.symbol } : {}),
|
|
267
|
-
...(target.
|
|
290
|
+
...(target.mode !== "static" ? { traceKey: target.id } : {}),
|
|
268
291
|
status: fileTrace.issues.length ? "failed" : "ok",
|
|
269
292
|
matchedNodeIds: [],
|
|
270
293
|
issues: fileTrace.issues,
|
|
@@ -301,15 +324,25 @@ export async function evaluateVerificationTargets(input) {
|
|
|
301
324
|
target,
|
|
302
325
|
});
|
|
303
326
|
issues.push(...fileTrace.issues);
|
|
327
|
+
const executed = input.executedTests?.get(target.commandLabel);
|
|
328
|
+
if (target.mode !== "static" && executed !== undefined) {
|
|
329
|
+
// safePath permits "./" prefixes; reporter files are cwd-relative.
|
|
330
|
+
const targetFile = path.posix.normalize(target.file.replace(/\\/g, "/"));
|
|
331
|
+
const matching = executed.filter((test) => test.file === targetFile && traceKeyMatchesTitle(target.id, test.title));
|
|
332
|
+
if (matching.length === 0 || matching.some((test) => test.status !== "passed")) {
|
|
333
|
+
issues.push(`behavior target was not fully executed and passed: ${target.id} in ${target.file}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
304
336
|
const status = issues.length ? "failed" : "ok";
|
|
305
337
|
if (issues.length)
|
|
306
338
|
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
307
339
|
targets.push({
|
|
308
340
|
id: target.id,
|
|
341
|
+
commandId: target.commandId,
|
|
309
342
|
commandLabel: target.commandLabel,
|
|
310
343
|
file: target.file,
|
|
311
344
|
symbol: target.symbol,
|
|
312
|
-
...(target.
|
|
345
|
+
...(target.mode !== "static" ? { traceKey: target.id } : {}),
|
|
313
346
|
status,
|
|
314
347
|
matchedNodeIds: owners ?? [],
|
|
315
348
|
issues,
|
|
@@ -383,7 +416,7 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
383
416
|
.join("; ")}`);
|
|
384
417
|
}
|
|
385
418
|
const contract = parsed.data;
|
|
386
|
-
const requiresBehaviorVerification = contract.verificationTargets.some((target) => target.
|
|
419
|
+
const requiresBehaviorVerification = contract.verificationTargets.some((target) => target.mode !== "static");
|
|
387
420
|
// Prefer post-repair reverify nodes when present (M3); else initial verify nodes (M2).
|
|
388
421
|
const staticCandidates = [
|
|
389
422
|
"frontend-static-reverify-shell",
|
|
@@ -471,6 +504,7 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
471
504
|
contract,
|
|
472
505
|
labelOwners,
|
|
473
506
|
workspaceRoot: input.workspaceRoot,
|
|
507
|
+
executedTests: input.evidence?.behavior.executedTests,
|
|
474
508
|
});
|
|
475
509
|
if (hardIssues.length) {
|
|
476
510
|
throw new Error(`trace: ${hardIssues.join("; ")}`);
|