@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1645 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1150 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +42 -16
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +347 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { readCommittedEvents, readTypedEventStoreFromJsonl, writeTypedEventStoreJsonl, } from "./frontend-typed-event-store.js";
|
|
2
|
+
import { adoptTypedEventFact, stageTypedEventFact, } from "./frontend-typed-event-transaction.js";
|
|
3
|
+
import { canonicalNodeOutput } from "./node-execution.js";
|
|
4
|
+
import { createDagEventObserver } from "./event-observer.js";
|
|
5
|
+
import { extractFrontendImplementationJson } from "./frontend-implementation-contract.js";
|
|
6
|
+
/**
|
|
7
|
+
* M2 shadow dual write (AC-001 / AC-002).
|
|
8
|
+
*
|
|
9
|
+
* The three read-only frontend nodes keep their authoritative text/JSON output
|
|
10
|
+
* protocols untouched. This module derives schema-valid shadow facts from those
|
|
11
|
+
* outputs and submits them through the M1 typed event store for audit and
|
|
12
|
+
* deterministic difference comparison only. Shadow facts never participate in
|
|
13
|
+
* writer admission and never control a DAG branch (M3 is the authority switch).
|
|
14
|
+
*
|
|
15
|
+
* Failure isolation (AC-001): every parse, stage, and adopt is swallowed, so a
|
|
16
|
+
* shadow-channel failure can never fail the producing node. The canonical event
|
|
17
|
+
* JSONL, when requested, is delegated to `createDagEventObserver`.
|
|
18
|
+
*/
|
|
19
|
+
export const SHADOW_FACT_ORIGINS = ["contract", "scout", "plan"];
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function asString(value) {
|
|
24
|
+
return typeof value === "string" ? value.trim() : "";
|
|
25
|
+
}
|
|
26
|
+
function asStringArray(value) {
|
|
27
|
+
if (!Array.isArray(value))
|
|
28
|
+
return [];
|
|
29
|
+
return value
|
|
30
|
+
.map((item) => asString(item))
|
|
31
|
+
.filter((item) => item.length > 0);
|
|
32
|
+
}
|
|
33
|
+
/** Deterministic, sorted, de-duplicated string set. Lexicographic order only;
|
|
34
|
+
* never locale/Date-dependent so diff output is reproducible. */
|
|
35
|
+
function canonicalStringSet(values) {
|
|
36
|
+
return [
|
|
37
|
+
...new Set(values
|
|
38
|
+
.map((item) => item.trim())
|
|
39
|
+
.filter((item) => item.length > 0)),
|
|
40
|
+
].sort();
|
|
41
|
+
}
|
|
42
|
+
/* ------------------------------------------------------------------ *
|
|
43
|
+
* Contract Markdown parser (authoritative Markdown contract). *
|
|
44
|
+
* ------------------------------------------------------------------ */
|
|
45
|
+
const REQUIREMENT_ID_PATTERN = /(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*/i;
|
|
46
|
+
function classifyContractHeading(heading) {
|
|
47
|
+
const normalized = heading.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
48
|
+
if (normalized.includes("non goal"))
|
|
49
|
+
return "non-goal";
|
|
50
|
+
if (normalized.includes("acceptance"))
|
|
51
|
+
return "acceptance";
|
|
52
|
+
if (normalized.includes("open question"))
|
|
53
|
+
return "open-question";
|
|
54
|
+
if (normalized.includes("risk"))
|
|
55
|
+
return "risk";
|
|
56
|
+
if (normalized.includes("constraint"))
|
|
57
|
+
return "constraint";
|
|
58
|
+
if (normalized.includes("split"))
|
|
59
|
+
return "split";
|
|
60
|
+
if (normalized.includes("scope"))
|
|
61
|
+
return "scope";
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function extractRequirementId(text) {
|
|
65
|
+
const match = REQUIREMENT_ID_PATTERN.exec(text);
|
|
66
|
+
return match ? match[0].toUpperCase() : undefined;
|
|
67
|
+
}
|
|
68
|
+
function extractBulletItems(sectionText) {
|
|
69
|
+
const items = [];
|
|
70
|
+
for (const rawLine of sectionText.split("\n")) {
|
|
71
|
+
const line = rawLine.trim();
|
|
72
|
+
if (!line)
|
|
73
|
+
continue;
|
|
74
|
+
const stripped = line.replace(/^(?:[-*+]|\d+[.)])\s*/, "").trim();
|
|
75
|
+
if (!stripped)
|
|
76
|
+
continue;
|
|
77
|
+
items.push(stripped);
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
}
|
|
81
|
+
function buildContractFact(section, item) {
|
|
82
|
+
switch (section) {
|
|
83
|
+
case "scope":
|
|
84
|
+
case "acceptance":
|
|
85
|
+
return {
|
|
86
|
+
kind: "requirement",
|
|
87
|
+
origin: "contract",
|
|
88
|
+
id: extractRequirementId(item) ?? item,
|
|
89
|
+
text: item,
|
|
90
|
+
};
|
|
91
|
+
case "non-goal":
|
|
92
|
+
return {
|
|
93
|
+
kind: "constraint",
|
|
94
|
+
origin: "contract",
|
|
95
|
+
category: "non-goal",
|
|
96
|
+
text: item,
|
|
97
|
+
};
|
|
98
|
+
case "constraint":
|
|
99
|
+
return {
|
|
100
|
+
kind: "constraint",
|
|
101
|
+
origin: "contract",
|
|
102
|
+
category: "constraint",
|
|
103
|
+
text: item,
|
|
104
|
+
};
|
|
105
|
+
case "risk":
|
|
106
|
+
return {
|
|
107
|
+
kind: "constraint",
|
|
108
|
+
origin: "contract",
|
|
109
|
+
category: "risk",
|
|
110
|
+
text: item,
|
|
111
|
+
};
|
|
112
|
+
case "open-question":
|
|
113
|
+
return { kind: "open-question", origin: "contract", text: item };
|
|
114
|
+
case "split":
|
|
115
|
+
return { kind: "split-proposal", origin: "contract", text: item };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function parseContractFacts(text) {
|
|
119
|
+
const facts = [];
|
|
120
|
+
const lines = text.split("\n");
|
|
121
|
+
let current;
|
|
122
|
+
let buffer = [];
|
|
123
|
+
const flush = () => {
|
|
124
|
+
if (!current)
|
|
125
|
+
return;
|
|
126
|
+
for (const item of extractBulletItems(buffer.join("\n"))) {
|
|
127
|
+
facts.push(buildContractFact(current, item));
|
|
128
|
+
}
|
|
129
|
+
buffer = [];
|
|
130
|
+
};
|
|
131
|
+
for (const rawLine of lines) {
|
|
132
|
+
const line = rawLine.trimEnd();
|
|
133
|
+
const heading = /^#{1,6}\s+(.+?)\s*$/.exec(line.trim());
|
|
134
|
+
if (heading) {
|
|
135
|
+
flush();
|
|
136
|
+
current = classifyContractHeading(heading[1]);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (current)
|
|
140
|
+
buffer.push(line);
|
|
141
|
+
}
|
|
142
|
+
flush();
|
|
143
|
+
return facts;
|
|
144
|
+
}
|
|
145
|
+
/* ------------------------------------------------------------------ *
|
|
146
|
+
* TARGET_SURFACE parser (authoritative scout text). *
|
|
147
|
+
* ------------------------------------------------------------------ */
|
|
148
|
+
const TARGET_SURFACE_LABELS = [
|
|
149
|
+
"entrypoint",
|
|
150
|
+
"routeOrMount",
|
|
151
|
+
"implementationPaths",
|
|
152
|
+
"testPaths",
|
|
153
|
+
"dataSource",
|
|
154
|
+
"allowedPathConflicts",
|
|
155
|
+
];
|
|
156
|
+
function escapeRegex(value) {
|
|
157
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
158
|
+
}
|
|
159
|
+
function splitListValue(raw) {
|
|
160
|
+
const cleaned = raw.replace(/^\[|\]$/g, "").trim();
|
|
161
|
+
if (!cleaned)
|
|
162
|
+
return [];
|
|
163
|
+
return cleaned
|
|
164
|
+
.split(/[,\n]/)
|
|
165
|
+
.map((item) => item
|
|
166
|
+
.trim()
|
|
167
|
+
.replace(/^[-*+]\s+/, "")
|
|
168
|
+
.replace(/^["']|["']$/g, ""))
|
|
169
|
+
.filter((item) => item.length > 0);
|
|
170
|
+
}
|
|
171
|
+
function parseTargetSurfaceLabels(text) {
|
|
172
|
+
const result = {};
|
|
173
|
+
const sectionStart = text.indexOf("TARGET_SURFACE");
|
|
174
|
+
if (sectionStart < 0) {
|
|
175
|
+
for (const label of TARGET_SURFACE_LABELS)
|
|
176
|
+
result[label] = "";
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
const section = text.slice(sectionStart);
|
|
180
|
+
const positions = [];
|
|
181
|
+
for (const label of TARGET_SURFACE_LABELS) {
|
|
182
|
+
const match = new RegExp(`\\b${escapeRegex(label)}\\s*(?::|\\s)`).exec(section);
|
|
183
|
+
if (match && match.index !== undefined) {
|
|
184
|
+
positions.push({ label, index: match.index });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
positions.sort((a, b) => a.index - b.index);
|
|
188
|
+
for (let i = 0; i < positions.length; i += 1) {
|
|
189
|
+
const current = positions[i];
|
|
190
|
+
const colon = section.indexOf(":", current.index);
|
|
191
|
+
const valueStart = colon >= 0 && colon < (positions[i + 1]?.index ?? section.length)
|
|
192
|
+
? colon + 1
|
|
193
|
+
: current.index + current.label.length;
|
|
194
|
+
const valueEnd = i + 1 < positions.length ? positions[i + 1].index : section.length;
|
|
195
|
+
result[current.label] = section.slice(valueStart, valueEnd).trim();
|
|
196
|
+
}
|
|
197
|
+
for (const label of TARGET_SURFACE_LABELS) {
|
|
198
|
+
result[label] ??= "";
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
export function parseTargetSurfaceFacts(text) {
|
|
203
|
+
const values = parseTargetSurfaceLabels(text);
|
|
204
|
+
const implementationPaths = canonicalStringSet(splitListValue(values.implementationPaths));
|
|
205
|
+
const testPaths = canonicalStringSet(splitListValue(values.testPaths));
|
|
206
|
+
const allowedPathConflicts = canonicalStringSet(splitListValue(values.allowedPathConflicts));
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
kind: "target-surface",
|
|
210
|
+
origin: "scout",
|
|
211
|
+
entrypoint: asString(values.entrypoint),
|
|
212
|
+
routeOrMount: asString(values.routeOrMount),
|
|
213
|
+
implementationPaths,
|
|
214
|
+
testPaths,
|
|
215
|
+
dataSource: asString(values.dataSource),
|
|
216
|
+
allowedPathConflicts,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
kind: "design-evidence",
|
|
220
|
+
origin: "scout",
|
|
221
|
+
source: "TARGET_SURFACE",
|
|
222
|
+
paths: canonicalStringSet([...implementationPaths, ...testPaths]),
|
|
223
|
+
conflicts: allowedPathConflicts,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
export const PLAN_LEDGER_FACT_KINDS = [
|
|
228
|
+
"target-surface",
|
|
229
|
+
"component-choice",
|
|
230
|
+
"state-flow",
|
|
231
|
+
"data-flow",
|
|
232
|
+
"mock-api",
|
|
233
|
+
"design-deviation",
|
|
234
|
+
"dependency",
|
|
235
|
+
];
|
|
236
|
+
function skeletonTargetFiles(skeleton) {
|
|
237
|
+
if (!isRecord(skeleton))
|
|
238
|
+
return [];
|
|
239
|
+
const targets = isRecord(skeleton.targets) ? skeleton.targets : undefined;
|
|
240
|
+
return canonicalStringSet(asStringArray(targets?.files));
|
|
241
|
+
}
|
|
242
|
+
/** Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
243
|
+
* Production compile authority is the tool payload (`patch` on any committed
|
|
244
|
+
* plan fact, else a `target-surface.patch`). Text JSON is never the compile
|
|
245
|
+
* authority. */
|
|
246
|
+
export function restorePlanPatchFromCommittedFacts(records) {
|
|
247
|
+
const planFacts = records
|
|
248
|
+
.filter((record) => record.phase === "committed")
|
|
249
|
+
.map((record) => record.fact)
|
|
250
|
+
.filter((fact) => isRecord(fact) &&
|
|
251
|
+
fact.origin === "plan" &&
|
|
252
|
+
typeof fact.kind === "string" &&
|
|
253
|
+
PLAN_LEDGER_FACT_KINDS.includes(fact.kind));
|
|
254
|
+
if (planFacts.length === 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
for (const fact of planFacts) {
|
|
257
|
+
if (isRecord(fact.patch))
|
|
258
|
+
return fact.patch;
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A+B (AC-004): reverse of `planLedgerFactsFromPatch` for the incremental
|
|
264
|
+
* `record_*` tool flow. Reconstructs the canonical editable patch from the raw
|
|
265
|
+
* fields each `record_*` tool committed. `finalize_plan` calls this and then
|
|
266
|
+
* re-publishes the assembled patch onto a `target-surface` fact so the
|
|
267
|
+
* writeSet-external compile authority (`frontend-implementation-contract.ts`)
|
|
268
|
+
* can still restore a plain `patch` object.
|
|
269
|
+
*/
|
|
270
|
+
export function assemblePlanPatchFromCommittedFacts(records) {
|
|
271
|
+
const facts = records
|
|
272
|
+
.filter((record) => record.phase === "committed")
|
|
273
|
+
.map((record) => record.fact)
|
|
274
|
+
.filter((fact) => isRecord(fact) &&
|
|
275
|
+
fact.origin === "plan" &&
|
|
276
|
+
typeof fact.kind === "string" &&
|
|
277
|
+
PLAN_LEDGER_FACT_KINDS.includes(fact.kind));
|
|
278
|
+
if (facts.length === 0)
|
|
279
|
+
return undefined;
|
|
280
|
+
for (const fact of facts) {
|
|
281
|
+
if (isRecord(fact.patch))
|
|
282
|
+
return fact.patch;
|
|
283
|
+
}
|
|
284
|
+
const patch = {};
|
|
285
|
+
const targetSurface = facts.find((fact) => fact.kind === "target-surface");
|
|
286
|
+
if (targetSurface) {
|
|
287
|
+
const routes = canonicalStringSet(asStringArray(targetSurface.routes));
|
|
288
|
+
if (routes.length > 0)
|
|
289
|
+
patch.targets = { routes };
|
|
290
|
+
}
|
|
291
|
+
const componentChoice = facts.find((fact) => fact.kind === "component-choice");
|
|
292
|
+
if (componentChoice) {
|
|
293
|
+
if (Array.isArray(componentChoice.uiComponentChoices)) {
|
|
294
|
+
patch.uiComponentChoices = componentChoice.uiComponentChoices.filter(isRecord);
|
|
295
|
+
}
|
|
296
|
+
const stylingStrategy = asString(componentChoice.stylingStrategy);
|
|
297
|
+
if (stylingStrategy)
|
|
298
|
+
patch.stylingStrategy = stylingStrategy;
|
|
299
|
+
}
|
|
300
|
+
const stateFlow = facts.find((fact) => fact.kind === "state-flow");
|
|
301
|
+
if (stateFlow) {
|
|
302
|
+
if (Array.isArray(stateFlow.uiStates)) {
|
|
303
|
+
patch.uiStates = stateFlow.uiStates.filter(isRecord);
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(stateFlow.interactions)) {
|
|
306
|
+
patch.interactions = stateFlow.interactions.filter(isRecord);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const mockApi = facts.find((fact) => fact.kind === "mock-api");
|
|
310
|
+
if (mockApi && isRecord(mockApi.mockApi)) {
|
|
311
|
+
patch.mockApi = mockApi.mockApi;
|
|
312
|
+
}
|
|
313
|
+
const deviation = facts.find((fact) => fact.kind === "design-deviation");
|
|
314
|
+
if (deviation) {
|
|
315
|
+
const conflicts = canonicalStringSet(asStringArray(deviation.conflicts));
|
|
316
|
+
if (conflicts.length > 0)
|
|
317
|
+
patch.designEvidence = { conflicts };
|
|
318
|
+
}
|
|
319
|
+
const dependency = facts.find((fact) => fact.kind === "dependency");
|
|
320
|
+
const dependencyPolicy = asString(dependency?.policy);
|
|
321
|
+
if (dependencyPolicy)
|
|
322
|
+
patch.dependencyPolicy = dependencyPolicy;
|
|
323
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
324
|
+
}
|
|
325
|
+
/** Derive the 7 origin=plan ledger facts from an editable patch. Skeleton
|
|
326
|
+
* `targets.files` fills `target-surface.files` because the patch must not
|
|
327
|
+
* write that protected path. */
|
|
328
|
+
export function planLedgerFactsFromPatch(patch, skeleton) {
|
|
329
|
+
const record = isRecord(patch) ? patch : undefined;
|
|
330
|
+
if (!record) {
|
|
331
|
+
throw new Error("frontend plan ledger requires an editable RFC 7386 patch object");
|
|
332
|
+
}
|
|
333
|
+
const facts = [];
|
|
334
|
+
const targets = isRecord(record.targets) ? record.targets : undefined;
|
|
335
|
+
const files = canonicalStringSet([
|
|
336
|
+
...asStringArray(targets?.files),
|
|
337
|
+
...skeletonTargetFiles(skeleton),
|
|
338
|
+
]);
|
|
339
|
+
const routes = canonicalStringSet(asStringArray(targets?.routes));
|
|
340
|
+
facts.push({
|
|
341
|
+
kind: "target-surface",
|
|
342
|
+
origin: "plan",
|
|
343
|
+
files,
|
|
344
|
+
routes,
|
|
345
|
+
patch: record,
|
|
346
|
+
});
|
|
347
|
+
const uiComponentChoices = Array.isArray(record.uiComponentChoices)
|
|
348
|
+
? record.uiComponentChoices.filter(isRecord)
|
|
349
|
+
: [];
|
|
350
|
+
const components = canonicalStringSet(uiComponentChoices.map((choice) => asString(choice.component)));
|
|
351
|
+
const stylingStrategy = asString(record.stylingStrategy);
|
|
352
|
+
if (components.length > 0 || stylingStrategy) {
|
|
353
|
+
facts.push({
|
|
354
|
+
kind: "component-choice",
|
|
355
|
+
origin: "plan",
|
|
356
|
+
components,
|
|
357
|
+
...(stylingStrategy ? { stylingStrategy } : {}),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const uiStates = Array.isArray(record.uiStates)
|
|
361
|
+
? record.uiStates.filter(isRecord)
|
|
362
|
+
: [];
|
|
363
|
+
const interactions = Array.isArray(record.interactions)
|
|
364
|
+
? record.interactions.filter(isRecord)
|
|
365
|
+
: [];
|
|
366
|
+
const stateNames = canonicalStringSet(uiStates.map((state) => asString(state.name)));
|
|
367
|
+
const interactionNames = canonicalStringSet(interactions.map((interaction) => asString(interaction.name)));
|
|
368
|
+
if (stateNames.length > 0 || interactionNames.length > 0) {
|
|
369
|
+
facts.push({
|
|
370
|
+
kind: "state-flow",
|
|
371
|
+
origin: "plan",
|
|
372
|
+
states: stateNames,
|
|
373
|
+
interactions: interactionNames,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const mockApi = isRecord(record.mockApi) ? record.mockApi : undefined;
|
|
377
|
+
const endpoints = Array.isArray(mockApi?.endpoints)
|
|
378
|
+
? mockApi.endpoints.filter(isRecord)
|
|
379
|
+
: [];
|
|
380
|
+
const endpointPaths = canonicalStringSet(endpoints.map((endpoint) => `${asString(endpoint.method)} ${asString(endpoint.path)}`.trim()));
|
|
381
|
+
if (interactionNames.length > 0 || endpointPaths.length > 0) {
|
|
382
|
+
facts.push({
|
|
383
|
+
kind: "data-flow",
|
|
384
|
+
origin: "plan",
|
|
385
|
+
interactions: interactionNames,
|
|
386
|
+
endpoints: endpointPaths,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (mockApi) {
|
|
390
|
+
facts.push({
|
|
391
|
+
kind: "mock-api",
|
|
392
|
+
origin: "plan",
|
|
393
|
+
strategy: asString(mockApi.strategy),
|
|
394
|
+
endpoints: endpointPaths,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const designEvidence = isRecord(record.designEvidence)
|
|
398
|
+
? record.designEvidence
|
|
399
|
+
: undefined;
|
|
400
|
+
const conflicts = canonicalStringSet(asStringArray(designEvidence?.conflicts));
|
|
401
|
+
if (conflicts.length > 0) {
|
|
402
|
+
facts.push({ kind: "design-deviation", origin: "plan", conflicts });
|
|
403
|
+
}
|
|
404
|
+
const dependencyPolicy = asString(record.dependencyPolicy);
|
|
405
|
+
if (dependencyPolicy) {
|
|
406
|
+
facts.push({ kind: "dependency", origin: "plan", policy: dependencyPolicy });
|
|
407
|
+
}
|
|
408
|
+
return facts;
|
|
409
|
+
}
|
|
410
|
+
/** Map committed plan facts, else a test-helper JSON patch, into the 7
|
|
411
|
+
* origin=plan kinds. Production compile authority is the committed ledger;
|
|
412
|
+
* a missing ledger/patch throws instead of returning a fake empty book. */
|
|
413
|
+
export function parsePlanLedgerFacts(textOrRecords, skeleton) {
|
|
414
|
+
if (typeof textOrRecords !== "string") {
|
|
415
|
+
const fromFacts = planLedgerFactsFromCommittedRecords(textOrRecords);
|
|
416
|
+
if (fromFacts.length > 0)
|
|
417
|
+
return fromFacts;
|
|
418
|
+
throw new Error("frontend plan ledger missing: no committed origin=plan facts");
|
|
419
|
+
}
|
|
420
|
+
const text = textOrRecords;
|
|
421
|
+
const jsonlLines = text
|
|
422
|
+
.split("\n")
|
|
423
|
+
.map((line) => line.trim())
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
const looksLikeJsonlLedger = jsonlLines.length > 0 &&
|
|
426
|
+
jsonlLines.every((line) => {
|
|
427
|
+
try {
|
|
428
|
+
const record = JSON.parse(line);
|
|
429
|
+
return (isRecord(record) &&
|
|
430
|
+
record.phase === "committed" &&
|
|
431
|
+
isRecord(record.fact) &&
|
|
432
|
+
typeof record.eventId === "string");
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (looksLikeJsonlLedger) {
|
|
439
|
+
const records = jsonlLines.map((line) => JSON.parse(line));
|
|
440
|
+
const fromFacts = planLedgerFactsFromCommittedRecords(records);
|
|
441
|
+
if (fromFacts.length > 0)
|
|
442
|
+
return fromFacts;
|
|
443
|
+
}
|
|
444
|
+
let value;
|
|
445
|
+
try {
|
|
446
|
+
value = extractFrontendImplementationJson(text);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
throw new Error("frontend plan ledger missing: no committed plan facts");
|
|
450
|
+
}
|
|
451
|
+
return planLedgerFactsFromPatch(value, skeleton);
|
|
452
|
+
}
|
|
453
|
+
function planLedgerFactsFromCommittedRecords(records) {
|
|
454
|
+
const restored = restorePlanPatchFromCommittedFacts(records);
|
|
455
|
+
if (restored)
|
|
456
|
+
return planLedgerFactsFromPatch(restored);
|
|
457
|
+
return records
|
|
458
|
+
.filter((record) => record.phase === "committed")
|
|
459
|
+
.map((record) => record.fact)
|
|
460
|
+
.filter((fact) => isRecord(fact) &&
|
|
461
|
+
fact.origin === "plan" &&
|
|
462
|
+
typeof fact.kind === "string" &&
|
|
463
|
+
PLAN_LEDGER_FACT_KINDS.includes(fact.kind));
|
|
464
|
+
}
|
|
465
|
+
export async function loadCommittedPlanLedgerFacts(runDir, nodeId = "frontend-plan-pi") {
|
|
466
|
+
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
467
|
+
const pathMod = await import("node:path");
|
|
468
|
+
return readTypedEventStoreFromJsonl(pathMod.join(runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
469
|
+
}
|
|
470
|
+
/** A+B (AC-001/003): read only the committed-phase origin facts from a node's
|
|
471
|
+
* typed event store JSONL. Staged/quarantined records never leak downstream. */
|
|
472
|
+
export async function readCommittedOriginFacts(runDir, nodeId, fileName) {
|
|
473
|
+
const pathMod = await import("node:path");
|
|
474
|
+
const records = await readTypedEventStoreFromJsonl(pathMod.join(runDir, nodeId, fileName));
|
|
475
|
+
return records.filter((record) => record.phase === "committed");
|
|
476
|
+
}
|
|
477
|
+
/** A+B (AC-001/003): fail-closed committed contract/scout fact prerequisites.
|
|
478
|
+
* `contract` requires a committed `contract-finalized` terminal fact; `scout`
|
|
479
|
+
* requires a committed `target-surface` fact. Missing facts block downstream
|
|
480
|
+
* consumption — never fall back to text parsing. */
|
|
481
|
+
export function checkCommittedOriginFacts(input) {
|
|
482
|
+
const kinds = new Set(input.records
|
|
483
|
+
.filter((record) => record.phase === "committed")
|
|
484
|
+
.map((record) => record.fact.kind)
|
|
485
|
+
.filter((kind) => typeof kind === "string"));
|
|
486
|
+
if (input.origin === "contract") {
|
|
487
|
+
return kinds.has("contract-finalized")
|
|
488
|
+
? { ok: true }
|
|
489
|
+
: {
|
|
490
|
+
ok: false,
|
|
491
|
+
reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (input.origin === "scout") {
|
|
495
|
+
return kinds.has("target-surface")
|
|
496
|
+
? { ok: true }
|
|
497
|
+
: {
|
|
498
|
+
ok: false,
|
|
499
|
+
reason: "frontend scout facts missing: no committed target-surface fact",
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return { ok: true };
|
|
503
|
+
}
|
|
504
|
+
/* ------------------------------------------------------------------ *
|
|
505
|
+
* Shadow observer factory. *
|
|
506
|
+
* ------------------------------------------------------------------ */
|
|
507
|
+
const SHADOW_NODE_PARSERS = {
|
|
508
|
+
"frontend-contract-pi": parseContractFacts,
|
|
509
|
+
"frontend-scout-pi": parseTargetSurfaceFacts,
|
|
510
|
+
"frontend-plan-pi": parsePlanLedgerFacts,
|
|
511
|
+
};
|
|
512
|
+
function isShadowNodeId(nodeId) {
|
|
513
|
+
return nodeId in SHADOW_NODE_PARSERS;
|
|
514
|
+
}
|
|
515
|
+
function reportError(context, error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
process.stderr.write(`[frontend-shadow-dual-write] ${context} failed: ${message}\n`);
|
|
518
|
+
}
|
|
519
|
+
export function createShadowDualWriteObserver(options) {
|
|
520
|
+
const base = options.eventsJsonlPath
|
|
521
|
+
? createDagEventObserver({
|
|
522
|
+
eventsJsonlPath: options.eventsJsonlPath,
|
|
523
|
+
dagRunId: options.dagRunId,
|
|
524
|
+
spec: options.spec,
|
|
525
|
+
})
|
|
526
|
+
: null;
|
|
527
|
+
const recordNodeFacts = async (nodeId, state) => {
|
|
528
|
+
if (!isShadowNodeId(nodeId))
|
|
529
|
+
return;
|
|
530
|
+
const text = canonicalNodeOutput(state.nodes[nodeId]);
|
|
531
|
+
let facts;
|
|
532
|
+
try {
|
|
533
|
+
facts = options.parseNodeFacts
|
|
534
|
+
? options.parseNodeFacts(nodeId, text)
|
|
535
|
+
: SHADOW_NODE_PARSERS[nodeId](text);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
for (const [index, fact] of facts.entries()) {
|
|
541
|
+
try {
|
|
542
|
+
// Deterministic requestId keeps replay idempotent within a node.
|
|
543
|
+
const requestId = `${options.requestId}:${nodeId}:${fact.kind}:${index}`;
|
|
544
|
+
const staged = stageTypedEventFact({
|
|
545
|
+
store: options.store,
|
|
546
|
+
requestId,
|
|
547
|
+
attemptId: options.attemptId,
|
|
548
|
+
fact,
|
|
549
|
+
});
|
|
550
|
+
await adoptTypedEventFact({
|
|
551
|
+
store: options.store,
|
|
552
|
+
requestId,
|
|
553
|
+
attemptId: options.attemptId,
|
|
554
|
+
fact,
|
|
555
|
+
eventId: staged.eventId,
|
|
556
|
+
expectedRevision: options.store.revision,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// A shadow commit failure never blocks the producing node (AC-001).
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const observer = {
|
|
565
|
+
onRunStart: async (state) => {
|
|
566
|
+
await base?.observer.onRunStart?.(state);
|
|
567
|
+
},
|
|
568
|
+
onNodeStart: async (nodeId, state) => {
|
|
569
|
+
await base?.observer.onNodeStart?.(nodeId, state);
|
|
570
|
+
},
|
|
571
|
+
onNodeOutput: async (nodeId, chunk, state) => {
|
|
572
|
+
await base?.observer.onNodeOutput?.(nodeId, chunk, state);
|
|
573
|
+
},
|
|
574
|
+
onNodeFinish: async (nodeId, state) => {
|
|
575
|
+
try {
|
|
576
|
+
await recordNodeFacts(nodeId, state);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// Derived view; never affect canonical execution.
|
|
580
|
+
}
|
|
581
|
+
await base?.observer.onNodeFinish?.(nodeId, state);
|
|
582
|
+
},
|
|
583
|
+
onRunFinish: async (state) => {
|
|
584
|
+
await base?.observer.onRunFinish?.(state);
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
return {
|
|
588
|
+
observer,
|
|
589
|
+
flush: async () => {
|
|
590
|
+
try {
|
|
591
|
+
const committed = readCommittedEvents(options.store, options.attemptId);
|
|
592
|
+
await writeTypedEventStoreJsonl(options.shadowStoreJsonlPath, committed);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
reportError("flush", error);
|
|
596
|
+
}
|
|
597
|
+
await base?.flush();
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function compareDimension(factValues, authorityValues) {
|
|
602
|
+
const facts = canonicalStringSet(factValues);
|
|
603
|
+
const authority = canonicalStringSet(authorityValues);
|
|
604
|
+
const matched = facts.filter((item) => authority.includes(item));
|
|
605
|
+
const factsOnly = facts.filter((item) => !authority.includes(item));
|
|
606
|
+
const authorityOnly = authority.filter((item) => !facts.includes(item));
|
|
607
|
+
return { facts, authority, matched, factsOnly, authorityOnly };
|
|
608
|
+
}
|
|
609
|
+
function factFieldStrings(facts, kinds, fields) {
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const fact of facts) {
|
|
612
|
+
if (!kinds.includes(fact.kind))
|
|
613
|
+
continue;
|
|
614
|
+
for (const field of fields) {
|
|
615
|
+
const value = fact[field];
|
|
616
|
+
if (typeof value === "string")
|
|
617
|
+
out.push(value);
|
|
618
|
+
else if (Array.isArray(value))
|
|
619
|
+
out.push(...asStringArray(value));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
export function compareShadowFactsToAuthority(input) {
|
|
625
|
+
const contractFacts = parseContractFacts(input.contractText);
|
|
626
|
+
const scoutFacts = parseTargetSurfaceFacts(input.scoutText);
|
|
627
|
+
const planFacts = parsePlanLedgerFacts(input.planText);
|
|
628
|
+
const requirementCoverage = compareDimension(factFieldStrings(input.facts, ["requirement"], ["id"]), factFieldStrings(contractFacts, ["requirement"], ["id"]));
|
|
629
|
+
const targetPaths = compareDimension([
|
|
630
|
+
...factFieldStrings(input.facts, ["target-surface"], ["implementationPaths", "testPaths", "files", "routes"]),
|
|
631
|
+
], [
|
|
632
|
+
...factFieldStrings(scoutFacts, ["target-surface"], ["implementationPaths", "testPaths"]),
|
|
633
|
+
...factFieldStrings(planFacts, ["target-surface"], ["files", "routes"]),
|
|
634
|
+
]);
|
|
635
|
+
const componentDecisions = compareDimension(factFieldStrings(input.facts, ["component-choice", "state-flow", "data-flow", "mock-api"], ["components", "stylingStrategy", "states", "interactions", "endpoints", "strategy"]), factFieldStrings(planFacts, ["component-choice", "state-flow", "data-flow", "mock-api"], ["components", "stylingStrategy", "states", "interactions", "endpoints", "strategy"]));
|
|
636
|
+
const blockers = compareDimension(factFieldStrings(input.facts, ["constraint", "open-question", "design-deviation"], ["text", "conflicts"]), [
|
|
637
|
+
...factFieldStrings(contractFacts, ["constraint", "open-question"], ["text"]),
|
|
638
|
+
...factFieldStrings(planFacts, ["design-deviation"], ["conflicts"]),
|
|
639
|
+
...factFieldStrings(scoutFacts, ["design-evidence"], ["conflicts"]),
|
|
640
|
+
]);
|
|
641
|
+
return { requirementCoverage, targetPaths, componentDecisions, blockers };
|
|
642
|
+
}
|
|
643
|
+
export function renderShadowDiff(result) {
|
|
644
|
+
const lines = ["# Shadow Dual Write Diff", ""];
|
|
645
|
+
const sections = [
|
|
646
|
+
["Requirement Coverage", result.requirementCoverage],
|
|
647
|
+
["Target Paths", result.targetPaths],
|
|
648
|
+
["Component Decisions", result.componentDecisions],
|
|
649
|
+
["Blockers", result.blockers],
|
|
650
|
+
];
|
|
651
|
+
for (const [title, dimension] of sections) {
|
|
652
|
+
lines.push(`## ${title}`);
|
|
653
|
+
lines.push(`- matched: ${dimension.matched.length}`);
|
|
654
|
+
lines.push(`- factsOnly: ${dimension.factsOnly.length}`);
|
|
655
|
+
lines.push(`- authorityOnly: ${dimension.authorityOnly.length}`);
|
|
656
|
+
for (const item of dimension.matched)
|
|
657
|
+
lines.push(` - [=] ${item}`);
|
|
658
|
+
for (const item of dimension.factsOnly)
|
|
659
|
+
lines.push(` - [+] ${item}`);
|
|
660
|
+
for (const item of dimension.authorityOnly)
|
|
661
|
+
lines.push(` - [-] ${item}`);
|
|
662
|
+
lines.push("");
|
|
663
|
+
}
|
|
664
|
+
return lines.join("\n");
|
|
665
|
+
}
|