chainlesschain 0.165.10 → 0.166.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/completions/_cc +2 -1
- package/completions/cc.bash +2 -2
- package/completions/cc.fish +2 -2
- package/completions/cc.ps1 +2 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DoxapYEM.js → AIOps-DwdhBUBc.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DyiHvT2O.js → ActionButton-Dd1lBEeo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CAX06POB.js → Analytics-DuGuuI36.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-JXxcyn0R.js → AppLayout-CvHpSs3M.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-40nZDb9N.js → Artifacts-BU9F3wJh.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B_wc8Kcq.js → Audit-DK_n_WFm.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CReWOiQY.js → BackgroundAgents-qKsxF5tv.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-sXrv9fE6.js → Backup-JS58pSyV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CHWmIERI.js → BaseInput-gGl6Ray1.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DgQrMZCb.js → Chat-B2FZNq3U.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Clpgj0fi.js → ChatBubbleRenderer-CZS_bLAr.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DmRwzt3e.js → Checkbox-tsI0TE22.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-tllAfrrc.js → Codegen-D5kqhfAr.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CA_wqwEp.js → Col-DdN1TrqS.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DQ_szKAv.js → Community-DgbEeh6V.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CjkqX60k.js → Compact-FIm-fRB_.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CfDNkDex.js → Compliance-B-N0pudf.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-PINc6CPv.js → Cowork-CuMafBpi.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-snYq4WCj.js → Cron-BTwzNFHR.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CKR7he4B.js → Crosschain-DRoYbLOd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Bp5xJSbE.js → DID-CPhnNkz_.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Dn-y6txl.js → Dashboard-DJQftix2.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D0uEeunr.js → Dropdown-PSAH1Kfy.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DIhjyeNw.js → EmailListRenderer-PVRqeMiZ.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D2Pv1Hnb.js → FamilyGuardDashboard-CYtnjqjl.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D6oSp4P4.js → Federation-_-53kTDz.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-s6bzlfQs.js → FormItemContext-DTQ4qGuE.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B_NTgGP9.js → GenericCardRenderer-ON_NfX8k.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dk1lCoBr.js → Git-DUc3w_Bf.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-xOnLBQzd.js → Governance-CktsJ2ku.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BFVHy-0I.js → Inference-4wUHQvju.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BXzITB9_.js → KnowledgeGraph-Dq4saPnZ.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BD2M4NPu.js → Logs-Btb50m5s.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Cre8P8HG.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-awpllI_9.js → Marketplace-D3rSncxx.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-tL0IgbIX.js → McpTools-Bc6Mori_.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CrptLGSN.js → Memory-C-ottUdN.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DZX-l_BS.js → MobileBridge-C0DITBmP.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-O6cr0Nal.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-dQ5-FjpQ.js → Mtc-B_MXKXvY.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-U0nDxDlG.js → MtcAudit-BDkUqWIT.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-D1PpAtiq.js → Multisig-BaxNWm8c.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-phZr6nyZ.js → NLProgramming-UD2GndZc.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BPzSSUrX.js → Notes-pU7wtMwB.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-AypFEc7p.js → NotificationSettings-BIs6ULr3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-S3k1qjT-.js → OrderTableRenderer-0NuumnfG.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BvdQXFPR.js → Organization-C9o-n2lq.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CUz_jbQC.js → Overflow-CEvMXxCS.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-C0xQ7H_2.js → P2P-C3gQMTK3.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DJKMjfB8.js → PdhVaultBrowser-D1LVpBOr.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-dkMtmDwK.js → Permissions-Dj5nIi0Y.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CyHE7fnn.js → PersonalDataHub-uiLYuFcu.js} +6 -6
- package/src/assets/web-panel/assets/{Pipeline-_dU0Nwy3.js → Pipeline-CIrmF89x.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-fqLyg57f.js → Privacy-B3K1CUBa.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-COybCr5d.js → ProjectInit-C_wCZ0zW.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B4DngMZW.js → ProjectSettings-iCitfuNJ.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-D0bMwUBm.js → Projects-BwimeXkW.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Ba-7WNA8.js → Providers-BLpepgr6.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-X-I8dDMT.js → QrScannerModal-DPy8tjXV.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BCUP0a90.js → QuickAsk-CRq8VhID.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DtFNmiQZ.js → Recommend-CirlWIiK.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-INR5CIAI.js → RemoteSession-nlnJlKXO.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-B6NfaCc6.js → Reputation-B5aaxee5.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dmz76txh.js → Row-lzsTQZlq.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B7qyFCea.js → RssFeed-C41AzjgC.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Bi6eS0oY.js → Search-1Hxx3TZ3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-D9_xim1L.js → Security-8VqwRxAp.js} +4 -4
- package/src/assets/web-panel/assets/{Services-PI1O-uYU.js → Services-DCm0HHAD.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B8BrJnk8.js → Skeleton-D6w5gpmW.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DvSm92dJ.js → Skills-Bw_pUTJB.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BWyhCoRo.js → Sla-BkMj8Dxu.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BL9H27jl.js → SpeechSettings-BOmLqW44.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B7r--6yN.js → SyncSettings-2xKSqFDO.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-sc64p2Jt.js → Tasks-BPAnzVau.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DEK55RVb.js → Templates-CcOGWgZz.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DElr8R1l.js → Tenant-CAyzzQWN.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BywaLBgz.js → Terminal-Daagerkm.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-B5NcgLyS.js → TimelineRenderer-1jGbMj7I.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-COFYqr6A.js → Tokens-CYQ-FQju.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CfnVS3ij.js → Trigger-C70zRwuR.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bz94Gu5k.js → Trust-CeqXfiX_.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Be10drSk.js → UkeySign-D0j6m1bU.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CUB_cMem.js → VideoEditing-DFHpM7Yd.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DxBJJuvG.js → Wallet-BVC2J0Yx.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BZx5lwT8.js → WebAuthn-BceIN2Gn.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B4sJikPT.js → WorkflowEditor-Bgeb-i-S.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Wl-95AgM.js → chat-BmMBqgvz.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DUSwYVCg.js → colors-sjRzwnyw.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DkKRDHcU.js → compact-item-DNt6m2bM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-B5nAVC_Y.js → createContext-DELaLit3.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Ct8o3bu_.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-rtotsRlA.js → hasIn-dfwk9O8x.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_KDhTdt.js → index-1w7dha_a.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7kGJlSA.js → index-6bjBUhoH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CN2LNCWW.js → index-B0wkOqE9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAtnIdx_.js → index-B4Z4HZcf.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7zQADRc.js → index-B7qiZc2Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoBh4Ezg.js → index-BLbvXsxf.js} +1 -1
- package/src/assets/web-panel/assets/{index-mwPG3TJQ.js → index-BMnvNNEd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ba8raWtd.js → index-BSB9Gin8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJWpTdCW.js → index-BXJ68ObJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DCCTgytM.js → index-Bs-mWOWt.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7Nx-jzS.js → index-BvIQ9sZa.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUS7KK-i.js → index-BzddvOOb.js} +1 -1
- package/src/assets/web-panel/assets/{index-X36Wyd1S.js → index-C2Sxf7yC.js} +1 -1
- package/src/assets/web-panel/assets/{index-btxpIsXs.js → index-CAIn5cS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CskW4gDB.js → index-CLSqyAcE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUDraKtq.js → index-Cbu3-zFd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cv38Cwx0.js → index-Clrqtycd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DupOVIEb.js → index-CpVt6M_8.js} +1 -1
- package/src/assets/web-panel/assets/{index-yt994jkW.js → index-CtfYPnsY.js} +1 -1
- package/src/assets/web-panel/assets/{index-JWQxvZ4x.js → index-CujMHspH.js} +1 -1
- package/src/assets/web-panel/assets/index-D685kO6L.js +1 -0
- package/src/assets/web-panel/assets/{index-Bj6mJ7tw.js → index-DC6FlHlP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQhlLyT6.js → index-DFOYbzg-.js} +1 -1
- package/src/assets/web-panel/assets/index-DTlIRMqX.js +1 -0
- package/src/assets/web-panel/assets/{index-BMHNKa11.js → index-DV8q_Hki.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3lcOYkZ.js → index-DX3wwu3G.js} +3 -3
- package/src/assets/web-panel/assets/{index-CUqLWyY0.js → index-DY2G1Th2.js} +1 -1
- package/src/assets/web-panel/assets/{index-cKc70R9h.js → index-DlYL8VP8.js} +1 -1
- package/src/assets/web-panel/assets/{index-1J_Q-Ovw.js → index-DnQOc5wo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDWNrtvg.js → index-Du62m6pG.js} +1 -1
- package/src/assets/web-panel/assets/{index-03w73yRM.js → index-LMfsEdLk.js} +1 -1
- package/src/assets/web-panel/assets/{index-YANfaEyE.js → index-OC3qYV4-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dnbdqjxz.js → index-Rt-fym5b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfNeBiyO.js → index-WpEDoyTv.js} +1 -1
- package/src/assets/web-panel/assets/{index-INU8TpRu.js → index-aj9ObF2R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAdTY3kv.js → index-dAF9qRVa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxAMRZR0.js → index-dpDeFZNt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbqotoPt.js → index-fNwu9CU_.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ch5SqZaK.js → index-vCQ3l6DX.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Ck8wTHsR.js → initDefaultProps-DUt9Xj8y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-teBWI8ni.js → motion-DdiFPnv7.js} +1 -1
- package/src/assets/web-panel/assets/{move-BdxzRStQ.js → move-BJ2VH7Y2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DMpcDDsk.js → mtc-parser-BCnX6oOJ.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BwkyHyWr.js → omit-CyHxlDLV.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-SlaxZUQd.js → pickAttrs-BJiGW3bY.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BlEZZ3cs.js → placementArrow-DhJ9Rhx8.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cl5uK6S5.js → responsiveObserve-C6zpL9bC.js} +1 -1
- package/src/assets/web-panel/assets/{slide-LlOJWbeE.js → slide-DCbMFRF-.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DxbwIpQR.js → statusUtils-CNtVLsIq.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-G_P9gils.js → styleChecker-Z078OtUc.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BDS7KDuK.js → useFlexGapSupport-w4RVmcGs.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Xx3R_4Qk.js → useFs-D4Xw1XRt.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-ChOtRWax.js → usePersonalDataHub-Bnbd1PgY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CAUrMwRC.js → vnode-DSOQxMFN.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Choqv7b5.js → zoom-DNPE1Wca.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/command-manifest.json +1 -1
- package/src/commands/agent.js +1 -1
- package/src/commands/graph.js +100 -0
- package/src/commands/orchestrate.js +85 -18
- package/src/commands/serve.js +39 -0
- package/src/commands/team.js +2 -0
- package/src/data/changelog.json +27 -1
- package/src/generated/cc-agent-protocol.schema.json +2013 -0
- package/src/lib/app-server/bounded-queue.js +87 -0
- package/src/lib/app-server/cli-agent-kernel-adapter.js +309 -0
- package/src/lib/app-server/protocol.js +125 -0
- package/src/lib/app-server/rollout-store.js +603 -0
- package/src/lib/app-server/server.js +922 -0
- package/src/lib/app-server/sqlite-rollout-store.js +315 -0
- package/src/lib/app-server/stdio-transport.js +118 -0
- package/src/lib/codex-app-server-adapter.js +249 -0
- package/src/lib/graph-kernel/adapters.js +238 -0
- package/src/lib/graph-kernel/compiler.js +1443 -0
- package/src/lib/graph-kernel/eval.js +207 -0
- package/src/lib/graph-kernel/event-store.js +101 -0
- package/src/lib/graph-kernel/index.js +7 -0
- package/src/lib/graph-kernel/runtime.js +3788 -0
- package/src/lib/graph-kernel/trace-reducer.js +409 -0
- package/src/lib/graph-kernel/trigger-adapter.js +183 -0
- package/src/lib/orchestrator.js +2 -1
- package/src/lib/record-replay/index.js +1 -0
- package/src/lib/record-replay/skill-recorder.js +386 -0
- package/src/lib/webhook-security.js +159 -0
- package/src/repl/agent-repl.js +5 -4
- package/src/repl/prompt-interactions.js +11 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CDSwkj7Y.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-krNyADYi.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DFO12EfA.js +0 -1
- package/src/assets/web-panel/assets/index-DaDlUWea.js +0 -1
- package/src/assets/web-panel/assets/index-xn89TjWJ.js +0 -1
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { validate } from "../json-schema-validate.js";
|
|
4
|
+
|
|
5
|
+
const graphProtocolSchema = JSON.parse(
|
|
6
|
+
readFileSync(
|
|
7
|
+
new URL("../../generated/cc-agent-protocol.schema.json", import.meta.url),
|
|
8
|
+
"utf8",
|
|
9
|
+
),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const GRAPH_DEFINITION_VERSION = 1;
|
|
13
|
+
export const GRAPH_DEFINITION_MIN_VERSION = Math.max(
|
|
14
|
+
0,
|
|
15
|
+
GRAPH_DEFINITION_VERSION - 1,
|
|
16
|
+
);
|
|
17
|
+
export const GRAPH_COMPILE_ERROR = "CC_GRAPH_COMPILE_FAILED";
|
|
18
|
+
|
|
19
|
+
const compiledGraphs = new WeakSet();
|
|
20
|
+
const REFERENCE_PATTERNS = Object.freeze([
|
|
21
|
+
/\$\{node\.(.+?)\.output\.([A-Za-z0-9][A-Za-z0-9._:/-]*)[^}]*\}/gu,
|
|
22
|
+
/\$\{step\.([^.}]+)\.([A-Za-z0-9][A-Za-z0-9._:/-]*)[^}]*\}/gu,
|
|
23
|
+
]);
|
|
24
|
+
const BUDGET_FIELDS = Object.freeze([
|
|
25
|
+
"turns",
|
|
26
|
+
"tokens",
|
|
27
|
+
"costUsd",
|
|
28
|
+
"wallMs",
|
|
29
|
+
"spawnCount",
|
|
30
|
+
]);
|
|
31
|
+
const GRAPH_DEFINITION_SCHEMA = Object.freeze({
|
|
32
|
+
...graphProtocolSchema.$defs.GraphDefinition,
|
|
33
|
+
$defs: graphProtocolSchema.$defs,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function stableValue(value, state = { seen: new WeakSet(), nodes: 0 }) {
|
|
37
|
+
state.nodes += 1;
|
|
38
|
+
if (state.nodes > 250_000) {
|
|
39
|
+
throw new TypeError("graph definition exceeds canonicalization limits");
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
return value.map((item) => stableValue(item, state));
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
45
|
+
throw new TypeError("graph definition contains a non-finite number");
|
|
46
|
+
}
|
|
47
|
+
if (["bigint", "function", "symbol", "undefined"].includes(typeof value)) {
|
|
48
|
+
throw new TypeError("graph definition contains a non-JSON value");
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (state.seen.has(value)) {
|
|
53
|
+
throw new TypeError("graph definition contains a cycle or repeated object");
|
|
54
|
+
}
|
|
55
|
+
state.seen.add(value);
|
|
56
|
+
const prototype = Object.getPrototypeOf(value);
|
|
57
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
58
|
+
throw new TypeError("graph definition must contain plain JSON objects");
|
|
59
|
+
}
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.keys(value)
|
|
62
|
+
.filter((key) => value[key] !== undefined)
|
|
63
|
+
.sort()
|
|
64
|
+
.map((key) => [key, stableValue(value[key], state)]),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function deepFreeze(value) {
|
|
69
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value))
|
|
70
|
+
return value;
|
|
71
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
72
|
+
return Object.freeze(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function canonicalGraphJson(value) {
|
|
76
|
+
return JSON.stringify(stableValue(value));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function graphDigest(value, domain = "cc.graph.definition/v1") {
|
|
80
|
+
return `sha256:${createHash("sha256")
|
|
81
|
+
.update(domain)
|
|
82
|
+
.update("\0")
|
|
83
|
+
.update(canonicalGraphJson(value))
|
|
84
|
+
.digest("hex")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class GraphCompileError extends Error {
|
|
88
|
+
constructor(diagnostics) {
|
|
89
|
+
const sorted = [...diagnostics].sort(
|
|
90
|
+
(left, right) =>
|
|
91
|
+
left.path.localeCompare(right.path) ||
|
|
92
|
+
left.code.localeCompare(right.code),
|
|
93
|
+
);
|
|
94
|
+
super(
|
|
95
|
+
`Graph compilation failed: ${sorted
|
|
96
|
+
.slice(0, 8)
|
|
97
|
+
.map((item) => `${item.code} at ${item.path}`)
|
|
98
|
+
.join("; ")}`,
|
|
99
|
+
);
|
|
100
|
+
this.name = "GraphCompileError";
|
|
101
|
+
this.code = GRAPH_COMPILE_ERROR;
|
|
102
|
+
this.diagnostics = Object.freeze(sorted.map(Object.freeze));
|
|
103
|
+
this.effectStarted = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function diagnostic(diagnostics, code, path, message, details = {}) {
|
|
108
|
+
diagnostics.push({ code, path, message, ...details });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function duplicateValues(values) {
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const duplicates = new Set();
|
|
114
|
+
for (const value of values) {
|
|
115
|
+
if (seen.has(value)) duplicates.add(value);
|
|
116
|
+
seen.add(value);
|
|
117
|
+
}
|
|
118
|
+
return [...duplicates].sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function outgoingMap(nodeIds, dependencies) {
|
|
122
|
+
const output = new Map([...nodeIds].map((id) => [id, new Set()]));
|
|
123
|
+
for (const [nodeId, parents] of dependencies) {
|
|
124
|
+
for (const parent of parents) output.get(parent)?.add(nodeId);
|
|
125
|
+
}
|
|
126
|
+
return output;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function topologicalSort(nodeIds, dependencies, diagnostics) {
|
|
130
|
+
const indegree = new Map(
|
|
131
|
+
[...nodeIds].map((id) => [id, dependencies.get(id)?.size || 0]),
|
|
132
|
+
);
|
|
133
|
+
const outgoing = outgoingMap(nodeIds, dependencies);
|
|
134
|
+
const ready = [...nodeIds].filter((id) => indegree.get(id) === 0).sort();
|
|
135
|
+
const order = [];
|
|
136
|
+
while (ready.length) {
|
|
137
|
+
const id = ready.shift();
|
|
138
|
+
order.push(id);
|
|
139
|
+
for (const child of [...(outgoing.get(id) || [])].sort()) {
|
|
140
|
+
indegree.set(child, indegree.get(child) - 1);
|
|
141
|
+
if (indegree.get(child) === 0) {
|
|
142
|
+
ready.push(child);
|
|
143
|
+
ready.sort();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (order.length !== nodeIds.size) {
|
|
148
|
+
const cycleNodes = [...nodeIds].filter((id) => !order.includes(id)).sort();
|
|
149
|
+
diagnostic(
|
|
150
|
+
diagnostics,
|
|
151
|
+
"GRAPH_DEPENDENCY_CYCLE",
|
|
152
|
+
"#/nodes",
|
|
153
|
+
`task dependency graph contains a cycle: ${cycleNodes.join(", ")}`,
|
|
154
|
+
{ nodeIds: cycleNodes },
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return { order, outgoing };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function transitiveSets(order, dependencies, outgoing) {
|
|
161
|
+
const ancestors = new Map(
|
|
162
|
+
[...dependencies.keys()].map((id) => [id, new Set()]),
|
|
163
|
+
);
|
|
164
|
+
for (const id of order) {
|
|
165
|
+
const set = ancestors.get(id);
|
|
166
|
+
for (const parent of dependencies.get(id) || []) {
|
|
167
|
+
set.add(parent);
|
|
168
|
+
for (const ancestor of ancestors.get(parent) || []) set.add(ancestor);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const descendants = new Map(
|
|
172
|
+
[...dependencies.keys()].map((id) => [id, new Set()]),
|
|
173
|
+
);
|
|
174
|
+
for (const id of [...order].reverse()) {
|
|
175
|
+
const set = descendants.get(id);
|
|
176
|
+
for (const child of outgoing.get(id) || []) {
|
|
177
|
+
set.add(child);
|
|
178
|
+
for (const descendant of descendants.get(child) || [])
|
|
179
|
+
set.add(descendant);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { ancestors, descendants };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function portMap(node, field, diagnostics, index) {
|
|
186
|
+
const ports = new Map();
|
|
187
|
+
const values = node[field] || [];
|
|
188
|
+
for (const [portIndex, port] of values.entries()) {
|
|
189
|
+
if (ports.has(port.name)) {
|
|
190
|
+
diagnostic(
|
|
191
|
+
diagnostics,
|
|
192
|
+
"GRAPH_DUPLICATE_PORT",
|
|
193
|
+
`#/nodes/${index}/${field}/${portIndex}/name`,
|
|
194
|
+
`duplicate ${field} port: ${port.name}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
ports.set(port.name, port);
|
|
198
|
+
}
|
|
199
|
+
return ports;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function schemaTypeSet(schema) {
|
|
203
|
+
if (!schema || Object.keys(schema).length === 0) return null;
|
|
204
|
+
const type = schema.type;
|
|
205
|
+
if (Array.isArray(type)) return new Set(type);
|
|
206
|
+
if (typeof type === "string") return new Set([type]);
|
|
207
|
+
if (schema.const !== undefined) {
|
|
208
|
+
if (schema.const === null) return new Set(["null"]);
|
|
209
|
+
if (Array.isArray(schema.const)) return new Set(["array"]);
|
|
210
|
+
return new Set([typeof schema.const]);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isPortSchemaAssignable(producer, consumer) {
|
|
216
|
+
if (!consumer || Object.keys(consumer).length === 0) return true;
|
|
217
|
+
if (canonicalGraphJson(producer) === canonicalGraphJson(consumer))
|
|
218
|
+
return true;
|
|
219
|
+
const sourceTypes = schemaTypeSet(producer);
|
|
220
|
+
const targetTypes = schemaTypeSet(consumer);
|
|
221
|
+
if (
|
|
222
|
+
sourceTypes &&
|
|
223
|
+
targetTypes &&
|
|
224
|
+
[...sourceTypes].some((type) => !targetTypes.has(type))
|
|
225
|
+
) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(producer?.enum) && Array.isArray(consumer?.enum)) {
|
|
229
|
+
const allowed = new Set(consumer.enum.map((item) => JSON.stringify(item)));
|
|
230
|
+
if (producer.enum.some((item) => !allowed.has(JSON.stringify(item))))
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
producer?.type === "object" &&
|
|
235
|
+
consumer?.type === "object" &&
|
|
236
|
+
consumer.properties
|
|
237
|
+
) {
|
|
238
|
+
const sourceProperties = producer.properties || {};
|
|
239
|
+
for (const required of consumer.required || []) {
|
|
240
|
+
if (!Object.hasOwn(sourceProperties, required)) return false;
|
|
241
|
+
}
|
|
242
|
+
for (const [name, target] of Object.entries(consumer.properties)) {
|
|
243
|
+
if (
|
|
244
|
+
Object.hasOwn(sourceProperties, name) &&
|
|
245
|
+
!isPortSchemaAssignable(sourceProperties[name], target)
|
|
246
|
+
) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (producer?.type === "array" && consumer?.type === "array") {
|
|
252
|
+
return isPortSchemaAssignable(producer.items || {}, consumer.items || {});
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeScope(value) {
|
|
258
|
+
return String(value || "")
|
|
259
|
+
.replaceAll("\\", "/")
|
|
260
|
+
.replace(/^\.\//u, "")
|
|
261
|
+
.replace(/\/{2,}/gu, "/")
|
|
262
|
+
.replace(/\/$/u, "");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function staticScopePrefix(value) {
|
|
266
|
+
const scope = normalizeScope(value);
|
|
267
|
+
const wildcard = scope.search(/[?*[\]{}]/u);
|
|
268
|
+
return (wildcard < 0 ? scope : scope.slice(0, wildcard)).replace(/\/$/u, "");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function writeScopesOverlap(left, right) {
|
|
272
|
+
const a = staticScopePrefix(left);
|
|
273
|
+
const b = staticScopePrefix(right);
|
|
274
|
+
if (!a || !b) return true;
|
|
275
|
+
return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function loopMultipliers(
|
|
279
|
+
definition,
|
|
280
|
+
diagnostics,
|
|
281
|
+
nodeIds,
|
|
282
|
+
nodes,
|
|
283
|
+
dependencies,
|
|
284
|
+
compensationByNode,
|
|
285
|
+
) {
|
|
286
|
+
const multiplier = new Map([...nodeIds].map((id) => [id, 1]));
|
|
287
|
+
const membership = new Map();
|
|
288
|
+
for (const [index, loop] of definition.loops.entries()) {
|
|
289
|
+
for (const endpoint of [loop.entryNodeId, loop.exitNodeId]) {
|
|
290
|
+
if (!nodeIds.has(endpoint)) {
|
|
291
|
+
diagnostic(
|
|
292
|
+
diagnostics,
|
|
293
|
+
"GRAPH_UNKNOWN_LOOP_ENDPOINT",
|
|
294
|
+
`#/loops/${index}`,
|
|
295
|
+
`loop endpoint does not exist: ${endpoint}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const nodeId of loop.nodeIds) {
|
|
300
|
+
if (!nodeIds.has(nodeId)) {
|
|
301
|
+
diagnostic(
|
|
302
|
+
diagnostics,
|
|
303
|
+
"GRAPH_UNKNOWN_LOOP_NODE",
|
|
304
|
+
`#/loops/${index}/nodeIds`,
|
|
305
|
+
`loop node does not exist: ${nodeId}`,
|
|
306
|
+
);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const owners = membership.get(nodeId) || [];
|
|
310
|
+
owners.push(loop.id);
|
|
311
|
+
membership.set(nodeId, owners);
|
|
312
|
+
multiplier.set(nodeId, multiplier.get(nodeId) * loop.maxIterations);
|
|
313
|
+
const node = nodes.get(nodeId);
|
|
314
|
+
if (
|
|
315
|
+
node &&
|
|
316
|
+
!["none", "read"].includes(node.effectClass || "none") &&
|
|
317
|
+
!compensationByNode.has(nodeId)
|
|
318
|
+
) {
|
|
319
|
+
diagnostic(
|
|
320
|
+
diagnostics,
|
|
321
|
+
"GRAPH_LOOP_EFFECT_COMPENSATION_REQUIRED",
|
|
322
|
+
`#/loops/${index}/nodeIds`,
|
|
323
|
+
`effectful loop node requires an isolated compensation handler: ${nodeId}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (node?.kind === "subgraph") {
|
|
327
|
+
diagnostic(
|
|
328
|
+
diagnostics,
|
|
329
|
+
"GRAPH_LOOP_SUBGRAPH_UNSUPPORTED",
|
|
330
|
+
`#/loops/${index}/nodeIds`,
|
|
331
|
+
`subgraph calls cannot be nested inside a loop region: ${nodeId}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
!loop.nodeIds.includes(loop.entryNodeId) ||
|
|
337
|
+
!loop.nodeIds.includes(loop.exitNodeId)
|
|
338
|
+
) {
|
|
339
|
+
diagnostic(
|
|
340
|
+
diagnostics,
|
|
341
|
+
"GRAPH_LOOP_BOUNDARY_OUTSIDE_REGION",
|
|
342
|
+
`#/loops/${index}`,
|
|
343
|
+
"loop entry and exit must be members of the region",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const regionNodeIds = new Set(loop.nodeIds);
|
|
347
|
+
const regionOutgoing = new Map(
|
|
348
|
+
loop.nodeIds.map((nodeId) => [nodeId, new Set()]),
|
|
349
|
+
);
|
|
350
|
+
for (const nodeId of loop.nodeIds) {
|
|
351
|
+
for (const dependency of dependencies.get(nodeId) || []) {
|
|
352
|
+
if (regionNodeIds.has(dependency)) {
|
|
353
|
+
regionOutgoing.get(dependency).add(nodeId);
|
|
354
|
+
} else if (nodeId !== loop.entryNodeId) {
|
|
355
|
+
diagnostic(
|
|
356
|
+
diagnostics,
|
|
357
|
+
"GRAPH_LOOP_ENTRY_LEAK",
|
|
358
|
+
`#/loops/${index}/entryNodeId`,
|
|
359
|
+
`external dependencies may only enter a loop through its entry node: ${nodeId} depends on ${dependency}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const canReach = (source, target) => {
|
|
365
|
+
const pending = [source];
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
while (pending.length) {
|
|
368
|
+
const current = pending.pop();
|
|
369
|
+
if (current === target) return true;
|
|
370
|
+
if (seen.has(current)) continue;
|
|
371
|
+
seen.add(current);
|
|
372
|
+
pending.push(...(regionOutgoing.get(current) || []));
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
};
|
|
376
|
+
for (const nodeId of loop.nodeIds) {
|
|
377
|
+
if (!canReach(loop.entryNodeId, nodeId)) {
|
|
378
|
+
diagnostic(
|
|
379
|
+
diagnostics,
|
|
380
|
+
"GRAPH_LOOP_NODE_UNREACHABLE_FROM_ENTRY",
|
|
381
|
+
`#/loops/${index}/nodeIds`,
|
|
382
|
+
`loop node is unreachable from its entry: ${nodeId}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (!canReach(nodeId, loop.exitNodeId)) {
|
|
386
|
+
diagnostic(
|
|
387
|
+
diagnostics,
|
|
388
|
+
"GRAPH_LOOP_NODE_CANNOT_REACH_EXIT",
|
|
389
|
+
`#/loops/${index}/nodeIds`,
|
|
390
|
+
`loop node cannot reach its exit: ${nodeId}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const node of definition.nodes) {
|
|
395
|
+
if (regionNodeIds.has(node.id)) continue;
|
|
396
|
+
for (const dependency of dependencies.get(node.id) || []) {
|
|
397
|
+
if (regionNodeIds.has(dependency) && dependency !== loop.exitNodeId) {
|
|
398
|
+
diagnostic(
|
|
399
|
+
diagnostics,
|
|
400
|
+
"GRAPH_LOOP_EXIT_LEAK",
|
|
401
|
+
`#/loops/${index}/exitNodeId`,
|
|
402
|
+
`nodes outside a loop may only depend on its exit node: ${node.id} depends on ${dependency}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const [nodeId, owners] of membership) {
|
|
409
|
+
if (owners.length > 1) {
|
|
410
|
+
diagnostic(
|
|
411
|
+
diagnostics,
|
|
412
|
+
"GRAPH_OVERLAPPING_LOOP_REGION",
|
|
413
|
+
"#/loops",
|
|
414
|
+
`node belongs to overlapping loop regions: ${nodeId}`,
|
|
415
|
+
{ nodeId, loopIds: owners },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return multiplier;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function budgetUpperBound(
|
|
423
|
+
definition,
|
|
424
|
+
multiplier,
|
|
425
|
+
diagnostics,
|
|
426
|
+
subgraphCalls = new Map(),
|
|
427
|
+
) {
|
|
428
|
+
const total = Object.fromEntries(BUDGET_FIELDS.map((field) => [field, 0]));
|
|
429
|
+
for (const node of definition.nodes) {
|
|
430
|
+
const subgraphBudget = subgraphCalls.get(node.id)?.budget;
|
|
431
|
+
for (const field of BUDGET_FIELDS) {
|
|
432
|
+
total[field] +=
|
|
433
|
+
Number(subgraphBudget?.[field] ?? node.budget?.[field] ?? 0) *
|
|
434
|
+
multiplier.get(node.id);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const field of BUDGET_FIELDS) {
|
|
438
|
+
const cap = definition.budget?.[field];
|
|
439
|
+
if (cap != null && total[field] > cap) {
|
|
440
|
+
diagnostic(
|
|
441
|
+
diagnostics,
|
|
442
|
+
"GRAPH_BUDGET_EXCEEDED",
|
|
443
|
+
`#/budget/${field}`,
|
|
444
|
+
`worst-case ${field} budget ${total[field]} exceeds cap ${cap}`,
|
|
445
|
+
{ field, required: total[field], cap },
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return total;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function subgraphTarget(registry, definitionId) {
|
|
453
|
+
return registry instanceof Map
|
|
454
|
+
? registry.get(definitionId)
|
|
455
|
+
: registry[definitionId];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function requiredPortNames(ports) {
|
|
459
|
+
return (ports || [])
|
|
460
|
+
.filter((port) => port.required !== false)
|
|
461
|
+
.map((port) => port.name);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function validateSubgraphPortBindings(
|
|
465
|
+
call,
|
|
466
|
+
callNode,
|
|
467
|
+
targetDefinition,
|
|
468
|
+
callIndex,
|
|
469
|
+
diagnostics,
|
|
470
|
+
) {
|
|
471
|
+
const parentInputs = new Map(
|
|
472
|
+
(callNode?.inputs || []).map((port) => [port.name, port]),
|
|
473
|
+
);
|
|
474
|
+
const parentOutputs = new Map(
|
|
475
|
+
(callNode?.outputs || []).map((port) => [port.name, port]),
|
|
476
|
+
);
|
|
477
|
+
const childNodes = new Map(
|
|
478
|
+
(targetDefinition?.nodes || []).map((node) => [node.id, node]),
|
|
479
|
+
);
|
|
480
|
+
const mappedInputs = new Set();
|
|
481
|
+
const mappedChildInputs = new Set();
|
|
482
|
+
const mappedOutputs = new Set();
|
|
483
|
+
|
|
484
|
+
const validateBindings = (direction) => {
|
|
485
|
+
const bindings = call[`${direction}Bindings`] || [];
|
|
486
|
+
for (const [bindingIndex, binding] of bindings.entries()) {
|
|
487
|
+
const path = `#/subgraphCalls/${callIndex}/${direction}Bindings/${bindingIndex}`;
|
|
488
|
+
const parentPorts = direction === "input" ? parentInputs : parentOutputs;
|
|
489
|
+
const parentPort = parentPorts.get(binding.parentPort);
|
|
490
|
+
const childNode = childNodes.get(binding.childNodeId);
|
|
491
|
+
const childPorts = new Map(
|
|
492
|
+
(childNode?.[direction === "input" ? "inputs" : "outputs"] || []).map(
|
|
493
|
+
(port) => [port.name, port],
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
const childPort = childPorts.get(binding.childPort);
|
|
497
|
+
const parentSet = direction === "input" ? mappedInputs : mappedOutputs;
|
|
498
|
+
if (parentSet.has(binding.parentPort)) {
|
|
499
|
+
diagnostic(
|
|
500
|
+
diagnostics,
|
|
501
|
+
"GRAPH_SUBGRAPH_DUPLICATE_PARENT_PORT",
|
|
502
|
+
`${path}/parentPort`,
|
|
503
|
+
`subgraph ${direction} port is mapped more than once: ${binding.parentPort}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
parentSet.add(binding.parentPort);
|
|
507
|
+
if (!parentPort) {
|
|
508
|
+
diagnostic(
|
|
509
|
+
diagnostics,
|
|
510
|
+
"GRAPH_SUBGRAPH_UNKNOWN_PARENT_PORT",
|
|
511
|
+
`${path}/parentPort`,
|
|
512
|
+
`unknown parent ${direction} port: ${binding.parentPort}`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (!childNode) {
|
|
516
|
+
diagnostic(
|
|
517
|
+
diagnostics,
|
|
518
|
+
"GRAPH_SUBGRAPH_UNKNOWN_CHILD_NODE",
|
|
519
|
+
`${path}/childNodeId`,
|
|
520
|
+
`unknown child node: ${binding.childNodeId}`,
|
|
521
|
+
);
|
|
522
|
+
} else if (!childPort) {
|
|
523
|
+
diagnostic(
|
|
524
|
+
diagnostics,
|
|
525
|
+
"GRAPH_SUBGRAPH_UNKNOWN_CHILD_PORT",
|
|
526
|
+
`${path}/childPort`,
|
|
527
|
+
`unknown child ${direction} port: ${binding.childNodeId}.${binding.childPort}`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (direction === "input") {
|
|
531
|
+
const childKey = `${binding.childNodeId}\0${binding.childPort}`;
|
|
532
|
+
if (mappedChildInputs.has(childKey)) {
|
|
533
|
+
diagnostic(
|
|
534
|
+
diagnostics,
|
|
535
|
+
"GRAPH_SUBGRAPH_DUPLICATE_CHILD_INPUT",
|
|
536
|
+
path,
|
|
537
|
+
`child input is mapped more than once: ${binding.childNodeId}.${binding.childPort}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
mappedChildInputs.add(childKey);
|
|
541
|
+
}
|
|
542
|
+
const producer = direction === "input" ? parentPort : childPort;
|
|
543
|
+
const consumer = direction === "input" ? childPort : parentPort;
|
|
544
|
+
if (
|
|
545
|
+
producer &&
|
|
546
|
+
consumer &&
|
|
547
|
+
!isPortSchemaAssignable(producer.schema, consumer.schema)
|
|
548
|
+
) {
|
|
549
|
+
diagnostic(
|
|
550
|
+
diagnostics,
|
|
551
|
+
"GRAPH_SUBGRAPH_PORT_TYPE_MISMATCH",
|
|
552
|
+
path,
|
|
553
|
+
direction === "input"
|
|
554
|
+
? `${callNode.id}.${binding.parentPort} is not assignable to ${binding.childNodeId}.${binding.childPort}`
|
|
555
|
+
: `${binding.childNodeId}.${binding.childPort} is not assignable to ${callNode.id}.${binding.parentPort}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
validateBindings("input");
|
|
562
|
+
validateBindings("output");
|
|
563
|
+
|
|
564
|
+
for (const portName of requiredPortNames(callNode?.inputs)) {
|
|
565
|
+
if (!mappedInputs.has(portName)) {
|
|
566
|
+
diagnostic(
|
|
567
|
+
diagnostics,
|
|
568
|
+
"GRAPH_SUBGRAPH_REQUIRED_INPUT_UNMAPPED",
|
|
569
|
+
`#/subgraphCalls/${callIndex}/inputBindings`,
|
|
570
|
+
`required parent input is not mapped into the child graph: ${portName}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
for (const portName of requiredPortNames(callNode?.outputs)) {
|
|
575
|
+
if (!mappedOutputs.has(portName)) {
|
|
576
|
+
diagnostic(
|
|
577
|
+
diagnostics,
|
|
578
|
+
"GRAPH_SUBGRAPH_REQUIRED_OUTPUT_UNMAPPED",
|
|
579
|
+
`#/subgraphCalls/${callIndex}/outputBindings`,
|
|
580
|
+
`required parent output is not mapped from the child graph: ${portName}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const internallyBoundInputs = new Set(
|
|
586
|
+
(targetDefinition?.edges || [])
|
|
587
|
+
.filter((edge) => edge.kind === "data")
|
|
588
|
+
.map((edge) => `${edge.to}\0${edge.toPort}`),
|
|
589
|
+
);
|
|
590
|
+
for (const childNode of childNodes.values()) {
|
|
591
|
+
for (const portName of requiredPortNames(childNode.inputs)) {
|
|
592
|
+
const key = `${childNode.id}\0${portName}`;
|
|
593
|
+
if (
|
|
594
|
+
!mappedChildInputs.has(key) &&
|
|
595
|
+
!internallyBoundInputs.has(key) &&
|
|
596
|
+
!Object.hasOwn(childNode.inputBindings || {}, portName)
|
|
597
|
+
) {
|
|
598
|
+
diagnostic(
|
|
599
|
+
diagnostics,
|
|
600
|
+
"GRAPH_SUBGRAPH_REQUIRED_CHILD_INPUT_UNBOUND",
|
|
601
|
+
`#/subgraphCalls/${callIndex}/inputBindings`,
|
|
602
|
+
`required child input has no internal or parent binding: ${childNode.id}.${portName}`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function validateSubgraphs(definition, nodeIds, options, diagnostics) {
|
|
610
|
+
const registry = options.subgraphs || new Map();
|
|
611
|
+
const calls = new Map();
|
|
612
|
+
const registryDefinitions = new Map();
|
|
613
|
+
const registryEntries =
|
|
614
|
+
registry instanceof Map ? registry.entries() : Object.entries(registry);
|
|
615
|
+
for (const [id, value] of registryEntries) {
|
|
616
|
+
registryDefinitions.set(id, stableValue(value?.definition || value));
|
|
617
|
+
}
|
|
618
|
+
for (const [index, call] of definition.subgraphCalls.entries()) {
|
|
619
|
+
if (!nodeIds.has(call.nodeId)) {
|
|
620
|
+
diagnostic(
|
|
621
|
+
diagnostics,
|
|
622
|
+
"GRAPH_UNKNOWN_SUBGRAPH_NODE",
|
|
623
|
+
`#/subgraphCalls/${index}/nodeId`,
|
|
624
|
+
`subgraph call node does not exist: ${call.nodeId}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const callNode = definition.nodes.find((node) => node.id === call.nodeId);
|
|
628
|
+
if (callNode && callNode.kind !== "subgraph") {
|
|
629
|
+
diagnostic(
|
|
630
|
+
diagnostics,
|
|
631
|
+
"GRAPH_SUBGRAPH_NODE_KIND_INVALID",
|
|
632
|
+
`#/subgraphCalls/${index}/nodeId`,
|
|
633
|
+
`subgraph call must target a node with kind=subgraph: ${call.nodeId}`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const target = subgraphTarget(registry, call.definitionId);
|
|
637
|
+
if (!target && options.allowUnresolvedSubgraphs !== true) {
|
|
638
|
+
diagnostic(
|
|
639
|
+
diagnostics,
|
|
640
|
+
"GRAPH_SUBGRAPH_DEFINITION_REQUIRED",
|
|
641
|
+
`#/subgraphCalls/${index}/definitionId`,
|
|
642
|
+
`subgraph definition must be available during compilation: ${call.definitionId}`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (target && target.revisionDigest !== call.revisionDigest) {
|
|
646
|
+
diagnostic(
|
|
647
|
+
diagnostics,
|
|
648
|
+
"GRAPH_SUBGRAPH_DIGEST_MISMATCH",
|
|
649
|
+
`#/subgraphCalls/${index}/revisionDigest`,
|
|
650
|
+
`subgraph digest does not match pinned revision: ${call.definitionId}`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const targetDefinition = target?.definition || target;
|
|
654
|
+
if (targetDefinition) {
|
|
655
|
+
validateSubgraphPortBindings(
|
|
656
|
+
call,
|
|
657
|
+
callNode,
|
|
658
|
+
targetDefinition,
|
|
659
|
+
index,
|
|
660
|
+
diagnostics,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
const targetBudget = target?.budgetUpperBound || targetDefinition?.budget;
|
|
664
|
+
const budget = stableValue(
|
|
665
|
+
call.budget || callNode?.budget || targetDefinition?.budget || {},
|
|
666
|
+
);
|
|
667
|
+
for (const field of BUDGET_FIELDS) {
|
|
668
|
+
const required = Number(targetBudget?.[field] || 0);
|
|
669
|
+
const cap = budget[field];
|
|
670
|
+
if (required > 0 && (cap == null || Number(cap) < required)) {
|
|
671
|
+
diagnostic(
|
|
672
|
+
diagnostics,
|
|
673
|
+
"GRAPH_SUBGRAPH_BUDGET_SLICE_TOO_SMALL",
|
|
674
|
+
`#/subgraphCalls/${index}/budget/${field}`,
|
|
675
|
+
`subgraph ${field} budget slice ${cap ?? "unbounded"} is below the child upper bound ${required}`,
|
|
676
|
+
{ field, required, cap: cap ?? null },
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
calls.set(call.nodeId, { ...call, budget });
|
|
681
|
+
}
|
|
682
|
+
for (const [index, node] of definition.nodes.entries()) {
|
|
683
|
+
if (node.kind === "subgraph" && !calls.has(node.id)) {
|
|
684
|
+
diagnostic(
|
|
685
|
+
diagnostics,
|
|
686
|
+
"GRAPH_SUBGRAPH_CALL_REQUIRED",
|
|
687
|
+
`#/nodes/${index}`,
|
|
688
|
+
`subgraph node requires a pinned SubgraphCall: ${node.id}`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const definitions = new Map();
|
|
694
|
+
definitions.set(definition.id, definition);
|
|
695
|
+
if (registry instanceof Map) {
|
|
696
|
+
for (const [id, value] of registry) {
|
|
697
|
+
definitions.set(id, value.definition || value);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
for (const [id, value] of Object.entries(registry)) {
|
|
701
|
+
definitions.set(id, value.definition || value);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const visiting = new Set();
|
|
705
|
+
const visited = new Set();
|
|
706
|
+
const visit = (id, path = []) => {
|
|
707
|
+
if (visiting.has(id)) {
|
|
708
|
+
diagnostic(
|
|
709
|
+
diagnostics,
|
|
710
|
+
"GRAPH_SUBGRAPH_CALL_CYCLE",
|
|
711
|
+
"#/subgraphCalls",
|
|
712
|
+
`subgraph call cycle: ${[...path, id].join(" -> ")}`,
|
|
713
|
+
);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (visited.has(id)) return;
|
|
717
|
+
const current = definitions.get(id);
|
|
718
|
+
if (!current) return;
|
|
719
|
+
visiting.add(id);
|
|
720
|
+
for (const call of current.subgraphCalls || []) {
|
|
721
|
+
visit(call.definitionId, [...path, id]);
|
|
722
|
+
}
|
|
723
|
+
visiting.delete(id);
|
|
724
|
+
visited.add(id);
|
|
725
|
+
};
|
|
726
|
+
visit(definition.id);
|
|
727
|
+
return { calls, definitions: registryDefinitions };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function upcastGraphDefinitionV0(input) {
|
|
731
|
+
const definition = stableValue(input);
|
|
732
|
+
return {
|
|
733
|
+
schemaVersion: 1,
|
|
734
|
+
id: definition.id,
|
|
735
|
+
revision: definition.revision ?? 1,
|
|
736
|
+
triggers: definition.triggers || [],
|
|
737
|
+
regions: definition.regions || [],
|
|
738
|
+
nodes: (definition.nodes || []).map((node) => ({
|
|
739
|
+
...node,
|
|
740
|
+
kind: node.kind || "task",
|
|
741
|
+
dependsOn: node.dependsOn || [],
|
|
742
|
+
inputs: node.inputs || [],
|
|
743
|
+
outputs: node.outputs || [],
|
|
744
|
+
effectClass: node.effectClass || "none",
|
|
745
|
+
})),
|
|
746
|
+
edges: (definition.edges || []).map((edge, index) => ({
|
|
747
|
+
...edge,
|
|
748
|
+
id: edge.id || `edge-${index + 1}`,
|
|
749
|
+
kind: edge.kind || "control",
|
|
750
|
+
when: edge.when || "success",
|
|
751
|
+
})),
|
|
752
|
+
loops: definition.loops || [],
|
|
753
|
+
subgraphCalls: definition.subgraphCalls || [],
|
|
754
|
+
budget: definition.budget || {},
|
|
755
|
+
allowedCapabilities: definition.allowedCapabilities || [],
|
|
756
|
+
metadata: definition.metadata || {},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const BUILTIN_GRAPH_UPCASTERS = Object.freeze({
|
|
761
|
+
0: upcastGraphDefinitionV0,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
function migrateDefinition(input, options) {
|
|
765
|
+
const sourceVersion = Number(input?.schemaVersion);
|
|
766
|
+
if (sourceVersion === GRAPH_DEFINITION_VERSION) {
|
|
767
|
+
return { definition: stableValue(input), migratedFrom: null };
|
|
768
|
+
}
|
|
769
|
+
const upcaster =
|
|
770
|
+
options.upcasters?.[sourceVersion] ||
|
|
771
|
+
BUILTIN_GRAPH_UPCASTERS[sourceVersion];
|
|
772
|
+
if (
|
|
773
|
+
sourceVersion === GRAPH_DEFINITION_VERSION - 1 &&
|
|
774
|
+
typeof upcaster === "function"
|
|
775
|
+
) {
|
|
776
|
+
const migrated = upcaster(stableValue(input));
|
|
777
|
+
if (Number(migrated?.schemaVersion) !== GRAPH_DEFINITION_VERSION) {
|
|
778
|
+
throw new TypeError("graph upcaster did not produce the current version");
|
|
779
|
+
}
|
|
780
|
+
return { definition: stableValue(migrated), migratedFrom: sourceVersion };
|
|
781
|
+
}
|
|
782
|
+
return { definition: stableValue(input), migratedFrom: null };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function compileGraphDefinition(input, options = {}) {
|
|
786
|
+
const diagnostics = [];
|
|
787
|
+
let migrated;
|
|
788
|
+
try {
|
|
789
|
+
migrated = migrateDefinition(input, options);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
throw new GraphCompileError([
|
|
792
|
+
{
|
|
793
|
+
code: "GRAPH_CANONICALIZATION_FAILED",
|
|
794
|
+
path: "#",
|
|
795
|
+
message: error.message,
|
|
796
|
+
},
|
|
797
|
+
]);
|
|
798
|
+
}
|
|
799
|
+
const definition = migrated.definition;
|
|
800
|
+
const schemaResult = validate(definition, GRAPH_DEFINITION_SCHEMA);
|
|
801
|
+
for (const error of schemaResult.errors) {
|
|
802
|
+
diagnostic(
|
|
803
|
+
diagnostics,
|
|
804
|
+
"GRAPH_SCHEMA_INVALID",
|
|
805
|
+
error.instancePath || error.schemaPath || "#",
|
|
806
|
+
error.message || "graph schema is invalid",
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (!schemaResult.valid) throw new GraphCompileError(diagnostics);
|
|
810
|
+
if (
|
|
811
|
+
definition.schemaVersion < GRAPH_DEFINITION_MIN_VERSION ||
|
|
812
|
+
definition.schemaVersion > GRAPH_DEFINITION_VERSION
|
|
813
|
+
) {
|
|
814
|
+
diagnostic(
|
|
815
|
+
diagnostics,
|
|
816
|
+
"GRAPH_SCHEMA_VERSION_UNSUPPORTED",
|
|
817
|
+
"#/schemaVersion",
|
|
818
|
+
`supported GraphDefinition versions are ${GRAPH_DEFINITION_MIN_VERSION}-${GRAPH_DEFINITION_VERSION}`,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
for (const id of duplicateValues(definition.nodes.map((node) => node.id))) {
|
|
823
|
+
diagnostic(
|
|
824
|
+
diagnostics,
|
|
825
|
+
"GRAPH_DUPLICATE_NODE_ID",
|
|
826
|
+
"#/nodes",
|
|
827
|
+
`duplicate node id: ${id}`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
for (const id of duplicateValues(definition.edges.map((edge) => edge.id))) {
|
|
831
|
+
diagnostic(
|
|
832
|
+
diagnostics,
|
|
833
|
+
"GRAPH_DUPLICATE_EDGE_ID",
|
|
834
|
+
"#/edges",
|
|
835
|
+
`duplicate edge id: ${id}`,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
for (const id of duplicateValues(definition.loops.map((loop) => loop.id))) {
|
|
839
|
+
diagnostic(
|
|
840
|
+
diagnostics,
|
|
841
|
+
"GRAPH_DUPLICATE_REGION_ID",
|
|
842
|
+
"#/loops",
|
|
843
|
+
`duplicate loop region id: ${id}`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
for (const id of duplicateValues(
|
|
847
|
+
(definition.triggers || []).map((item) => item.id),
|
|
848
|
+
)) {
|
|
849
|
+
diagnostic(
|
|
850
|
+
diagnostics,
|
|
851
|
+
"GRAPH_DUPLICATE_TRIGGER_ID",
|
|
852
|
+
"#/triggers",
|
|
853
|
+
`duplicate trigger binding id: ${id}`,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const declaredRegionIds = [
|
|
857
|
+
...definition.loops.map((region) => region.id),
|
|
858
|
+
...(definition.regions || []).map((region) => region.id),
|
|
859
|
+
];
|
|
860
|
+
for (const id of duplicateValues(declaredRegionIds)) {
|
|
861
|
+
diagnostic(
|
|
862
|
+
diagnostics,
|
|
863
|
+
"GRAPH_DUPLICATE_REGION_ID",
|
|
864
|
+
"#/regions",
|
|
865
|
+
`duplicate region id: ${id}`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const nodes = new Map(definition.nodes.map((node) => [node.id, node]));
|
|
869
|
+
const nodeIds = new Set(nodes.keys());
|
|
870
|
+
const compensationTargets = new Map();
|
|
871
|
+
const declareCompensation = (sourceId, targetId) => {
|
|
872
|
+
if (!sourceId || !targetId) return;
|
|
873
|
+
const targets = compensationTargets.get(sourceId) || new Set();
|
|
874
|
+
targets.add(targetId);
|
|
875
|
+
compensationTargets.set(sourceId, targets);
|
|
876
|
+
};
|
|
877
|
+
for (const node of definition.nodes) {
|
|
878
|
+
if (node.compensationNodeId) {
|
|
879
|
+
declareCompensation(node.id, node.compensationNodeId);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const edge of definition.edges) {
|
|
883
|
+
if (edge.kind === "compensation") {
|
|
884
|
+
declareCompensation(edge.from, edge.to);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const compensationByNode = new Map();
|
|
888
|
+
const compensationOwners = new Map();
|
|
889
|
+
for (const [sourceId, targets] of compensationTargets) {
|
|
890
|
+
if (targets.size !== 1) {
|
|
891
|
+
diagnostic(
|
|
892
|
+
diagnostics,
|
|
893
|
+
"GRAPH_COMPENSATION_TARGET_CONFLICT",
|
|
894
|
+
"#/nodes",
|
|
895
|
+
`effect node declares multiple compensation targets: ${sourceId}`,
|
|
896
|
+
{ sourceId, targetIds: [...targets].sort() },
|
|
897
|
+
);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const [targetId] = targets;
|
|
901
|
+
const source = nodes.get(sourceId);
|
|
902
|
+
const target = nodes.get(targetId);
|
|
903
|
+
if (!target) {
|
|
904
|
+
diagnostic(
|
|
905
|
+
diagnostics,
|
|
906
|
+
"GRAPH_UNKNOWN_COMPENSATION_TARGET",
|
|
907
|
+
"#/nodes",
|
|
908
|
+
`unknown compensation target ${targetId} for ${sourceId}`,
|
|
909
|
+
{ sourceId, targetId },
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
if (!source || !target) continue;
|
|
913
|
+
if (sourceId === targetId) {
|
|
914
|
+
diagnostic(
|
|
915
|
+
diagnostics,
|
|
916
|
+
"GRAPH_COMPENSATION_SELF_REFERENCE",
|
|
917
|
+
"#/nodes",
|
|
918
|
+
`node cannot compensate itself: ${sourceId}`,
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (!["workspace_write", "external"].includes(source.effectClass)) {
|
|
922
|
+
diagnostic(
|
|
923
|
+
diagnostics,
|
|
924
|
+
"GRAPH_COMPENSATION_SOURCE_NOT_EFFECTFUL",
|
|
925
|
+
"#/nodes",
|
|
926
|
+
`compensation source must be effectful: ${sourceId}`,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (!["workspace_write", "external"].includes(target.effectClass)) {
|
|
930
|
+
diagnostic(
|
|
931
|
+
diagnostics,
|
|
932
|
+
"GRAPH_COMPENSATION_TARGET_NOT_EFFECTFUL",
|
|
933
|
+
"#/nodes",
|
|
934
|
+
`compensation target must be effectful: ${targetId}`,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const priorOwner = compensationOwners.get(targetId);
|
|
938
|
+
if (priorOwner && priorOwner !== sourceId) {
|
|
939
|
+
diagnostic(
|
|
940
|
+
diagnostics,
|
|
941
|
+
"GRAPH_COMPENSATION_TARGET_REUSED",
|
|
942
|
+
"#/nodes",
|
|
943
|
+
`compensation target ${targetId} is already owned by ${priorOwner}`,
|
|
944
|
+
);
|
|
945
|
+
} else {
|
|
946
|
+
compensationOwners.set(targetId, sourceId);
|
|
947
|
+
compensationByNode.set(sourceId, targetId);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const compensationNodeIds = new Set(compensationOwners.keys());
|
|
951
|
+
for (const targetId of compensationNodeIds) {
|
|
952
|
+
const target = nodes.get(targetId);
|
|
953
|
+
const usedAsForwardDependency = definition.nodes.some((node) =>
|
|
954
|
+
node.dependsOn.includes(targetId),
|
|
955
|
+
);
|
|
956
|
+
const usedByForwardEdge = definition.edges.some(
|
|
957
|
+
(edge) =>
|
|
958
|
+
edge.kind !== "compensation" &&
|
|
959
|
+
(edge.from === targetId || edge.to === targetId),
|
|
960
|
+
);
|
|
961
|
+
if (
|
|
962
|
+
target.dependsOn.length > 0 ||
|
|
963
|
+
usedAsForwardDependency ||
|
|
964
|
+
usedByForwardEdge
|
|
965
|
+
) {
|
|
966
|
+
diagnostic(
|
|
967
|
+
diagnostics,
|
|
968
|
+
"GRAPH_COMPENSATION_TARGET_IN_FORWARD_GRAPH",
|
|
969
|
+
"#/nodes",
|
|
970
|
+
`compensation target must be isolated from the forward dependency graph: ${targetId}`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (compensationTargets.has(targetId)) {
|
|
974
|
+
diagnostic(
|
|
975
|
+
diagnostics,
|
|
976
|
+
"GRAPH_COMPENSATION_RECURSIVE",
|
|
977
|
+
"#/nodes",
|
|
978
|
+
`compensation target cannot declare another compensation target: ${targetId}`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
for (const [index, trigger] of (definition.triggers || []).entries()) {
|
|
983
|
+
if (!nodeIds.has(trigger.targetNodeId)) {
|
|
984
|
+
diagnostic(
|
|
985
|
+
diagnostics,
|
|
986
|
+
"GRAPH_UNKNOWN_TRIGGER_TARGET",
|
|
987
|
+
`#/triggers/${index}/targetNodeId`,
|
|
988
|
+
`trigger targets unknown node: ${trigger.targetNodeId}`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const regions = new Map(
|
|
993
|
+
(definition.regions || []).map((region) => [region.id, region]),
|
|
994
|
+
);
|
|
995
|
+
for (const [index, region] of (definition.regions || []).entries()) {
|
|
996
|
+
for (const regionNodeId of region.nodeIds) {
|
|
997
|
+
if (!nodeIds.has(regionNodeId)) {
|
|
998
|
+
diagnostic(
|
|
999
|
+
diagnostics,
|
|
1000
|
+
"GRAPH_UNKNOWN_REGION_NODE",
|
|
1001
|
+
`#/regions/${index}/nodeIds`,
|
|
1002
|
+
`region contains unknown node: ${regionNodeId}`,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
for (const [field, regionNodeId] of [
|
|
1007
|
+
["entryNodeId", region.entryNodeId],
|
|
1008
|
+
["exitNodeId", region.exitNodeId],
|
|
1009
|
+
]) {
|
|
1010
|
+
if (regionNodeId && !region.nodeIds.includes(regionNodeId)) {
|
|
1011
|
+
diagnostic(
|
|
1012
|
+
diagnostics,
|
|
1013
|
+
"GRAPH_REGION_BOUNDARY_OUTSIDE_REGION",
|
|
1014
|
+
`#/regions/${index}/${field}`,
|
|
1015
|
+
`${field} must belong to region ${region.id}`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (region.parentRegionId && !regions.has(region.parentRegionId)) {
|
|
1020
|
+
diagnostic(
|
|
1021
|
+
diagnostics,
|
|
1022
|
+
"GRAPH_UNKNOWN_PARENT_REGION",
|
|
1023
|
+
`#/regions/${index}/parentRegionId`,
|
|
1024
|
+
`unknown parent region: ${region.parentRegionId}`,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const parentPath = new Set([region.id]);
|
|
1028
|
+
let parentId = region.parentRegionId;
|
|
1029
|
+
while (parentId && regions.has(parentId)) {
|
|
1030
|
+
if (parentPath.has(parentId)) {
|
|
1031
|
+
diagnostic(
|
|
1032
|
+
diagnostics,
|
|
1033
|
+
"GRAPH_REGION_CYCLE",
|
|
1034
|
+
`#/regions/${index}/parentRegionId`,
|
|
1035
|
+
`region parent hierarchy contains a cycle at ${parentId}`,
|
|
1036
|
+
);
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
parentPath.add(parentId);
|
|
1040
|
+
parentId = regions.get(parentId).parentRegionId;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const inputs = new Map();
|
|
1044
|
+
const outputs = new Map();
|
|
1045
|
+
const dependencies = new Map(
|
|
1046
|
+
definition.nodes.map((node) => [node.id, new Set(node.dependsOn)]),
|
|
1047
|
+
);
|
|
1048
|
+
for (const [index, node] of definition.nodes.entries()) {
|
|
1049
|
+
inputs.set(node.id, portMap(node, "inputs", diagnostics, index));
|
|
1050
|
+
outputs.set(node.id, portMap(node, "outputs", diagnostics, index));
|
|
1051
|
+
for (const dependency of node.dependsOn) {
|
|
1052
|
+
if (!nodeIds.has(dependency)) {
|
|
1053
|
+
diagnostic(
|
|
1054
|
+
diagnostics,
|
|
1055
|
+
"GRAPH_UNKNOWN_DEPENDENCY",
|
|
1056
|
+
`#/nodes/${index}/dependsOn`,
|
|
1057
|
+
`unknown dependency ${dependency} for ${node.id}`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
if (dependency === node.id) {
|
|
1061
|
+
diagnostic(
|
|
1062
|
+
diagnostics,
|
|
1063
|
+
"GRAPH_SELF_DEPENDENCY",
|
|
1064
|
+
`#/nodes/${index}/dependsOn`,
|
|
1065
|
+
`node cannot depend on itself: ${node.id}`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const allowedCapabilities = new Set(definition.allowedCapabilities || []);
|
|
1070
|
+
for (const capability of node.capabilities || []) {
|
|
1071
|
+
if (!allowedCapabilities.has(capability)) {
|
|
1072
|
+
diagnostic(
|
|
1073
|
+
diagnostics,
|
|
1074
|
+
"GRAPH_CAPABILITY_ESCALATION",
|
|
1075
|
+
`#/nodes/${index}/capabilities`,
|
|
1076
|
+
`node requests capability outside graph authority: ${capability}`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (
|
|
1081
|
+
["workspace_write", "external"].includes(node.effectClass) &&
|
|
1082
|
+
!node.idempotencyKey
|
|
1083
|
+
) {
|
|
1084
|
+
diagnostic(
|
|
1085
|
+
diagnostics,
|
|
1086
|
+
"GRAPH_EFFECT_MISSING_IDEMPOTENCY_KEY",
|
|
1087
|
+
`#/nodes/${index}/idempotencyKey`,
|
|
1088
|
+
`effectful node requires an idempotency key: ${node.id}`,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
if (
|
|
1092
|
+
node.effectClass === "workspace_write" &&
|
|
1093
|
+
node.workspaceIsolation !== "worktree" &&
|
|
1094
|
+
(!Array.isArray(node.writeSet) || node.writeSet.length === 0)
|
|
1095
|
+
) {
|
|
1096
|
+
diagnostic(
|
|
1097
|
+
diagnostics,
|
|
1098
|
+
"GRAPH_WRITE_SCOPE_REQUIRED",
|
|
1099
|
+
`#/nodes/${index}/writeSet`,
|
|
1100
|
+
`writable node must use a worktree or declare a write set: ${node.id}`,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
if (node.join === "quorum" && !(node.quorum >= 1)) {
|
|
1104
|
+
diagnostic(
|
|
1105
|
+
diagnostics,
|
|
1106
|
+
"GRAPH_INVALID_QUORUM",
|
|
1107
|
+
`#/nodes/${index}/quorum`,
|
|
1108
|
+
"quorum join requires a positive quorum",
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const dependencyPolicies = new Map(
|
|
1114
|
+
definition.nodes.map((node) => [
|
|
1115
|
+
node.id,
|
|
1116
|
+
new Map(node.dependsOn.map((dependency) => [dependency, "success"])),
|
|
1117
|
+
]),
|
|
1118
|
+
);
|
|
1119
|
+
for (const [index, edge] of definition.edges.entries()) {
|
|
1120
|
+
const from = nodes.get(edge.from);
|
|
1121
|
+
const to = nodes.get(edge.to);
|
|
1122
|
+
if (!from) {
|
|
1123
|
+
diagnostic(
|
|
1124
|
+
diagnostics,
|
|
1125
|
+
"GRAPH_UNKNOWN_EDGE_SOURCE",
|
|
1126
|
+
`#/edges/${index}/from`,
|
|
1127
|
+
`unknown edge source: ${edge.from}`,
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (!to) {
|
|
1131
|
+
diagnostic(
|
|
1132
|
+
diagnostics,
|
|
1133
|
+
"GRAPH_UNKNOWN_EDGE_TARGET",
|
|
1134
|
+
`#/edges/${index}/to`,
|
|
1135
|
+
`unknown edge target: ${edge.to}`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
if (!from || !to) continue;
|
|
1139
|
+
if (edge.kind !== "compensation") {
|
|
1140
|
+
dependencies.get(to.id).add(from.id);
|
|
1141
|
+
dependencyPolicies.get(to.id).set(from.id, edge.when);
|
|
1142
|
+
}
|
|
1143
|
+
if (edge.kind === "data") {
|
|
1144
|
+
const sourcePort = outputs.get(from.id).get(edge.fromPort);
|
|
1145
|
+
const targetPort = inputs.get(to.id).get(edge.toPort);
|
|
1146
|
+
if (!sourcePort) {
|
|
1147
|
+
diagnostic(
|
|
1148
|
+
diagnostics,
|
|
1149
|
+
"GRAPH_UNKNOWN_OUTPUT_PORT",
|
|
1150
|
+
`#/edges/${index}/fromPort`,
|
|
1151
|
+
`unknown output port ${edge.fromPort} on ${from.id}`,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (!targetPort) {
|
|
1155
|
+
diagnostic(
|
|
1156
|
+
diagnostics,
|
|
1157
|
+
"GRAPH_UNKNOWN_INPUT_PORT",
|
|
1158
|
+
`#/edges/${index}/toPort`,
|
|
1159
|
+
`unknown input port ${edge.toPort} on ${to.id}`,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (
|
|
1163
|
+
sourcePort &&
|
|
1164
|
+
targetPort &&
|
|
1165
|
+
!isPortSchemaAssignable(sourcePort.schema, targetPort.schema)
|
|
1166
|
+
) {
|
|
1167
|
+
diagnostic(
|
|
1168
|
+
diagnostics,
|
|
1169
|
+
"GRAPH_PORT_TYPE_MISMATCH",
|
|
1170
|
+
`#/edges/${index}`,
|
|
1171
|
+
`${from.id}.${edge.fromPort} is not assignable to ${to.id}.${edge.toPort}`,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const { order, outgoing } = topologicalSort(
|
|
1178
|
+
nodeIds,
|
|
1179
|
+
dependencies,
|
|
1180
|
+
diagnostics,
|
|
1181
|
+
);
|
|
1182
|
+
const { ancestors, descendants } = transitiveSets(
|
|
1183
|
+
order,
|
|
1184
|
+
dependencies,
|
|
1185
|
+
outgoing,
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
for (const [index, node] of definition.nodes.entries()) {
|
|
1189
|
+
for (const [inputName, binding] of Object.entries(
|
|
1190
|
+
node.inputBindings || {},
|
|
1191
|
+
)) {
|
|
1192
|
+
for (const pattern of REFERENCE_PATTERNS) {
|
|
1193
|
+
pattern.lastIndex = 0;
|
|
1194
|
+
for (const match of binding.matchAll(pattern)) {
|
|
1195
|
+
const [, sourceId, portName] = match;
|
|
1196
|
+
if (!nodes.has(sourceId)) {
|
|
1197
|
+
diagnostic(
|
|
1198
|
+
diagnostics,
|
|
1199
|
+
"GRAPH_UNKNOWN_DATA_REFERENCE",
|
|
1200
|
+
`#/nodes/${index}/inputBindings/${inputName}`,
|
|
1201
|
+
`input references unknown node: ${sourceId}`,
|
|
1202
|
+
);
|
|
1203
|
+
} else if (!ancestors.get(node.id)?.has(sourceId)) {
|
|
1204
|
+
diagnostic(
|
|
1205
|
+
diagnostics,
|
|
1206
|
+
"GRAPH_REFERENCE_OUTSIDE_DEPENDENCY_CLOSURE",
|
|
1207
|
+
`#/nodes/${index}/inputBindings/${inputName}`,
|
|
1208
|
+
`input references ${sourceId} without a transitive dependency`,
|
|
1209
|
+
);
|
|
1210
|
+
} else if (!outputs.get(sourceId)?.has(portName)) {
|
|
1211
|
+
diagnostic(
|
|
1212
|
+
diagnostics,
|
|
1213
|
+
"GRAPH_UNKNOWN_OUTPUT_PORT",
|
|
1214
|
+
`#/nodes/${index}/inputBindings/${inputName}`,
|
|
1215
|
+
`input references unknown output ${sourceId}.${portName}`,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
for (let leftIndex = 0; leftIndex < definition.nodes.length; leftIndex += 1) {
|
|
1224
|
+
const left = definition.nodes[leftIndex];
|
|
1225
|
+
if (compensationNodeIds.has(left.id)) continue;
|
|
1226
|
+
if (!["workspace_write", "external"].includes(left.effectClass)) continue;
|
|
1227
|
+
for (
|
|
1228
|
+
let rightIndex = leftIndex + 1;
|
|
1229
|
+
rightIndex < definition.nodes.length;
|
|
1230
|
+
rightIndex += 1
|
|
1231
|
+
) {
|
|
1232
|
+
const right = definition.nodes[rightIndex];
|
|
1233
|
+
if (compensationNodeIds.has(right.id)) continue;
|
|
1234
|
+
if (!["workspace_write", "external"].includes(right.effectClass))
|
|
1235
|
+
continue;
|
|
1236
|
+
if (
|
|
1237
|
+
ancestors.get(left.id)?.has(right.id) ||
|
|
1238
|
+
ancestors.get(right.id)?.has(left.id)
|
|
1239
|
+
) {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (
|
|
1243
|
+
left.workspaceIsolation === "worktree" &&
|
|
1244
|
+
right.workspaceIsolation === "worktree"
|
|
1245
|
+
) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const overlap = (left.writeSet || []).some((leftScope) =>
|
|
1249
|
+
(right.writeSet || []).some((rightScope) =>
|
|
1250
|
+
writeScopesOverlap(leftScope, rightScope),
|
|
1251
|
+
),
|
|
1252
|
+
);
|
|
1253
|
+
if (
|
|
1254
|
+
overlap ||
|
|
1255
|
+
left.effectClass === "external" ||
|
|
1256
|
+
right.effectClass === "external"
|
|
1257
|
+
) {
|
|
1258
|
+
diagnostic(
|
|
1259
|
+
diagnostics,
|
|
1260
|
+
"GRAPH_PARALLEL_WRITE_CONFLICT",
|
|
1261
|
+
"#/nodes",
|
|
1262
|
+
`unordered effectful nodes require worktrees or disjoint scopes: ${left.id}, ${right.id}`,
|
|
1263
|
+
{ nodeIds: [left.id, right.id] },
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const multiplier = loopMultipliers(
|
|
1270
|
+
definition,
|
|
1271
|
+
diagnostics,
|
|
1272
|
+
nodeIds,
|
|
1273
|
+
nodes,
|
|
1274
|
+
dependencies,
|
|
1275
|
+
compensationByNode,
|
|
1276
|
+
);
|
|
1277
|
+
const subgraphs = validateSubgraphs(
|
|
1278
|
+
definition,
|
|
1279
|
+
nodeIds,
|
|
1280
|
+
options,
|
|
1281
|
+
diagnostics,
|
|
1282
|
+
);
|
|
1283
|
+
const budget = budgetUpperBound(
|
|
1284
|
+
definition,
|
|
1285
|
+
multiplier,
|
|
1286
|
+
diagnostics,
|
|
1287
|
+
subgraphs.calls,
|
|
1288
|
+
);
|
|
1289
|
+
if (diagnostics.length) throw new GraphCompileError(diagnostics);
|
|
1290
|
+
|
|
1291
|
+
const revisionDigest = graphDigest(
|
|
1292
|
+
definition,
|
|
1293
|
+
`cc.graph.definition/${definition.schemaVersion}`,
|
|
1294
|
+
);
|
|
1295
|
+
const compiled = {
|
|
1296
|
+
schema: "chainlesschain.compiled-graph/v1",
|
|
1297
|
+
schemaVersion: definition.schemaVersion,
|
|
1298
|
+
definition: deepFreeze(definition),
|
|
1299
|
+
definitionId: definition.id,
|
|
1300
|
+
revision: definition.revision,
|
|
1301
|
+
revisionDigest,
|
|
1302
|
+
migratedFrom: migrated.migratedFrom,
|
|
1303
|
+
nodes: deepFreeze(
|
|
1304
|
+
Object.fromEntries([...nodes].map(([id, value]) => [id, value])),
|
|
1305
|
+
),
|
|
1306
|
+
topologicalOrder: Object.freeze(order),
|
|
1307
|
+
forwardTopologicalOrder: Object.freeze(
|
|
1308
|
+
order.filter((nodeId) => !compensationNodeIds.has(nodeId)),
|
|
1309
|
+
),
|
|
1310
|
+
compensationByNode: deepFreeze(Object.fromEntries(compensationByNode)),
|
|
1311
|
+
compensationNodeIds: Object.freeze([...compensationNodeIds].sort()),
|
|
1312
|
+
dependencies: deepFreeze(
|
|
1313
|
+
Object.fromEntries(
|
|
1314
|
+
[...dependencies].map(([id, values]) => [id, [...values].sort()]),
|
|
1315
|
+
),
|
|
1316
|
+
),
|
|
1317
|
+
dependencyPolicies: deepFreeze(
|
|
1318
|
+
Object.fromEntries(
|
|
1319
|
+
[...dependencyPolicies].map(([id, values]) => [
|
|
1320
|
+
id,
|
|
1321
|
+
Object.fromEntries([...values].sort()),
|
|
1322
|
+
]),
|
|
1323
|
+
),
|
|
1324
|
+
),
|
|
1325
|
+
ancestors: deepFreeze(
|
|
1326
|
+
Object.fromEntries(
|
|
1327
|
+
[...ancestors].map(([id, values]) => [id, [...values].sort()]),
|
|
1328
|
+
),
|
|
1329
|
+
),
|
|
1330
|
+
descendants: deepFreeze(
|
|
1331
|
+
Object.fromEntries(
|
|
1332
|
+
[...descendants].map(([id, values]) => [id, [...values].sort()]),
|
|
1333
|
+
),
|
|
1334
|
+
),
|
|
1335
|
+
loopMultipliers: deepFreeze(Object.fromEntries(multiplier)),
|
|
1336
|
+
loops: deepFreeze(
|
|
1337
|
+
Object.fromEntries(definition.loops.map((loop) => [loop.id, loop])),
|
|
1338
|
+
),
|
|
1339
|
+
loopByNode: deepFreeze(
|
|
1340
|
+
Object.fromEntries(
|
|
1341
|
+
definition.loops.flatMap((loop) =>
|
|
1342
|
+
loop.nodeIds.map((nodeId) => [nodeId, loop.id]),
|
|
1343
|
+
),
|
|
1344
|
+
),
|
|
1345
|
+
),
|
|
1346
|
+
loopByExitNode: deepFreeze(
|
|
1347
|
+
Object.fromEntries(
|
|
1348
|
+
definition.loops.map((loop) => [loop.exitNodeId, loop.id]),
|
|
1349
|
+
),
|
|
1350
|
+
),
|
|
1351
|
+
budgetUpperBound: deepFreeze(budget),
|
|
1352
|
+
subgraphCalls: deepFreeze(Object.fromEntries(subgraphs.calls)),
|
|
1353
|
+
subgraphDefinitions: deepFreeze(Object.fromEntries(subgraphs.definitions)),
|
|
1354
|
+
triggers: deepFreeze([...(definition.triggers || [])]),
|
|
1355
|
+
regions: deepFreeze([...(definition.regions || [])]),
|
|
1356
|
+
};
|
|
1357
|
+
deepFreeze(compiled);
|
|
1358
|
+
compiledGraphs.add(compiled);
|
|
1359
|
+
return compiled;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
export function isCompiledGraph(value) {
|
|
1363
|
+
return compiledGraphs.has(value);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
export function assertCompiledGraph(value) {
|
|
1367
|
+
if (!isCompiledGraph(value)) {
|
|
1368
|
+
const error = new TypeError(
|
|
1369
|
+
"Graph effects require an authenticated compileGraphDefinition result",
|
|
1370
|
+
);
|
|
1371
|
+
error.code = "CC_GRAPH_NOT_COMPILED";
|
|
1372
|
+
error.effectStarted = false;
|
|
1373
|
+
throw error;
|
|
1374
|
+
}
|
|
1375
|
+
return value;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export function migrateGraphDefinition(input, options = {}) {
|
|
1379
|
+
const { definition, migratedFrom } = migrateDefinition(input, options);
|
|
1380
|
+
const compiled = compileGraphDefinition(definition, options);
|
|
1381
|
+
const backupDefinition =
|
|
1382
|
+
migratedFrom == null ? null : deepFreeze(stableValue(input));
|
|
1383
|
+
return Object.freeze({
|
|
1384
|
+
dryRun: options.dryRun !== false,
|
|
1385
|
+
fromVersion: migratedFrom ?? definition.schemaVersion,
|
|
1386
|
+
toVersion: compiled.schemaVersion,
|
|
1387
|
+
definition: compiled.definition,
|
|
1388
|
+
revisionDigest: compiled.revisionDigest,
|
|
1389
|
+
backupRequired: migratedFrom != null,
|
|
1390
|
+
backupDefinition,
|
|
1391
|
+
rollbackDigest:
|
|
1392
|
+
backupDefinition == null
|
|
1393
|
+
? null
|
|
1394
|
+
: graphDigest(
|
|
1395
|
+
backupDefinition,
|
|
1396
|
+
`cc.graph.definition-backup/${migratedFrom}`,
|
|
1397
|
+
),
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
export function restoreGraphDefinitionBackup(
|
|
1402
|
+
backupDefinition,
|
|
1403
|
+
expectedRollbackDigest,
|
|
1404
|
+
) {
|
|
1405
|
+
const backup = stableValue(backupDefinition);
|
|
1406
|
+
const version = Number(backup.schemaVersion);
|
|
1407
|
+
if (version !== GRAPH_DEFINITION_VERSION - 1) {
|
|
1408
|
+
const error = new TypeError(
|
|
1409
|
+
`backup must use GraphDefinition version ${GRAPH_DEFINITION_VERSION - 1}`,
|
|
1410
|
+
);
|
|
1411
|
+
error.code = "CC_GRAPH_MIGRATION_BACKUP_INVALID";
|
|
1412
|
+
throw error;
|
|
1413
|
+
}
|
|
1414
|
+
const digest = graphDigest(backup, `cc.graph.definition-backup/${version}`);
|
|
1415
|
+
if (digest !== expectedRollbackDigest) {
|
|
1416
|
+
const error = new TypeError("graph migration backup digest does not match");
|
|
1417
|
+
error.code = "CC_GRAPH_MIGRATION_BACKUP_TAMPERED";
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1420
|
+
return deepFreeze(backup);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
export function executionAttemptId(nodeId, iterationPath = [], attempt = 1) {
|
|
1424
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
1425
|
+
throw new TypeError("attempt must be a positive integer");
|
|
1426
|
+
}
|
|
1427
|
+
const path = Array.isArray(iterationPath)
|
|
1428
|
+
? iterationPath.map((value) => {
|
|
1429
|
+
const number = Number(value);
|
|
1430
|
+
if (!Number.isSafeInteger(number) || number < 0) {
|
|
1431
|
+
throw new TypeError(
|
|
1432
|
+
"iterationPath must contain non-negative integers",
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
return number;
|
|
1436
|
+
})
|
|
1437
|
+
: [];
|
|
1438
|
+
const digest = graphDigest(
|
|
1439
|
+
{ nodeId: String(nodeId), iterationPath: path, attempt },
|
|
1440
|
+
"cc.graph.execution-attempt/v1",
|
|
1441
|
+
);
|
|
1442
|
+
return `attempt-${digest.slice(7, 47)}`;
|
|
1443
|
+
}
|