chainlesschain 0.166.21 → 0.166.22
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/README.md +67 -9
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BjMnsUci.js → AIOps-oJ7USzMC.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DdYFP03A.js → ActionButton-BlgnZe_C.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bb8TuuK_.js → Analytics-D2rRbTLu.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-Kn3TIOtk.js → AppLayout-FwS3f0B2.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-4IxpqI_b.js → Artifacts-iF-_08Hi.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BOV2caqU.js → Audit-D24p0zQw.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-B86k-ay4.js → BackgroundAgents-Cg3mRVi9.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-C5_ie1e3.js → Backup-EOqht3H-.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-xpwg6QCW.js → BaseInput-9XWYq2hj.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CVZOcZau.js → Chat-Dq5yardA.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D8fPYhUu.js → ChatBubbleRenderer-Ds3pA-ET.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-MKTq3zdP.js → Checkbox-CuyCBoBf.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-JUk3wzep.js → Codegen-DjWtYHNe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-JPvTA0kC.js → Col-CrVr_iuG.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Tqgl87NQ.js → Community-C6QxpfGc.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BuAFYBmN.js → Compact-B-v5r6Pa.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CXS9WIPZ.js → Compliance-zNiLiFgD.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Ds2bFHrr.js → Cowork-D9K7lMOu.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-C_dV_OeW.js → Cron-DAjO36z4.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BsO6tIRo.js → Crosschain-Bxqn0aqB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-rNOFqBac.js → DID-DfRmLUe-.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C2fcKKVY.js → Dashboard-Cz8KF3fr.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Y7Ndr0X_.js → Dropdown-C4oVfkP0.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DcP_uPmL.js → EmailListRenderer-ivyZKN8k.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cwn3gDzk.js → FamilyGuardDashboard-DoxY1CPW.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-ClQuOsqb.js → Federation-C93gZxnb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DfwtmlcK.js → FormItemContext-D_lEjYJE.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-zNZhtbC5.js → GenericCardRenderer-BNFN9YjN.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BZmdW-G5.js → Git-mUu2HcLI.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CqBk4lEn.js → Governance-CNuosrge.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dbmt3ZXk.js → Inference-DwY5pbCB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-hMTH6qf3.js → KnowledgeGraph-aRUxhACD.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CkiYgiW2.js → Logs-DFfsWYy_.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BL1FElXg.js → MarkdownRenderer-DqTx7_u1.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CbB6wfin.js → Marketplace-COI-ciM-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BSwiFtte.js → McpTools-BJJwaIpe.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-B03PeHrz.js → Memory-CDO6SGDZ.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CRGrnd-X.js → MobileBridge-BlZ5tFf4.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CE_5v2kO.js → MobileProjects-DuoxbgWz.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Dp11QVx1.js → Mtc-6ec9j70b.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CYu-3JBu.js → MtcAudit-BuL7HkC7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CGyJ0Xl3.js → Multisig-CJxR85lk.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CoWi_ov1.js → NLProgramming-B71O1S4l.js} +1 -1
- package/src/assets/web-panel/assets/Notes-Bsw5zbpV.js +7 -0
- package/src/assets/web-panel/assets/{NotificationSettings-DX4I2b4W.js → NotificationSettings-BnAhXuK_.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BJ0R5GGz.js +1 -0
- package/src/assets/web-panel/assets/{Organization-oopLO_tk.js → Organization-B7wwNxoP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BFVvjMyf.js → Overflow-CfAGr0gM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CBy5t-Tk.js → P2P-KhMg22Fz.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser--YWJNsu6.js → PdhVaultBrowser-NdAXI9ft.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C2dZ8scb.js → Permissions-CIYjqyT7.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Bs3l7Hlx.js → PersonalDataHub-D2ht3P_R.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DEBWbZuD.js → Pipeline-i6flZW1m.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CjHBb6NU.js → Privacy-DE0lhYVc.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-D2WipUz5.js → ProjectInit-BL3YbyxS.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B6WfnrGB.js → ProjectSettings-CEtdg7ni.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-FF_thRi6.js → Projects-CvtU5peJ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CCv5M_Dv.js → Providers-DKt_Cg-j.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CGthxU54.js → QrScannerModal-DA61jNSx.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CnPmXPFb.js → QuickAsk-fS9ryUmq.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Df_tGEa9.js → Recommend-CR9dZU5Y.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Z58N0Loe.js → RemoteSession-BV0BR2Dz.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CZTBQ2Vt.js → Reputation-BIeGUvYD.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CdK3YpCQ.js → Row-DHDT1aro.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CFMVT5sb.js → RssFeed-Cq6YKb1h.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DrF8RIvD.js → Search-DEF6vsy6.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Ddk2f2DM.js → Security-CHtYiKY9.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CCuK-VOo.js → Services-u16lvBJN.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DxCpHzeM.js → Skeleton-CGbizyDj.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-t2oTDgcv.js → Skills-BZMQcAa7.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Df63M9_R.js → Sla-Bm-CycVi.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-l7ox6KuX.js → SpeechSettings-BudpA5kr.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-COd_tgPp.js → SyncSettings-B8q1C1eC.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CzDCcrik.js → Tasks-SU1pBWc-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D-vfxTGA.js → Templates-CyOfJzmt.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BlpT_Gxc.js → Tenant-xqWSm2GP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D5rq52Hg.js → Terminal-D1Ttgjsi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DdoMndkK.js → TimelineRenderer-Da2lYs5D.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BhgBsI6g.js → Tokens-B34-SduO.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DwwzoikZ.js → Trigger-C8zyYm4R.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-HkTpzXFb.js → Trust-D1Pvqems.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D9nk6Ehe.js → UkeySign-Do3VVnAy.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BKk0rfoy.js → VideoEditing-Dz7egVIA.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-IVWIU_Lq.js → Wallet-Dcuw-OxR.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CFTCpfsG.js → WebAuthn-DPlw-QsB.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Dnf4Obif.js → WorkflowEditor-h4OsnU5q.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D2snERdj.js → chat-eO9wEwrW.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DKEjK1N1.js → colors-34lSmOXw.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BEDLwQdt.js → compact-item-CK4kEWJ4.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DA9iW1a0.js → createContext-E0A57xed.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CF3xafX2.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-5l2xEcY9.js → hasIn-BD9ZMGAl.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7sKbM5s.js → index--N0u6kTG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DU2TXJu-.js → index-8qMA1DWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxEtQYdi.js → index-B0rkHxKK.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7_W_rYd.js → index-B_G_ElKO.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4QYBliR.js → index-BbX_Dted.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLoalqRX.js → index-Bjs90Eh9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbkghqn_.js → index-BmNbU2Z6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgfImyZI.js → index-Brqc9Emx.js} +1 -1
- package/src/assets/web-panel/assets/{index-5pW4t1iD.js → index-BsGmo6v1.js} +1 -1
- package/src/assets/web-panel/assets/index-C907rPwd.js +1 -0
- package/src/assets/web-panel/assets/{index-CRPrHE7h.js → index-CT1_eRL-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvV03Ayk.js → index-CZBaqe3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7z4zB_a.js → index-CZkFMeWL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmruhzFi.js → index-Ch175NUw.js} +1 -1
- package/src/assets/web-panel/assets/{index-e-e40id6.js → index-CsrKJJcX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGlotREA.js → index-CuSau5ya.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjWltbi_.js → index-Cx3Jn1r0.js} +1 -1
- package/src/assets/web-panel/assets/{index-fccZmC_h.js → index-Cz3unNfL.js} +1 -1
- package/src/assets/web-panel/assets/{index-NJieh3T1.js → index-D4bB_x_v.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkMKuqUs.js → index-DCn-ECvK.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ5lY2zx.js → index-DFLE2qO9.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_eCoZgW.js → index-DGRxt5wj.js} +1 -1
- package/src/assets/web-panel/assets/{index-7P1b8HZo.js → index-DONOUtlz.js} +1 -1
- package/src/assets/web-panel/assets/{index-yYEQXYaU.js → index-DP-UZAQN.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbd7eEex.js → index-Da5fmOHB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqc8tdIF.js → index-DcE1BspQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXet1xol.js → index-DcbgVLOH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BO9dQjpt.js → index-DeXNCyQJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX1x4AFn.js → index-DiT_REby.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq5P3piS.js → index-DpfisY1g.js} +1 -1
- package/src/assets/web-panel/assets/{index-Csfb-7hw.js → index-DrOalGut.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpqtbXdE.js → index-DrSAAuJj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B00GeekA.js → index-DrsDA_md.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg2a9owQ.js → index-DvKcroix.js} +1 -1
- package/src/assets/web-panel/assets/{index-fgYk0QXO.js → index-DzXjP44T.js} +1 -1
- package/src/assets/web-panel/assets/{index-Td8ZwyoC.js → index-MIg5Z8qe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqwPFbOb.js → index-o8pYehRq.js} +3 -3
- package/src/assets/web-panel/assets/index-wueRSK2d.js +1 -0
- package/src/assets/web-panel/assets/{index-CbTPvp2_.js → index-yAGBu2E0.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CPrcB1TC.js → initDefaultProps-DMTkAomF.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BQ7mWRh_.js → motion-Cnti1F5g.js} +1 -1
- package/src/assets/web-panel/assets/{move-CzOEq99p.js → move-CGxmHUga.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BlQI5jrw.js → mtc-parser-CuwwLoq7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DlW4gxmv.js → omit-BpdlTkPM.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BBYms5nP.js → pickAttrs-D7nWrThp.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-az2MPJ_7.js → placementArrow-Bhx0TV8F.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D1-ibEol.js → responsiveObserve-CZ1KesZh.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DHGqa4GA.js → slide-CnldF8J6.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CYGDo6lD.js → statusUtils-CZfo_GOA.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cl2EM9K6.js → styleChecker-B2QCcAv5.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DdYhzuv1.js → useFlexGapSupport-DzzjIS4J.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DWumPuBW.js → useFs-B_atg9M0.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DxAO-Aqi.js → usePersonalDataHub-DhZs3QYN.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CplsQf_N.js → vnode-DcePTYAm.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Cw5HXNSG.js → zoom-BYmlnjRk.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/serve.js +31 -0
- package/src/data/changelog.json +13 -0
- package/src/index.js +11 -3
- package/src/lazy-dispatch.js +9 -1
- package/src/lib/app-server/websocket-transport.js +21 -0
- package/src/lib/evolution/evolution-artifact-ports.js +9 -0
- package/src/lib/evolution/evolution-deployment-loader.js +208 -0
- package/src/lib/evolution/evolution-release-train-ledger-adapter.js +335 -0
- package/src/lib/evolution/evolution-release-train.js +538 -0
- package/src/lib/evolution/evolution-workbench-projection.js +20 -1
- package/src/lib/evolution/evolvable-artifact-ledger-adapter.js +688 -0
- package/src/lib/evolution/governed-knowledge-artifact-lifecycle.js +364 -0
- package/src/lib/evolution/governed-knowledge-merge-ledger-executor.js +29 -2
- package/src/lib/evolution/governed-knowledge-merge-publisher-authority.js +31 -3
- package/src/lib/evolution/governed-knowledge-sync-ledger-adapter.js +92 -6
- package/src/lib/evolution/governed-knowledge-sync-merge-publisher.js +57 -4
- package/src/lib/evolution/governed-knowledge-sync.js +317 -7
- package/src/lib/evolution/skill-writer-inventory-manifest.js +7 -6
- package/src/lib/evolution/statistical-progressive-canary.js +683 -0
- package/src/lib/evolution/wikiskill-benchmark.js +510 -0
- package/src/lib/session-host-lease.js +11 -16
- package/src/runtime/agent-core.js +61 -7
- package/src/runtime/headless-stream.js +3 -0
- package/src/assets/web-panel/assets/Notes-CgU9DDo9.js +0 -7
- package/src/assets/web-panel/assets/OrderTableRenderer-CwHc15Zs.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DyhgpXPf.js +0 -1
- package/src/assets/web-panel/assets/index-Ck1jhFE0.js +0 -1
- package/src/assets/web-panel/assets/index-c8jJEHVs.js +0 -1
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const PROGRESSIVE_CANARY_PLAN_SCHEMA =
|
|
4
|
+
"chainlesschain.progressive-canary-plan/v1";
|
|
5
|
+
export const PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA =
|
|
6
|
+
"chainlesschain.progressive-canary-assignment/v1";
|
|
7
|
+
export const PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA =
|
|
8
|
+
"chainlesschain.progressive-canary-gate-report/v1";
|
|
9
|
+
|
|
10
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
11
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$/u;
|
|
12
|
+
const RISK_TIERS = new Set(["low", "medium", "high"]);
|
|
13
|
+
const AUTHORITIES = new WeakSet();
|
|
14
|
+
|
|
15
|
+
function canonical(value) {
|
|
16
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
17
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
18
|
+
return `{${Object.keys(value)
|
|
19
|
+
.sort()
|
|
20
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
21
|
+
.join(",")}}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hash(domain, value) {
|
|
25
|
+
return `sha256:${createHash("sha256")
|
|
26
|
+
.update(domain)
|
|
27
|
+
.update("\0")
|
|
28
|
+
.update(canonical(value))
|
|
29
|
+
.digest("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function exact(value, keys, name) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
34
|
+
throw new TypeError(`${name} must be an object`);
|
|
35
|
+
if (canonical(Object.keys(value).sort()) !== canonical([...keys].sort()))
|
|
36
|
+
throw new TypeError(`${name} has unexpected or missing fields`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function id(value, name) {
|
|
40
|
+
if (typeof value !== "string" || !SAFE_ID.test(value))
|
|
41
|
+
throw new TypeError(`${name} is invalid`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sha(value, name) {
|
|
46
|
+
if (!DIGEST.test(value ?? ""))
|
|
47
|
+
throw new TypeError(`${name} must be a sha256 digest`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function integer(value, name, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
53
|
+
throw new TypeError(`${name} is out of range`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function finite(value, name, { min = -Infinity, max = Infinity } = {}) {
|
|
58
|
+
if (!Number.isFinite(value) || value < min || value > max)
|
|
59
|
+
throw new TypeError(`${name} is out of range`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function verifyPlan(plan) {
|
|
64
|
+
if (!plan || plan.schema !== PROGRESSIVE_CANARY_PLAN_SCHEMA)
|
|
65
|
+
throw new TypeError("a canonical progressive Canary plan is required");
|
|
66
|
+
const core = structuredClone(plan);
|
|
67
|
+
delete core.planDigest;
|
|
68
|
+
if (hash(PROGRESSIVE_CANARY_PLAN_SCHEMA, core) !== plan.planDigest)
|
|
69
|
+
throw new Error("progressive Canary plan digest mismatch");
|
|
70
|
+
return plan;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeSteps(steps) {
|
|
74
|
+
if (!Array.isArray(steps) || steps.length < 3 || steps.length > 16)
|
|
75
|
+
throw new TypeError("progressive Canary plan requires 3..16 steps");
|
|
76
|
+
const ids = new Set();
|
|
77
|
+
const normalized = steps.map((step, index) => {
|
|
78
|
+
exact(
|
|
79
|
+
step,
|
|
80
|
+
[
|
|
81
|
+
"id",
|
|
82
|
+
"stage",
|
|
83
|
+
"trafficPercent",
|
|
84
|
+
"minSamples",
|
|
85
|
+
"minWindowMs",
|
|
86
|
+
"maxWindowMs",
|
|
87
|
+
],
|
|
88
|
+
`Canary step ${index}`,
|
|
89
|
+
);
|
|
90
|
+
const stepId = id(step.id, `Canary step ${index} id`);
|
|
91
|
+
if (ids.has(stepId))
|
|
92
|
+
throw new TypeError(`duplicate Canary step: ${stepId}`);
|
|
93
|
+
ids.add(stepId);
|
|
94
|
+
if (!new Set(["shadow", "canary", "active-probation"]).has(step.stage))
|
|
95
|
+
throw new TypeError(`Canary step ${index} stage is invalid`);
|
|
96
|
+
const result = {
|
|
97
|
+
id: stepId,
|
|
98
|
+
stage: step.stage,
|
|
99
|
+
trafficPercent: finite(
|
|
100
|
+
step.trafficPercent,
|
|
101
|
+
`Canary step ${index} trafficPercent`,
|
|
102
|
+
{
|
|
103
|
+
min: 0,
|
|
104
|
+
max: 100,
|
|
105
|
+
},
|
|
106
|
+
),
|
|
107
|
+
minSamples: integer(step.minSamples, `Canary step ${index} minSamples`, {
|
|
108
|
+
min: 1,
|
|
109
|
+
max: 1_000_000,
|
|
110
|
+
}),
|
|
111
|
+
minWindowMs: integer(
|
|
112
|
+
step.minWindowMs,
|
|
113
|
+
`Canary step ${index} minWindowMs`,
|
|
114
|
+
{
|
|
115
|
+
min: 1,
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
maxWindowMs: integer(
|
|
119
|
+
step.maxWindowMs,
|
|
120
|
+
`Canary step ${index} maxWindowMs`,
|
|
121
|
+
{
|
|
122
|
+
min: 1,
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
if (result.maxWindowMs < result.minWindowMs)
|
|
127
|
+
throw new TypeError(`Canary step ${index} window is invalid`);
|
|
128
|
+
return Object.freeze(result);
|
|
129
|
+
});
|
|
130
|
+
if (
|
|
131
|
+
normalized[0].stage !== "shadow" ||
|
|
132
|
+
normalized[0].trafficPercent !== 0 ||
|
|
133
|
+
normalized.at(-1).stage !== "active-probation" ||
|
|
134
|
+
normalized.at(-1).trafficPercent !== 100 ||
|
|
135
|
+
normalized.slice(1, -1).some((step) => step.stage !== "canary")
|
|
136
|
+
)
|
|
137
|
+
throw new TypeError(
|
|
138
|
+
"progressive Canary steps must be shadow, Canary steps, then active-probation",
|
|
139
|
+
);
|
|
140
|
+
for (let index = 1; index < normalized.length; index += 1) {
|
|
141
|
+
if (
|
|
142
|
+
normalized[index].trafficPercent <= normalized[index - 1].trafficPercent
|
|
143
|
+
)
|
|
144
|
+
throw new TypeError(
|
|
145
|
+
"progressive Canary traffic must increase monotonically",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return Object.freeze(normalized);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createProgressiveCanaryPlan(input = {}) {
|
|
152
|
+
exact(
|
|
153
|
+
input,
|
|
154
|
+
[
|
|
155
|
+
"tenantId",
|
|
156
|
+
"pilotId",
|
|
157
|
+
"skillName",
|
|
158
|
+
"candidateDigest",
|
|
159
|
+
"baselineDigest",
|
|
160
|
+
"riskTier",
|
|
161
|
+
"assignmentSaltDigest",
|
|
162
|
+
"assignmentAuthority",
|
|
163
|
+
"steps",
|
|
164
|
+
"thresholds",
|
|
165
|
+
],
|
|
166
|
+
"progressive Canary plan",
|
|
167
|
+
);
|
|
168
|
+
if (!RISK_TIERS.has(input.riskTier))
|
|
169
|
+
throw new TypeError("progressive Canary riskTier is invalid");
|
|
170
|
+
exact(
|
|
171
|
+
input.assignmentAuthority,
|
|
172
|
+
["id", "revision", "handlerDigest"],
|
|
173
|
+
"assignmentAuthority",
|
|
174
|
+
);
|
|
175
|
+
exact(
|
|
176
|
+
input.thresholds,
|
|
177
|
+
[
|
|
178
|
+
"confidence",
|
|
179
|
+
"bootstrapSamples",
|
|
180
|
+
"minQualityDeltaLowerBound",
|
|
181
|
+
"maxMeanCostDelta",
|
|
182
|
+
"maxP95LatencyRatio",
|
|
183
|
+
"maxP99LatencyRatio",
|
|
184
|
+
"maxMeanToolCallDelta",
|
|
185
|
+
],
|
|
186
|
+
"Canary thresholds",
|
|
187
|
+
);
|
|
188
|
+
if (input.thresholds.bootstrapSamples !== 1_000)
|
|
189
|
+
throw new TypeError("Canary bootstrapSamples must equal 1000");
|
|
190
|
+
const core = {
|
|
191
|
+
schema: PROGRESSIVE_CANARY_PLAN_SCHEMA,
|
|
192
|
+
tenantId: id(input.tenantId, "tenantId"),
|
|
193
|
+
pilotId: id(input.pilotId, "pilotId"),
|
|
194
|
+
skillName: id(input.skillName, "skillName"),
|
|
195
|
+
candidateDigest: sha(input.candidateDigest, "candidateDigest"),
|
|
196
|
+
baselineDigest: sha(input.baselineDigest, "baselineDigest"),
|
|
197
|
+
riskTier: input.riskTier,
|
|
198
|
+
assignmentSaltDigest: sha(
|
|
199
|
+
input.assignmentSaltDigest,
|
|
200
|
+
"assignmentSaltDigest",
|
|
201
|
+
),
|
|
202
|
+
assignmentAuthority: Object.freeze({
|
|
203
|
+
id: id(input.assignmentAuthority.id, "assignmentAuthority.id"),
|
|
204
|
+
revision: integer(
|
|
205
|
+
input.assignmentAuthority.revision,
|
|
206
|
+
"assignmentAuthority.revision",
|
|
207
|
+
{ min: 1 },
|
|
208
|
+
),
|
|
209
|
+
handlerDigest: sha(
|
|
210
|
+
input.assignmentAuthority.handlerDigest,
|
|
211
|
+
"assignmentAuthority.handlerDigest",
|
|
212
|
+
),
|
|
213
|
+
}),
|
|
214
|
+
steps: normalizeSteps(input.steps),
|
|
215
|
+
thresholds: Object.freeze({
|
|
216
|
+
confidence: finite(input.thresholds.confidence, "confidence", {
|
|
217
|
+
min: 0.9,
|
|
218
|
+
max: 0.999,
|
|
219
|
+
}),
|
|
220
|
+
bootstrapSamples: 1_000,
|
|
221
|
+
minQualityDeltaLowerBound: finite(
|
|
222
|
+
input.thresholds.minQualityDeltaLowerBound,
|
|
223
|
+
"minQualityDeltaLowerBound",
|
|
224
|
+
{ min: -1, max: 1 },
|
|
225
|
+
),
|
|
226
|
+
maxMeanCostDelta: finite(
|
|
227
|
+
input.thresholds.maxMeanCostDelta,
|
|
228
|
+
"maxMeanCostDelta",
|
|
229
|
+
),
|
|
230
|
+
maxP95LatencyRatio: finite(
|
|
231
|
+
input.thresholds.maxP95LatencyRatio,
|
|
232
|
+
"maxP95LatencyRatio",
|
|
233
|
+
{ min: 0 },
|
|
234
|
+
),
|
|
235
|
+
maxP99LatencyRatio: finite(
|
|
236
|
+
input.thresholds.maxP99LatencyRatio,
|
|
237
|
+
"maxP99LatencyRatio",
|
|
238
|
+
{ min: 0 },
|
|
239
|
+
),
|
|
240
|
+
maxMeanToolCallDelta: finite(
|
|
241
|
+
input.thresholds.maxMeanToolCallDelta,
|
|
242
|
+
"maxMeanToolCallDelta",
|
|
243
|
+
),
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
...core,
|
|
248
|
+
planDigest: hash(PROGRESSIVE_CANARY_PLAN_SCHEMA, core),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function bucket(plan, subjectDigest) {
|
|
253
|
+
const digest = hash(PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA, {
|
|
254
|
+
planDigest: plan.planDigest,
|
|
255
|
+
assignmentSaltDigest: plan.assignmentSaltDigest,
|
|
256
|
+
subjectDigest,
|
|
257
|
+
});
|
|
258
|
+
return Number.parseInt(digest.slice(7, 15), 16) % 10_000;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createProgressiveCanaryAssignmentAuthority({
|
|
262
|
+
plan: planInput,
|
|
263
|
+
attestor,
|
|
264
|
+
verifier,
|
|
265
|
+
now = Date.now,
|
|
266
|
+
} = {}) {
|
|
267
|
+
const plan = verifyPlan(planInput);
|
|
268
|
+
if (typeof attestor !== "function" || typeof verifier !== "function")
|
|
269
|
+
throw new TypeError("Canary assignment attestor and verifier are required");
|
|
270
|
+
if (typeof now !== "function")
|
|
271
|
+
throw new TypeError("Canary authority clock is required");
|
|
272
|
+
|
|
273
|
+
async function verifyReceipt(receipt, { stepId, subjectDigest } = {}) {
|
|
274
|
+
exact(
|
|
275
|
+
receipt,
|
|
276
|
+
[
|
|
277
|
+
"schema",
|
|
278
|
+
"planDigest",
|
|
279
|
+
"tenantId",
|
|
280
|
+
"pilotId",
|
|
281
|
+
"stepId",
|
|
282
|
+
"subjectDigest",
|
|
283
|
+
"bucket",
|
|
284
|
+
"assigned",
|
|
285
|
+
"authorityId",
|
|
286
|
+
"authorityRevision",
|
|
287
|
+
"handlerDigest",
|
|
288
|
+
"issuedAt",
|
|
289
|
+
"expiresAt",
|
|
290
|
+
"signature",
|
|
291
|
+
"receiptDigest",
|
|
292
|
+
],
|
|
293
|
+
"Canary assignment receipt",
|
|
294
|
+
);
|
|
295
|
+
if (
|
|
296
|
+
receipt?.schema !== PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA ||
|
|
297
|
+
receipt.planDigest !== plan.planDigest ||
|
|
298
|
+
receipt.tenantId !== plan.tenantId ||
|
|
299
|
+
receipt.pilotId !== plan.pilotId ||
|
|
300
|
+
receipt.stepId !== stepId ||
|
|
301
|
+
receipt.subjectDigest !== subjectDigest ||
|
|
302
|
+
!DIGEST.test(receipt.receiptDigest ?? "")
|
|
303
|
+
)
|
|
304
|
+
throw new Error("Canary assignment receipt binding is invalid");
|
|
305
|
+
const core = structuredClone(receipt);
|
|
306
|
+
delete core.receiptDigest;
|
|
307
|
+
if (
|
|
308
|
+
hash(PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA, core) !== receipt.receiptDigest
|
|
309
|
+
)
|
|
310
|
+
throw new Error("Canary assignment receipt digest mismatch");
|
|
311
|
+
const step = plan.steps.find(({ id: value }) => value === stepId);
|
|
312
|
+
if (!step || receipt.bucket !== bucket(plan, subjectDigest))
|
|
313
|
+
throw new Error("Canary assignment bucket is invalid");
|
|
314
|
+
const expectedAssigned =
|
|
315
|
+
step.stage !== "canary" || receipt.bucket < step.trafficPercent * 100;
|
|
316
|
+
if (receipt.assigned !== expectedAssigned)
|
|
317
|
+
throw new Error("Canary assignment decision is invalid");
|
|
318
|
+
if (
|
|
319
|
+
receipt.authorityId !== plan.assignmentAuthority.id ||
|
|
320
|
+
receipt.authorityRevision !== plan.assignmentAuthority.revision ||
|
|
321
|
+
receipt.handlerDigest !== plan.assignmentAuthority.handlerDigest ||
|
|
322
|
+
typeof receipt.issuedAt !== "string" ||
|
|
323
|
+
typeof receipt.expiresAt !== "string" ||
|
|
324
|
+
!Number.isFinite(Date.parse(receipt.issuedAt)) ||
|
|
325
|
+
!Number.isFinite(Date.parse(receipt.expiresAt)) ||
|
|
326
|
+
Date.parse(receipt.issuedAt) > Number(now()) ||
|
|
327
|
+
Date.parse(receipt.expiresAt) < Number(now()) ||
|
|
328
|
+
typeof receipt.signature !== "string" ||
|
|
329
|
+
receipt.signature.length < 32
|
|
330
|
+
)
|
|
331
|
+
throw new Error("Canary assignment attestation is invalid or expired");
|
|
332
|
+
const payload = structuredClone(core);
|
|
333
|
+
delete payload.signature;
|
|
334
|
+
if (!(await verifier({ payload, signature: receipt.signature })))
|
|
335
|
+
throw new Error("Canary assignment signature rejected");
|
|
336
|
+
return Object.freeze(structuredClone(receipt));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const authority = Object.freeze({
|
|
340
|
+
async assign({ stepId, subjectDigest: subjectDigestInput } = {}) {
|
|
341
|
+
const step = plan.steps.find(({ id: value }) => value === stepId);
|
|
342
|
+
if (!step) throw new TypeError("Canary assignment step is invalid");
|
|
343
|
+
const subjectDigest = sha(subjectDigestInput, "subjectDigest");
|
|
344
|
+
const assignedBucket = bucket(plan, subjectDigest);
|
|
345
|
+
const payload = {
|
|
346
|
+
schema: PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA,
|
|
347
|
+
planDigest: plan.planDigest,
|
|
348
|
+
tenantId: plan.tenantId,
|
|
349
|
+
pilotId: plan.pilotId,
|
|
350
|
+
stepId,
|
|
351
|
+
subjectDigest,
|
|
352
|
+
bucket: assignedBucket,
|
|
353
|
+
assigned:
|
|
354
|
+
step.stage !== "canary" || assignedBucket < step.trafficPercent * 100,
|
|
355
|
+
authorityId: plan.assignmentAuthority.id,
|
|
356
|
+
authorityRevision: plan.assignmentAuthority.revision,
|
|
357
|
+
handlerDigest: plan.assignmentAuthority.handlerDigest,
|
|
358
|
+
};
|
|
359
|
+
const attestation = await attestor(
|
|
360
|
+
Object.freeze(structuredClone(payload)),
|
|
361
|
+
);
|
|
362
|
+
exact(
|
|
363
|
+
attestation,
|
|
364
|
+
["issuedAt", "expiresAt", "signature"],
|
|
365
|
+
"assignment attestation",
|
|
366
|
+
);
|
|
367
|
+
const core = { ...payload, ...attestation };
|
|
368
|
+
return verifyReceipt(
|
|
369
|
+
Object.freeze({
|
|
370
|
+
...core,
|
|
371
|
+
receiptDigest: hash(PROGRESSIVE_CANARY_ASSIGNMENT_SCHEMA, core),
|
|
372
|
+
}),
|
|
373
|
+
{ stepId, subjectDigest },
|
|
374
|
+
);
|
|
375
|
+
},
|
|
376
|
+
verify: verifyReceipt,
|
|
377
|
+
});
|
|
378
|
+
AUTHORITIES.add(authority);
|
|
379
|
+
return authority;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function percentile(sorted, probability) {
|
|
383
|
+
if (sorted.length === 1) return sorted[0];
|
|
384
|
+
const position = (sorted.length - 1) * probability;
|
|
385
|
+
const lower = Math.floor(position);
|
|
386
|
+
const upper = Math.ceil(position);
|
|
387
|
+
return (
|
|
388
|
+
sorted[lower] * (1 - (position - lower)) +
|
|
389
|
+
sorted[upper] * (position - lower)
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function mean(values) {
|
|
394
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function ratio(candidate, baseline) {
|
|
398
|
+
if (baseline === 0) return candidate === 0 ? 1 : Number.MAX_VALUE;
|
|
399
|
+
return candidate / baseline;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function randomFrom(value) {
|
|
403
|
+
let state = Number.parseInt(value.slice(7, 15), 16) || 0x9e3779b9;
|
|
404
|
+
return () => {
|
|
405
|
+
state ^= state << 13;
|
|
406
|
+
state ^= state >>> 17;
|
|
407
|
+
state ^= state << 5;
|
|
408
|
+
return (state >>> 0) / 0x1_0000_0000;
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function verifyGateReport(report, plan) {
|
|
413
|
+
if (
|
|
414
|
+
report?.schema !== PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA ||
|
|
415
|
+
report.planDigest !== plan.planDigest ||
|
|
416
|
+
!DIGEST.test(report.reportDigest ?? "")
|
|
417
|
+
)
|
|
418
|
+
throw new Error("progressive Canary gate report is invalid");
|
|
419
|
+
const core = structuredClone(report);
|
|
420
|
+
delete core.reportDigest;
|
|
421
|
+
if (hash(PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA, core) !== report.reportDigest)
|
|
422
|
+
throw new Error("progressive Canary gate report digest mismatch");
|
|
423
|
+
return report;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function evaluateProgressiveCanaryGate({
|
|
427
|
+
plan: planInput,
|
|
428
|
+
stepId,
|
|
429
|
+
stepStartedAt,
|
|
430
|
+
observedAt,
|
|
431
|
+
observations,
|
|
432
|
+
assignmentAuthority,
|
|
433
|
+
} = {}) {
|
|
434
|
+
const plan = verifyPlan(planInput);
|
|
435
|
+
if (!AUTHORITIES.has(assignmentAuthority))
|
|
436
|
+
throw new TypeError("a branded Canary assignment authority is required");
|
|
437
|
+
const step = plan.steps.find(({ id: value }) => value === stepId);
|
|
438
|
+
if (!step) throw new TypeError("progressive Canary step is invalid");
|
|
439
|
+
integer(stepStartedAt, "stepStartedAt");
|
|
440
|
+
integer(observedAt, "observedAt", { min: stepStartedAt });
|
|
441
|
+
if (!Array.isArray(observations) || observations.length > 1_000_000)
|
|
442
|
+
throw new TypeError("Canary observations must be a bounded array");
|
|
443
|
+
const subjects = new Set();
|
|
444
|
+
const normalized = [];
|
|
445
|
+
for (let index = 0; index < observations.length; index += 1) {
|
|
446
|
+
const item = observations[index];
|
|
447
|
+
exact(
|
|
448
|
+
item,
|
|
449
|
+
[
|
|
450
|
+
"subjectDigest",
|
|
451
|
+
"assignmentReceipt",
|
|
452
|
+
"outcomeReceiptDigest",
|
|
453
|
+
"observedAt",
|
|
454
|
+
"baselineSuccess",
|
|
455
|
+
"candidateSuccess",
|
|
456
|
+
"baselineCost",
|
|
457
|
+
"candidateCost",
|
|
458
|
+
"baselineLatencyMs",
|
|
459
|
+
"candidateLatencyMs",
|
|
460
|
+
"baselineToolCalls",
|
|
461
|
+
"candidateToolCalls",
|
|
462
|
+
"securityEvents",
|
|
463
|
+
"permissionEvents",
|
|
464
|
+
],
|
|
465
|
+
`Canary observation ${index}`,
|
|
466
|
+
);
|
|
467
|
+
const subjectDigest = sha(
|
|
468
|
+
item.subjectDigest,
|
|
469
|
+
`observation ${index} subjectDigest`,
|
|
470
|
+
);
|
|
471
|
+
if (subjects.has(subjectDigest))
|
|
472
|
+
throw new TypeError("Canary observations contain a duplicate subject");
|
|
473
|
+
subjects.add(subjectDigest);
|
|
474
|
+
const assignment = await assignmentAuthority.verify(
|
|
475
|
+
item.assignmentReceipt,
|
|
476
|
+
{
|
|
477
|
+
stepId,
|
|
478
|
+
subjectDigest,
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
if (!assignment.assigned)
|
|
482
|
+
throw new Error("non-assigned traffic cannot enter Canary evidence");
|
|
483
|
+
integer(item.observedAt, `observation ${index} observedAt`, {
|
|
484
|
+
min: stepStartedAt,
|
|
485
|
+
max: observedAt,
|
|
486
|
+
});
|
|
487
|
+
if (
|
|
488
|
+
typeof item.baselineSuccess !== "boolean" ||
|
|
489
|
+
typeof item.candidateSuccess !== "boolean"
|
|
490
|
+
)
|
|
491
|
+
throw new TypeError(`observation ${index} paired outcome is invalid`);
|
|
492
|
+
normalized.push(
|
|
493
|
+
Object.freeze({
|
|
494
|
+
subjectDigest,
|
|
495
|
+
assignmentReceiptDigest: assignment.receiptDigest,
|
|
496
|
+
outcomeReceiptDigest: sha(
|
|
497
|
+
item.outcomeReceiptDigest,
|
|
498
|
+
`observation ${index} outcomeReceiptDigest`,
|
|
499
|
+
),
|
|
500
|
+
observedAt: item.observedAt,
|
|
501
|
+
baselineSuccess: item.baselineSuccess,
|
|
502
|
+
candidateSuccess: item.candidateSuccess,
|
|
503
|
+
baselineCost: finite(
|
|
504
|
+
item.baselineCost,
|
|
505
|
+
`observation ${index} baselineCost`,
|
|
506
|
+
{
|
|
507
|
+
min: 0,
|
|
508
|
+
},
|
|
509
|
+
),
|
|
510
|
+
candidateCost: finite(
|
|
511
|
+
item.candidateCost,
|
|
512
|
+
`observation ${index} candidateCost`,
|
|
513
|
+
{
|
|
514
|
+
min: 0,
|
|
515
|
+
},
|
|
516
|
+
),
|
|
517
|
+
baselineLatencyMs: finite(
|
|
518
|
+
item.baselineLatencyMs,
|
|
519
|
+
`observation ${index} baselineLatencyMs`,
|
|
520
|
+
{ min: 0 },
|
|
521
|
+
),
|
|
522
|
+
candidateLatencyMs: finite(
|
|
523
|
+
item.candidateLatencyMs,
|
|
524
|
+
`observation ${index} candidateLatencyMs`,
|
|
525
|
+
{ min: 0 },
|
|
526
|
+
),
|
|
527
|
+
baselineToolCalls: integer(
|
|
528
|
+
item.baselineToolCalls,
|
|
529
|
+
`observation ${index} baselineToolCalls`,
|
|
530
|
+
),
|
|
531
|
+
candidateToolCalls: integer(
|
|
532
|
+
item.candidateToolCalls,
|
|
533
|
+
`observation ${index} candidateToolCalls`,
|
|
534
|
+
),
|
|
535
|
+
securityEvents: integer(
|
|
536
|
+
item.securityEvents,
|
|
537
|
+
`observation ${index} securityEvents`,
|
|
538
|
+
),
|
|
539
|
+
permissionEvents: integer(
|
|
540
|
+
item.permissionEvents,
|
|
541
|
+
`observation ${index} permissionEvents`,
|
|
542
|
+
),
|
|
543
|
+
}),
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
normalized.sort((left, right) =>
|
|
547
|
+
left.subjectDigest.localeCompare(right.subjectDigest),
|
|
548
|
+
);
|
|
549
|
+
const elapsedMs = observedAt - stepStartedAt;
|
|
550
|
+
const failures = [];
|
|
551
|
+
if (normalized.length < step.minSamples) failures.push("min_samples");
|
|
552
|
+
if (elapsedMs < step.minWindowMs) failures.push("min_window");
|
|
553
|
+
if (elapsedMs > step.maxWindowMs) failures.push("max_window");
|
|
554
|
+
const securityEvents = normalized.reduce(
|
|
555
|
+
(sum, item) => sum + item.securityEvents,
|
|
556
|
+
0,
|
|
557
|
+
);
|
|
558
|
+
const permissionEvents = normalized.reduce(
|
|
559
|
+
(sum, item) => sum + item.permissionEvents,
|
|
560
|
+
0,
|
|
561
|
+
);
|
|
562
|
+
if (securityEvents > 0) failures.push("security_events");
|
|
563
|
+
if (permissionEvents > 0) failures.push("permission_events");
|
|
564
|
+
|
|
565
|
+
let metrics = null;
|
|
566
|
+
if (normalized.length > 0) {
|
|
567
|
+
const qualityDeltas = normalized.map(
|
|
568
|
+
(item) => Number(item.candidateSuccess) - Number(item.baselineSuccess),
|
|
569
|
+
);
|
|
570
|
+
const seedDigest = hash(PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA, {
|
|
571
|
+
planDigest: plan.planDigest,
|
|
572
|
+
stepId,
|
|
573
|
+
receipts: normalized.map((item) => item.outcomeReceiptDigest),
|
|
574
|
+
});
|
|
575
|
+
const random = randomFrom(seedDigest);
|
|
576
|
+
const bootstrap = Array.from(
|
|
577
|
+
{ length: plan.thresholds.bootstrapSamples },
|
|
578
|
+
() =>
|
|
579
|
+
mean(
|
|
580
|
+
Array.from(
|
|
581
|
+
{ length: qualityDeltas.length },
|
|
582
|
+
() => qualityDeltas[Math.floor(random() * qualityDeltas.length)],
|
|
583
|
+
),
|
|
584
|
+
),
|
|
585
|
+
).sort((left, right) => left - right);
|
|
586
|
+
const alpha = 1 - plan.thresholds.confidence;
|
|
587
|
+
const baselineLatencies = normalized
|
|
588
|
+
.map((item) => item.baselineLatencyMs)
|
|
589
|
+
.sort((left, right) => left - right);
|
|
590
|
+
const candidateLatencies = normalized
|
|
591
|
+
.map((item) => item.candidateLatencyMs)
|
|
592
|
+
.sort((left, right) => left - right);
|
|
593
|
+
metrics = Object.freeze({
|
|
594
|
+
samples: normalized.length,
|
|
595
|
+
qualityDelta: mean(qualityDeltas),
|
|
596
|
+
qualityDeltaConfidenceInterval: Object.freeze([
|
|
597
|
+
percentile(bootstrap, alpha / 2),
|
|
598
|
+
percentile(bootstrap, 1 - alpha / 2),
|
|
599
|
+
]),
|
|
600
|
+
meanCostDelta: mean(
|
|
601
|
+
normalized.map((item) => item.candidateCost - item.baselineCost),
|
|
602
|
+
),
|
|
603
|
+
p95LatencyRatio: ratio(
|
|
604
|
+
percentile(candidateLatencies, 0.95),
|
|
605
|
+
percentile(baselineLatencies, 0.95),
|
|
606
|
+
),
|
|
607
|
+
p99LatencyRatio: ratio(
|
|
608
|
+
percentile(candidateLatencies, 0.99),
|
|
609
|
+
percentile(baselineLatencies, 0.99),
|
|
610
|
+
),
|
|
611
|
+
meanToolCallDelta: mean(
|
|
612
|
+
normalized.map(
|
|
613
|
+
(item) => item.candidateToolCalls - item.baselineToolCalls,
|
|
614
|
+
),
|
|
615
|
+
),
|
|
616
|
+
securityEvents,
|
|
617
|
+
permissionEvents,
|
|
618
|
+
});
|
|
619
|
+
if (
|
|
620
|
+
metrics.qualityDeltaConfidenceInterval[0] <
|
|
621
|
+
plan.thresholds.minQualityDeltaLowerBound
|
|
622
|
+
)
|
|
623
|
+
failures.push("quality_confidence_lower_bound");
|
|
624
|
+
if (metrics.meanCostDelta > plan.thresholds.maxMeanCostDelta)
|
|
625
|
+
failures.push("mean_cost_delta");
|
|
626
|
+
if (metrics.p95LatencyRatio > plan.thresholds.maxP95LatencyRatio)
|
|
627
|
+
failures.push("p95_latency_ratio");
|
|
628
|
+
if (metrics.p99LatencyRatio > plan.thresholds.maxP99LatencyRatio)
|
|
629
|
+
failures.push("p99_latency_ratio");
|
|
630
|
+
if (metrics.meanToolCallDelta > plan.thresholds.maxMeanToolCallDelta)
|
|
631
|
+
failures.push("mean_tool_call_delta");
|
|
632
|
+
}
|
|
633
|
+
const core = {
|
|
634
|
+
schema: PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA,
|
|
635
|
+
planDigest: plan.planDigest,
|
|
636
|
+
stepId,
|
|
637
|
+
stage: step.stage,
|
|
638
|
+
trafficPercent: step.trafficPercent,
|
|
639
|
+
stepStartedAt,
|
|
640
|
+
observedAt,
|
|
641
|
+
elapsedMs,
|
|
642
|
+
observationRootDigest: hash(
|
|
643
|
+
PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA,
|
|
644
|
+
normalized,
|
|
645
|
+
),
|
|
646
|
+
metrics,
|
|
647
|
+
failures: Object.freeze([...new Set(failures)]),
|
|
648
|
+
passed: failures.length === 0,
|
|
649
|
+
};
|
|
650
|
+
return Object.freeze({
|
|
651
|
+
...core,
|
|
652
|
+
reportDigest: hash(PROGRESSIVE_CANARY_GATE_REPORT_SCHEMA, core),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function nextProgressiveCanaryStage({
|
|
657
|
+
plan: planInput,
|
|
658
|
+
currentStepId,
|
|
659
|
+
gateReport,
|
|
660
|
+
} = {}) {
|
|
661
|
+
const plan = verifyPlan(planInput);
|
|
662
|
+
const report = verifyGateReport(gateReport, plan);
|
|
663
|
+
const index = plan.steps.findIndex(
|
|
664
|
+
({ id: value }) => value === currentStepId,
|
|
665
|
+
);
|
|
666
|
+
if (index < 0 || report.stepId !== currentStepId)
|
|
667
|
+
throw new Error("progressive Canary gate report is bound to another step");
|
|
668
|
+
if (!report.passed) throw new Error("progressive Canary gate did not pass");
|
|
669
|
+
if (index === plan.steps.length - 1) {
|
|
670
|
+
return Object.freeze({
|
|
671
|
+
stage: "stable",
|
|
672
|
+
stepId: null,
|
|
673
|
+
priorReportDigest: report.reportDigest,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const next = plan.steps[index + 1];
|
|
677
|
+
return Object.freeze({
|
|
678
|
+
stage: next.stage,
|
|
679
|
+
stepId: next.id,
|
|
680
|
+
trafficPercent: next.trafficPercent,
|
|
681
|
+
priorReportDigest: report.reportDigest,
|
|
682
|
+
});
|
|
683
|
+
}
|