chainlesschain 0.163.3 → 0.163.4
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/package.json +3 -2
- package/scripts/command-lifecycle-report.mjs +330 -38
- package/src/assets/web-panel/assets/{AIOps-DIY6KxV-.js → AIOps-DB5X7qqV.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BXH0muPt.js → ActionButton-DxHRtKLk.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D39w8vmg.js → Analytics-DIH5QrxT.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CUU1zvLs.js → AppLayout-uu0oxVz7.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D8azHO3T.js → Artifacts-BNSMiW9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-D0tAwXTg.js → Audit-DcAzB4_x.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C29mhJll.js → BackgroundAgents-ClSsvjIc.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D6Aa1Gro.js → Backup-BMciY0js.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B1uoHbPQ.js → BaseInput-Hn3b57Sh.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Bs4DLOR5.js → Chat-CoSlC53V.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DUYIBSle.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CNcBPEa4.js → Checkbox-B4L4Eu4w.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CIY89763.js → Codegen-C77FgRxF.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CsPYFmgo.js → Col-BrLtJLH3.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BBocEge4.js → Community-Be7pXXhA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-TQ8Ux0Lf.js → Compact-DuGPecjo.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DfBsZTft.js → Compliance-CzS66mjP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cg-I6FVH.js → Cowork-CEhjCoyK.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DTQzfRTp.js → Cron-D7ir8Vja.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DGxEKGpR.js → Crosschain-BcXmuLxf.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DLZ1RDSr.js → DID-Cto1dtBy.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-XneICKNI.js → Dashboard-B7KjgwOX.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BRU00J1-.js → Dropdown-COMqxijg.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BzCbJuAy.js → EmailListRenderer-BZrya-nf.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D7BRyNBG.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9DeFIhW.js → Federation-kZU8c-27.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-6rAtA9Ko.js → FormItemContext-sOV22v2-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-8Hqy80pU.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Cmvkj1rt.js → Git-K-ZxsLsH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BRiQUjjw.js → Governance-D3OEnawI.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CKN_EzVf.js → Inference-Bll0gRmP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BCDHwpwQ.js → KnowledgeGraph-DBXwjr53.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DK9ovhaB.js → Logs-CEzPwEfP.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-CLmCbHx6.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BILPGakp.js → Marketplace-BYyRyOPz.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DTVCOr9-.js → McpTools-DVu8w_rM.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Hzc1iow4.js → Memory-tenuZ_hO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-GXfgnGYV.js → MobileBridge-Bxgy4ozS.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B-3TJWsS.js → MobileProjects-Dgx1D6sc.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DwSJMWtK.js → Mtc-CRfJ8r8j.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-D2uxf-DA.js → MtcAudit-BBJq4Aiz.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DG6kbKK0.js → Multisig-B3ii9lai.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-f5p49EDU.js → NLProgramming-FIi-Cubv.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-FUgHyzSD.js → Notes-DIq8MPf1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DCHGee7e.js → NotificationSettings-BFOD9eGd.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dm6LlIoa.js → OrderTableRenderer--41u7vre.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BawA7qAg.js → Organization-B_0AWztW.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D6a5wZU1.js → Overflow-B6I7TuXv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-K8WavwnA.js → P2P-Cdf6qens.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-4JAJYaqx.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BDWvi8PD.js → Permissions-6nFBjsQp.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DF_K4E9i.js → PersonalDataHub-nQYV7xpn.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-BYeZlesI.js → Pipeline-CBVod5jm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BIHlgnIc.js → Privacy-D52jW1Nt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Cvf-6B5P.js → ProjectInit-Bdx3BYhf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CnwfJrRz.js → ProjectSettings-b7xr4DUa.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-b-jPXzF4.js → Projects-DhfifCKx.js} +1 -1
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-VLf3gAfL.js → QrScannerModal-FQKSmc3e.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B5vGjRiF.js → QuickAsk-CSTrhzcF.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend--UW4iYs-.js → Recommend-CxMABOUh.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DMSPcfKR.js → RemoteSession-Cv2T8yRa.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-kQXlEHmj.js → Reputation-CE1k4Vvu.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CxGu3pit.js → Row-BNgNcAV_.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Bi5lo8MZ.js → RssFeed-5ZiBZ3ph.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Cq_H5RTh.js → Search-mDOIF1UV.js} +1 -1
- package/src/assets/web-panel/assets/{Security-YFbmkaQS.js → Security-CtuCTC-e.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DLOGdaLm.js → Services-DxFO2Zg7.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CteIxb3D.js → Skeleton-CKmKkH9y.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CccopaIZ.js → Skills-Bmzl9qkk.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-AzfEQKX1.js → Sla-9RqaWHOC.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ekGo5l77.js → SpeechSettings-CGdzFIYw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BmZmAgYH.js → SyncSettings-CvNuCeBT.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-D7q3Z4uL.js → Tasks-B_sLdcnw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CX1Tgeka.js → Templates-D9_YijdK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Zc59jF4i.js → Tenant-Cuc7BUSP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CXOD7y83.js → Terminal-6VYRnJgd.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-B1aBHPBo.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-zALlOq_U.js → Tokens-BZ94Lt5W.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CQfJMXaq.js → Trigger-DEcZgDaX.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BU0RcM-w.js → Trust-CC3z9THe.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-IFZ3HsbH.js → UkeySign-Cvtw8cV3.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BzslgW0v.js → VideoEditing-CL4e6Bn3.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BZAcQNS6.js → Wallet-ycEemP27.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CBK1DMi0.js → WebAuthn-DgZ4X_aw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BlMTPzO-.js → WorkflowEditor-D0qMhSv7.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B51-8QVn.js → chat-DsAg01Zn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-504RTByU.js → colors-B0oqAhrQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C-GP6IQM.js → compact-item-CmX0Rabz.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DOkIj7ID.js → createContext-Be4cn1Ub.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DN7V-_FW.js → hasIn-D5v12kVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-kt_5rfcZ.js → index-0eWLkQKT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUhQpiYv.js → index-301ogenv.js} +1 -1
- package/src/assets/web-panel/assets/{index-f4Bo4KXm.js → index-5LGY0JbG.js} +1 -1
- package/src/assets/web-panel/assets/{index--n-ZciV3.js → index-7VKDn-t1.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1phyca8.js → index-8WvYGnON.js} +1 -1
- package/src/assets/web-panel/assets/{index-7jIqHbzw.js → index-B1YGjOWC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEMPUVa3.js → index-B3mUHuVc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bkxhaeta.js → index-BA4oulaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYopNjcJ.js → index-BBREnn1V.js} +1 -1
- package/src/assets/web-panel/assets/{index-P2mfY27Z.js → index-BBvyHguy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DA2f1UV3.js → index-BKmsS2jj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CP2A226L.js → index-BRIqLy1m.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfiEM64n.js → index-BXGSbelg.js} +3 -3
- package/src/assets/web-panel/assets/{index-B2-x9bS8.js → index-BaE0Ui07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CySTJKeq.js → index-BoAIAZ5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-kAqQXfP6.js → index-BqltWojQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAOpQvl_.js → index-CGiIyTZw.js} +1 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
- package/src/assets/web-panel/assets/{index-C4DV_5uA.js → index-CZ2wUEgr.js} +1 -1
- package/src/assets/web-panel/assets/{index-yYTQZypF.js → index-CZpfzcuy.js} +1 -1
- package/src/assets/web-panel/assets/{index-et5JIPHO.js → index-Ckrk3pIV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaA__-q1.js → index-CqoCZ5vR.js} +1 -1
- package/src/assets/web-panel/assets/{index-By6u5IV1.js → index-CyWBir7N.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKTfZyfU.js → index-CyyGTMKd.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_vGU-Js.js → index-D0BSkxYQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cao5DZy7.js → index-D5YVopyr.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3TSohzc.js → index-DKUBj36_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFIFJ5Dr.js → index-DM724NgU.js} +1 -1
- package/src/assets/web-panel/assets/{index-ConjaJro.js → index-DQRsQen0.js} +1 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
- package/src/assets/web-panel/assets/{index-C9_48OzU.js → index-DikJ0Qqe.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0k_Nqg9.js → index-Dn2dSdXx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE-hmI2l.js → index-DyUaFWEk.js} +1 -1
- package/src/assets/web-panel/assets/{index-so8eIIxF.js → index-Hfe_dEGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-FcV8J_Qz.js → index-_JjRLSD0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9LyiN-L.js → index-frSojpHj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX78k-qf.js → index-jK3ax6Z6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8UltbXK.js → index-qMuZ-NRR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqcLo5WV.js → index-q_fQ94SE.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CPYEIdpV.js → initDefaultProps-D4VdhJ9_.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Cjim9pxs.js → motion-CZJA0MQW.js} +1 -1
- package/src/assets/web-panel/assets/{move-DuU-cgz3.js → move-BqEs7Sb1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DJHNAkta.js → mtc-parser-BpHEtsu1.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DSQBgdBj.js → omit-BW94tY9W.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CTmT_j5m.js → pickAttrs-BnBjPWP9.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Lfm6v-r4.js → placementArrow-uUkjQ_hu.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CgrcPcDi.js → responsiveObserve-B-klUqXK.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CuE3110i.js → slide-CvY1cNAs.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DiK5nbpf.js → statusUtils-Ix5sEDT2.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DPEJdQaZ.js → styleChecker-C8qcXOne.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CnnodGmp.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DWF84Yav.js → useFs-DGJr345n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-a5b5Dw2f.js → usePersonalDataHub-BboN99rI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAluzXOq.js → vnode-Bgt8Vgte.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Dokg7uAZ.js → zoom-BhJtErgt.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/artifacts.js +98 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/review.js +221 -22
- package/src/commands/session-mcp-recovery.js +5 -5
- package/src/data/changelog.json +14 -1
- package/src/gateways/ws/ws-agent-handler.js +492 -14
- package/src/harness/jsonl-session-store.js +39 -1
- package/src/harness/mcp-client.js +172 -29
- package/src/harness/prompt-compressor.js +24 -0
- package/src/harness/provider-backed-compaction.js +205 -0
- package/src/lib/app-preview.js +3 -0
- package/src/lib/command-lifecycle-report.js +1340 -145
- package/src/lib/delivery-coordinator.js +30 -5
- package/src/lib/delivery-production-adapter.js +1777 -0
- package/src/lib/delivery-production-runner.js +240 -0
- package/src/lib/durable-security-store.js +29 -17
- package/src/lib/execution-trace/trace-context.cjs +179 -0
- package/src/lib/execution-trace/trace-context.js +8 -175
- package/src/lib/mcp-call-ledger-store.js +58 -6
- package/src/lib/mcp-host-recovery-runtime.js +2 -0
- package/src/lib/mcp-ledger-recovery-admission.js +39 -3
- package/src/lib/mcp-recovery-adjudication.js +256 -19
- package/src/lib/mcp-sandbox-policy.js +29 -0
- package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
- package/src/lib/mcp-stdio-executable-identity.js +33 -2
- package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
- package/src/lib/mcp-stdio-package-materialization.js +721 -333
- package/src/lib/observability/index.js +2 -0
- package/src/lib/observability/otlp-exporter.js +2 -2
- package/src/lib/packer/pack-update-applier.js +8 -1
- package/src/lib/packer/pkg-config-generator.js +34 -0
- package/src/lib/process-execution-broker/index.js +34 -5
- package/src/lib/process-execution-broker/platform-sandbox.js +82 -1
- package/src/lib/scheduler-kernel/contract.js +315 -0
- package/src/lib/scheduler-kernel/store.js +1345 -0
- package/src/lib/session-host-lease.js +403 -30
- package/src/lib/session-persistence-failure.js +4 -1
- package/src/lib/sub-agent-context.js +8 -0
- package/src/repl/agent-repl.js +285 -38
- package/src/runtime/agent-core.js +1044 -90
- package/src/runtime/headless-runner.js +260 -166
- package/src/runtime/headless-stream.js +254 -25
- package/src/assets/web-panel/assets/MarkdownRenderer-D53pHiHy.js +0 -1
- package/src/assets/web-panel/assets/Providers-CxC7JuYv.js +0 -1
- package/src/assets/web-panel/assets/devWarning-kB0nCNwN.js +0 -1
- package/src/assets/web-panel/assets/index-Bya64sZE.js +0 -1
- package/src/assets/web-panel/assets/index-D_FWpDZM.js +0 -1
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Privacy-preserving command lifecycle telemetry aggregation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Version 2 treats a lifecycle decision as an auditable evidence bundle. The
|
|
5
|
+
* observation window, public releases, raw Collector exports, coverage
|
|
6
|
+
* statement and human approval are all bound to the report. OTLP delta points
|
|
7
|
+
* are de-duplicated by their exact point identity; cumulative streams are
|
|
8
|
+
* reduced to their latest monotonic value instead of summing snapshots.
|
|
9
|
+
* Missing or ambiguous evidence can only retain a compatibility alias.
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { realpathSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { verifyPackUpdateManifest } from "./packer/pack-update-signature.js";
|
|
16
|
+
import executionBroker from "./process-execution-broker/index.js";
|
|
17
|
+
|
|
11
18
|
export const COMMAND_LIFECYCLE_REPORT_SCHEMA =
|
|
12
|
-
"chainlesschain.command-lifecycle-report.
|
|
19
|
+
"chainlesschain.command-lifecycle-report.v2";
|
|
13
20
|
export const COMMAND_LIFECYCLE_COVERAGE_SCHEMA =
|
|
14
|
-
"chainlesschain.command-lifecycle-coverage.
|
|
21
|
+
"chainlesschain.command-lifecycle-coverage.v2";
|
|
22
|
+
export const COMMAND_LIFECYCLE_APPROVAL_SCHEMA =
|
|
23
|
+
"chainlesschain.command-lifecycle-approval.v2";
|
|
24
|
+
export const COMMAND_LIFECYCLE_EXPORT_MANIFEST_SCHEMA =
|
|
25
|
+
"chainlesschain.command-lifecycle-export-manifest.v1";
|
|
15
26
|
export const COMMAND_LIFECYCLE_INVOCATION_METRIC =
|
|
16
27
|
"chainlesschain.cli.command.lifecycle.invocations";
|
|
17
28
|
|
|
@@ -27,6 +38,15 @@ export const COMMAND_ALIAS_DECISION_POLICY = Object.freeze({
|
|
|
27
38
|
requiredPlatforms: Object.freeze(["linux", "macos", "windows"]),
|
|
28
39
|
});
|
|
29
40
|
|
|
41
|
+
// This repository-owned anchor intentionally remains unconfigured until the
|
|
42
|
+
// first public release containing the DELTA lifecycle exporter is published.
|
|
43
|
+
// Updating it requires a reviewed code change with all three exact values.
|
|
44
|
+
export const COMMAND_LIFECYCLE_DELTA_RELEASE_POLICY = Object.freeze({
|
|
45
|
+
schema: "chainlesschain.command-lifecycle-delta-release-policy.v1",
|
|
46
|
+
firstPublicRelease: null,
|
|
47
|
+
incompatibleReleases: Object.freeze(["0.162.198", "0.163.3"]),
|
|
48
|
+
});
|
|
49
|
+
|
|
30
50
|
const REQUIRED_ATTRIBUTES = Object.freeze([
|
|
31
51
|
"command.name",
|
|
32
52
|
"command.route",
|
|
@@ -38,6 +58,87 @@ const REQUIRED_ATTRIBUTES = Object.freeze([
|
|
|
38
58
|
const ALLOWED_ATTRIBUTES = new Set(REQUIRED_ATTRIBUTES);
|
|
39
59
|
const EXACT_RELEASE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
40
60
|
const COMMAND_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
61
|
+
const EVIDENCE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
62
|
+
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
63
|
+
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
64
|
+
const SIGNING_KEY_ID = /^[0-9a-f]{32}$/;
|
|
65
|
+
const APPROVAL_SCOPE = "collector-coverage-and-command-lifecycle-metrics";
|
|
66
|
+
const REPOSITORY_TRUST_SOURCE = "repository-pinned";
|
|
67
|
+
// Repository authority only. Do not accept keys from report data, CLI flags,
|
|
68
|
+
// environment variables, or buildCommandLifecycleReport callers.
|
|
69
|
+
const REPOSITORY_APPROVAL_PUBLIC_KEYS = Object.freeze({});
|
|
70
|
+
const generatorVerificationCache = new Map();
|
|
71
|
+
|
|
72
|
+
const COVERAGE_FIELDS = new Set([
|
|
73
|
+
"schema",
|
|
74
|
+
"reportSchema",
|
|
75
|
+
"decisionVersion",
|
|
76
|
+
"observation",
|
|
77
|
+
"publicReleases",
|
|
78
|
+
"collector",
|
|
79
|
+
]);
|
|
80
|
+
const OBSERVATION_FIELDS = new Set([
|
|
81
|
+
"id",
|
|
82
|
+
"startedAt",
|
|
83
|
+
"endedAt",
|
|
84
|
+
"startRelease",
|
|
85
|
+
"endRelease",
|
|
86
|
+
]);
|
|
87
|
+
const PUBLIC_RELEASE_FIELDS = new Set([
|
|
88
|
+
"version",
|
|
89
|
+
"commitSha",
|
|
90
|
+
"tag",
|
|
91
|
+
"tagPublishedAt",
|
|
92
|
+
"npmPublishedAt",
|
|
93
|
+
]);
|
|
94
|
+
const DELTA_RELEASE_FIELDS = new Set([
|
|
95
|
+
"version",
|
|
96
|
+
"commitSha",
|
|
97
|
+
"npmPublishedAt",
|
|
98
|
+
]);
|
|
99
|
+
const COLLECTOR_FIELDS = new Set([
|
|
100
|
+
"eligibleInstallations",
|
|
101
|
+
"reportingInstallations",
|
|
102
|
+
"sampleRate",
|
|
103
|
+
"platforms",
|
|
104
|
+
"knownBiases",
|
|
105
|
+
]);
|
|
106
|
+
const BIAS_FIELDS = new Set(["id", "description", "blocking"]);
|
|
107
|
+
const EXPORT_MANIFEST_FIELDS = new Set([
|
|
108
|
+
"schema",
|
|
109
|
+
"reportSchema",
|
|
110
|
+
"observationId",
|
|
111
|
+
"generatorSha",
|
|
112
|
+
"policySha256",
|
|
113
|
+
"partitions",
|
|
114
|
+
]);
|
|
115
|
+
const EXPORT_PARTITION_FIELDS = new Set([
|
|
116
|
+
"sequence",
|
|
117
|
+
"id",
|
|
118
|
+
"sha256",
|
|
119
|
+
"startedAt",
|
|
120
|
+
"endedAt",
|
|
121
|
+
"temporality",
|
|
122
|
+
]);
|
|
123
|
+
const TELEMETRY_EXPORT_FIELDS = new Set(["id", "sha256", "documents"]);
|
|
124
|
+
const APPROVAL_FIELDS = new Set([
|
|
125
|
+
"schema",
|
|
126
|
+
"reportSchema",
|
|
127
|
+
"scope",
|
|
128
|
+
"status",
|
|
129
|
+
"attestationId",
|
|
130
|
+
"observationId",
|
|
131
|
+
"decisionVersion",
|
|
132
|
+
"coverageSha256",
|
|
133
|
+
"exportManifestSha256",
|
|
134
|
+
"policySha256",
|
|
135
|
+
"generatorSha",
|
|
136
|
+
"approvedAt",
|
|
137
|
+
"approvedBy",
|
|
138
|
+
"signature",
|
|
139
|
+
]);
|
|
140
|
+
const APPROVED_BY_FIELDS = new Set(["id", "role"]);
|
|
141
|
+
const SIGNATURE_FIELDS = new Set(["algorithm", "keyId", "value"]);
|
|
41
142
|
|
|
42
143
|
function parseRelease(value) {
|
|
43
144
|
if (typeof value !== "string") return null;
|
|
@@ -65,37 +166,211 @@ function compareReleases(left, right) {
|
|
|
65
166
|
return a.prerelease.localeCompare(b.prerelease);
|
|
66
167
|
}
|
|
67
168
|
|
|
169
|
+
function minorForRelease(value) {
|
|
170
|
+
const release = parseRelease(value);
|
|
171
|
+
return release ? `${release.major}.${release.minor}` : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function canonicalIso(value) {
|
|
175
|
+
if (typeof value !== "string") return null;
|
|
176
|
+
const milliseconds = Date.parse(value);
|
|
177
|
+
if (!Number.isFinite(milliseconds)) return null;
|
|
178
|
+
const normalized = new Date(milliseconds).toISOString();
|
|
179
|
+
return normalized === value ? { value: normalized, milliseconds } : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseNano(value) {
|
|
183
|
+
if (typeof value !== "string" && typeof value !== "number") return null;
|
|
184
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) return null;
|
|
185
|
+
if (!/^\d+$/.test(String(value))) return null;
|
|
186
|
+
try {
|
|
187
|
+
const nanoseconds = BigInt(value);
|
|
188
|
+
if (nanoseconds <= 0n) return null;
|
|
189
|
+
return {
|
|
190
|
+
nanoseconds,
|
|
191
|
+
milliseconds: Number(nanoseconds / 1_000_000n),
|
|
192
|
+
};
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function canonicalize(value) {
|
|
199
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
200
|
+
if (!value || typeof value !== "object") return value;
|
|
201
|
+
return Object.fromEntries(
|
|
202
|
+
Object.keys(value)
|
|
203
|
+
.sort()
|
|
204
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sha256Json(value) {
|
|
209
|
+
return `sha256:${createHash("sha256")
|
|
210
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
211
|
+
.digest("hex")}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const COMMAND_ALIAS_DECISION_POLICY_SHA256 = sha256Json({
|
|
215
|
+
aliasDecision: COMMAND_ALIAS_DECISION_POLICY,
|
|
216
|
+
deltaRelease: COMMAND_LIFECYCLE_DELTA_RELEASE_POLICY,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
function isPlainObject(value) {
|
|
220
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
221
|
+
const prototype = Object.getPrototypeOf(value);
|
|
222
|
+
return prototype === Object.prototype || prototype === null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasExactFields(value, fields) {
|
|
226
|
+
return (
|
|
227
|
+
isPlainObject(value) &&
|
|
228
|
+
Object.keys(value).length === fields.size &&
|
|
229
|
+
Object.keys(value).every((key) => fields.has(key)) &&
|
|
230
|
+
[...fields].every((key) => Object.hasOwn(value, key))
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sortedUnique(values) {
|
|
235
|
+
return [...new Set(values)].sort();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function repositoryPathIdentity(value) {
|
|
239
|
+
const resolved = realpathSync.native(value);
|
|
240
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runRepositoryGit(args, cwd) {
|
|
244
|
+
try {
|
|
245
|
+
return executionBroker.spawnSync("git", args, {
|
|
246
|
+
cwd,
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
windowsHide: true,
|
|
249
|
+
origin: "command-lifecycle-report:generator-verification",
|
|
250
|
+
policy: "allow",
|
|
251
|
+
scope: "telemetry",
|
|
252
|
+
shell: false,
|
|
253
|
+
timeout: 10_000,
|
|
254
|
+
maxBuffer: 1024 * 1024,
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
return { status: null, stdout: "", stderr: "" };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function verifyRepositoryGenerator(expectedSha) {
|
|
262
|
+
if (!COMMIT_SHA.test(expectedSha || "")) {
|
|
263
|
+
return { source: null, actualSha: null, verified: false };
|
|
264
|
+
}
|
|
265
|
+
if (generatorVerificationCache.has(expectedSha)) {
|
|
266
|
+
return generatorVerificationCache.get(expectedSha);
|
|
267
|
+
}
|
|
268
|
+
const unavailable = { source: null, actualSha: null, verified: false };
|
|
269
|
+
const packageRoot = path.resolve(import.meta.dirname, "../..");
|
|
270
|
+
const repository = runRepositoryGit(
|
|
271
|
+
["rev-parse", "--show-toplevel"],
|
|
272
|
+
packageRoot,
|
|
273
|
+
);
|
|
274
|
+
if (repository.status !== 0) return unavailable;
|
|
275
|
+
const repositoryRoot = String(repository.stdout || "").trim();
|
|
276
|
+
try {
|
|
277
|
+
if (
|
|
278
|
+
repositoryPathIdentity(packageRoot) !==
|
|
279
|
+
repositoryPathIdentity(path.join(repositoryRoot, "packages", "cli"))
|
|
280
|
+
) {
|
|
281
|
+
return unavailable;
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
return unavailable;
|
|
285
|
+
}
|
|
286
|
+
const head = runRepositoryGit(["rev-parse", "HEAD"], repositoryRoot);
|
|
287
|
+
if (head.status !== 0) return unavailable;
|
|
288
|
+
const actualSha = String(head.stdout || "")
|
|
289
|
+
.trim()
|
|
290
|
+
.toLowerCase();
|
|
291
|
+
const status = runRepositoryGit(
|
|
292
|
+
[
|
|
293
|
+
"status",
|
|
294
|
+
"--porcelain",
|
|
295
|
+
"--untracked-files=normal",
|
|
296
|
+
"--",
|
|
297
|
+
"packages/cli",
|
|
298
|
+
"docs/cli/COMMAND_LIFECYCLE_TELEMETRY.md",
|
|
299
|
+
],
|
|
300
|
+
repositoryRoot,
|
|
301
|
+
);
|
|
302
|
+
const verified =
|
|
303
|
+
status.status === 0 &&
|
|
304
|
+
String(status.stdout || "").trim().length === 0 &&
|
|
305
|
+
actualSha === expectedSha;
|
|
306
|
+
const result = {
|
|
307
|
+
source: verified ? "git-head-clean" : null,
|
|
308
|
+
actualSha: COMMIT_SHA.test(actualSha) ? actualSha : null,
|
|
309
|
+
verified,
|
|
310
|
+
};
|
|
311
|
+
// Cache only failures. A successful clean-tree check is an authority fact
|
|
312
|
+
// that must be re-established for every report in a long-lived process.
|
|
313
|
+
if (!verified) generatorVerificationCache.set(expectedSha, result);
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
68
317
|
function otlpValue(value) {
|
|
69
318
|
if (!value || typeof value !== "object") return undefined;
|
|
70
319
|
for (const key of ["stringValue", "intValue", "doubleValue", "boolValue"]) {
|
|
71
320
|
if (Object.hasOwn(value, key)) return value[key];
|
|
72
321
|
}
|
|
322
|
+
if (value.arrayValue && Array.isArray(value.arrayValue.values)) {
|
|
323
|
+
return value.arrayValue.values.map(otlpValue);
|
|
324
|
+
}
|
|
325
|
+
if (value.kvlistValue && Array.isArray(value.kvlistValue.values)) {
|
|
326
|
+
return Object.fromEntries(
|
|
327
|
+
value.kvlistValue.values
|
|
328
|
+
.filter((entry) => typeof entry?.key === "string")
|
|
329
|
+
.map((entry) => [entry.key, otlpValue(entry.value)]),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (typeof value.bytesValue === "string") return value.bytesValue;
|
|
73
333
|
return undefined;
|
|
74
334
|
}
|
|
75
335
|
|
|
76
|
-
function
|
|
77
|
-
if (!Array.isArray(
|
|
336
|
+
function attributeMap(items, { lifecycle = false } = {}) {
|
|
337
|
+
if (!Array.isArray(items)) return lifecycle ? null : {};
|
|
78
338
|
const attributes = {};
|
|
79
|
-
for (const item of
|
|
339
|
+
for (const item of items) {
|
|
80
340
|
if (
|
|
81
341
|
!item ||
|
|
82
342
|
typeof item.key !== "string" ||
|
|
83
|
-
!ALLOWED_ATTRIBUTES.has(item.key) ||
|
|
343
|
+
(lifecycle && !ALLOWED_ATTRIBUTES.has(item.key)) ||
|
|
84
344
|
Object.hasOwn(attributes, item.key)
|
|
85
345
|
) {
|
|
86
346
|
return null;
|
|
87
347
|
}
|
|
88
|
-
|
|
348
|
+
const value = otlpValue(item.value);
|
|
349
|
+
if (value === undefined) return null;
|
|
350
|
+
attributes[item.key] = value;
|
|
89
351
|
}
|
|
90
352
|
return attributes;
|
|
91
353
|
}
|
|
92
354
|
|
|
93
|
-
function pointCount(point) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
355
|
+
function pointCount(point, { allowZero = false } = {}) {
|
|
356
|
+
const hasInt = Object.hasOwn(point || {}, "asInt");
|
|
357
|
+
const hasDouble = Object.hasOwn(point || {}, "asDouble");
|
|
358
|
+
if (hasInt === hasDouble) return null;
|
|
359
|
+
const raw = hasInt ? point.asInt : point.asDouble;
|
|
360
|
+
if (
|
|
361
|
+
(hasInt &&
|
|
362
|
+
!(
|
|
363
|
+
(typeof raw === "string" && /^\d+$/.test(raw)) ||
|
|
364
|
+
Number.isSafeInteger(raw)
|
|
365
|
+
)) ||
|
|
366
|
+
(hasDouble && typeof raw !== "number")
|
|
367
|
+
) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
97
370
|
const value = Number(raw);
|
|
98
|
-
return Number.isSafeInteger(value) && value
|
|
371
|
+
return Number.isSafeInteger(value) && (allowZero ? value >= 0 : value > 0)
|
|
372
|
+
? value
|
|
373
|
+
: null;
|
|
99
374
|
}
|
|
100
375
|
|
|
101
376
|
function metricPayloads(document) {
|
|
@@ -120,6 +395,10 @@ function lifecycleEntries(manifest) {
|
|
|
120
395
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
121
396
|
}
|
|
122
397
|
|
|
398
|
+
function blankCounts() {
|
|
399
|
+
return { invocations: 0, completed: 0, error: 0 };
|
|
400
|
+
}
|
|
401
|
+
|
|
123
402
|
function blankCommand(entry) {
|
|
124
403
|
return {
|
|
125
404
|
command: entry.name,
|
|
@@ -127,55 +406,247 @@ function blankCommand(entry) {
|
|
|
127
406
|
deprecatedSince: entry.lifecycle.deprecatedSince,
|
|
128
407
|
removalNotBefore: entry.lifecycle.removalNotBefore,
|
|
129
408
|
minimumReleaseCycles: entry.lifecycle.minimumReleaseCycles,
|
|
130
|
-
legacy:
|
|
131
|
-
replacementRoute:
|
|
409
|
+
legacy: blankCounts(),
|
|
410
|
+
replacementRoute: blankCounts(),
|
|
132
411
|
versions: new Set(),
|
|
412
|
+
minors: new Map(),
|
|
133
413
|
};
|
|
134
414
|
}
|
|
135
415
|
|
|
136
|
-
function
|
|
416
|
+
function addInvocation(command, record, count) {
|
|
417
|
+
const route =
|
|
418
|
+
record.route === "legacy" ? command.legacy : command.replacementRoute;
|
|
419
|
+
route.invocations += count;
|
|
420
|
+
route[record.outcome] += count;
|
|
421
|
+
command.versions.add(record.version);
|
|
422
|
+
|
|
423
|
+
const minor = minorForRelease(record.version);
|
|
424
|
+
if (!command.minors.has(minor)) {
|
|
425
|
+
command.minors.set(minor, {
|
|
426
|
+
minor,
|
|
427
|
+
legacy: blankCounts(),
|
|
428
|
+
replacementRoute: blankCounts(),
|
|
429
|
+
versions: new Set(),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const minorBucket = command.minors.get(minor);
|
|
433
|
+
const minorRoute =
|
|
434
|
+
record.route === "legacy"
|
|
435
|
+
? minorBucket.legacy
|
|
436
|
+
: minorBucket.replacementRoute;
|
|
437
|
+
minorRoute.invocations += count;
|
|
438
|
+
minorRoute[record.outcome] += count;
|
|
439
|
+
minorBucket.versions.add(record.version);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function releaseTag(version) {
|
|
443
|
+
return `v-npm-${version.replaceAll(".", "-")}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function validateCoverage(coverage, policy, generatedAt) {
|
|
137
447
|
const blockers = [];
|
|
448
|
+
if (!hasExactFields(coverage, COVERAGE_FIELDS)) {
|
|
449
|
+
blockers.push("coverage-fields-invalid");
|
|
450
|
+
}
|
|
138
451
|
if (coverage?.schema !== COMMAND_LIFECYCLE_COVERAGE_SCHEMA) {
|
|
139
452
|
blockers.push("coverage-schema-invalid");
|
|
140
453
|
}
|
|
454
|
+
if (coverage?.reportSchema !== COMMAND_LIFECYCLE_REPORT_SCHEMA) {
|
|
455
|
+
blockers.push("coverage-report-schema-unbound");
|
|
456
|
+
}
|
|
141
457
|
|
|
142
458
|
const decisionVersion = parseRelease(coverage?.decisionVersion);
|
|
143
|
-
if (!decisionVersion
|
|
459
|
+
if (!decisionVersion || decisionVersion.prerelease !== null) {
|
|
460
|
+
blockers.push("decision-version-invalid");
|
|
461
|
+
}
|
|
144
462
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
463
|
+
const observation = coverage?.observation || {};
|
|
464
|
+
if (!hasExactFields(observation, OBSERVATION_FIELDS)) {
|
|
465
|
+
blockers.push("observation-fields-invalid");
|
|
466
|
+
}
|
|
467
|
+
const observationId =
|
|
468
|
+
typeof observation.id === "string" && EVIDENCE_ID.test(observation.id)
|
|
469
|
+
? observation.id
|
|
470
|
+
: null;
|
|
471
|
+
if (!observationId) blockers.push("observation-id-invalid");
|
|
472
|
+
const startedAt = canonicalIso(observation.startedAt);
|
|
473
|
+
const endedAt = canonicalIso(observation.endedAt);
|
|
474
|
+
if (
|
|
475
|
+
!startedAt ||
|
|
476
|
+
!endedAt ||
|
|
477
|
+
endedAt.milliseconds <= startedAt.milliseconds
|
|
478
|
+
) {
|
|
479
|
+
blockers.push("observation-window-invalid");
|
|
480
|
+
}
|
|
481
|
+
if (
|
|
482
|
+
endedAt &&
|
|
483
|
+
generatedAt &&
|
|
484
|
+
generatedAt.milliseconds < endedAt.milliseconds
|
|
485
|
+
) {
|
|
486
|
+
blockers.push("report-generated-before-window-end");
|
|
487
|
+
}
|
|
488
|
+
const startRelease = parseRelease(observation.startRelease);
|
|
489
|
+
const endRelease = parseRelease(observation.endRelease);
|
|
148
490
|
if (
|
|
149
|
-
|
|
150
|
-
|
|
491
|
+
!startRelease ||
|
|
492
|
+
!endRelease ||
|
|
493
|
+
startRelease.prerelease !== null ||
|
|
494
|
+
endRelease.prerelease !== null ||
|
|
495
|
+
compareReleases(startRelease, endRelease) > 0
|
|
151
496
|
) {
|
|
152
|
-
blockers.push("
|
|
497
|
+
blockers.push("observation-release-bounds-invalid");
|
|
153
498
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
499
|
+
if (
|
|
500
|
+
decisionVersion &&
|
|
501
|
+
endRelease &&
|
|
502
|
+
compareReleases(decisionVersion, endRelease) !== 0
|
|
503
|
+
) {
|
|
504
|
+
blockers.push("decision-version-not-bound-to-window-end-release");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const releases = [];
|
|
508
|
+
const releaseVersions = new Set();
|
|
509
|
+
const releaseTags = new Set();
|
|
510
|
+
let previousRelease = null;
|
|
511
|
+
let previousNpmTime = null;
|
|
512
|
+
for (const item of Array.isArray(coverage?.publicReleases)
|
|
513
|
+
? coverage.publicReleases
|
|
514
|
+
: []) {
|
|
515
|
+
const version = parseRelease(item?.version);
|
|
516
|
+
const tagPublishedAt = canonicalIso(item?.tagPublishedAt);
|
|
517
|
+
const npmPublishedAt = canonicalIso(item?.npmPublishedAt);
|
|
518
|
+
const valid =
|
|
519
|
+
hasExactFields(item, PUBLIC_RELEASE_FIELDS) &&
|
|
520
|
+
version &&
|
|
521
|
+
version.prerelease === null &&
|
|
522
|
+
typeof item?.commitSha === "string" &&
|
|
523
|
+
COMMIT_SHA.test(item.commitSha) &&
|
|
524
|
+
item?.tag === releaseTag(version.value) &&
|
|
525
|
+
tagPublishedAt &&
|
|
526
|
+
npmPublishedAt &&
|
|
527
|
+
tagPublishedAt.milliseconds <= npmPublishedAt.milliseconds &&
|
|
528
|
+
!releaseVersions.has(version.value) &&
|
|
529
|
+
!releaseTags.has(item.tag);
|
|
530
|
+
if (!valid) {
|
|
531
|
+
blockers.push("public-release-entry-invalid");
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (
|
|
535
|
+
previousRelease &&
|
|
536
|
+
(compareReleases(previousRelease, version) >= 0 ||
|
|
537
|
+
previousNpmTime >= npmPublishedAt.milliseconds)
|
|
538
|
+
) {
|
|
539
|
+
blockers.push("public-releases-not-strictly-ordered");
|
|
540
|
+
}
|
|
541
|
+
if (
|
|
542
|
+
(startRelease && compareReleases(version, startRelease) < 0) ||
|
|
543
|
+
(endRelease && compareReleases(version, endRelease) > 0) ||
|
|
544
|
+
(startedAt && npmPublishedAt.milliseconds < startedAt.milliseconds) ||
|
|
545
|
+
(endedAt && npmPublishedAt.milliseconds > endedAt.milliseconds)
|
|
546
|
+
) {
|
|
547
|
+
blockers.push("public-release-outside-observation-window");
|
|
548
|
+
}
|
|
549
|
+
releases.push({
|
|
550
|
+
version: version.value,
|
|
551
|
+
minor: minorForRelease(version.value),
|
|
552
|
+
commitSha: item.commitSha,
|
|
553
|
+
tag: item.tag,
|
|
554
|
+
tagPublishedAt: tagPublishedAt.value,
|
|
555
|
+
npmPublishedAt: npmPublishedAt.value,
|
|
556
|
+
npmPublishedAtMs: npmPublishedAt.milliseconds,
|
|
557
|
+
});
|
|
558
|
+
releaseVersions.add(version.value);
|
|
559
|
+
releaseTags.add(item.tag);
|
|
560
|
+
previousRelease = version;
|
|
561
|
+
previousNpmTime = npmPublishedAt.milliseconds;
|
|
562
|
+
}
|
|
563
|
+
if (!Array.isArray(coverage?.publicReleases)) {
|
|
564
|
+
blockers.push("public-releases-invalid");
|
|
565
|
+
}
|
|
566
|
+
if (releases.length === 0) blockers.push("public-releases-missing");
|
|
567
|
+
|
|
568
|
+
for (const release of releases) {
|
|
569
|
+
if (
|
|
570
|
+
COMMAND_LIFECYCLE_DELTA_RELEASE_POLICY.incompatibleReleases.includes(
|
|
571
|
+
release.version,
|
|
572
|
+
)
|
|
573
|
+
) {
|
|
574
|
+
blockers.push(`delta-evidence-release-incompatible:${release.version}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const configuredDeltaRelease =
|
|
578
|
+
COMMAND_LIFECYCLE_DELTA_RELEASE_POLICY.firstPublicRelease;
|
|
579
|
+
const deltaVersion = parseRelease(configuredDeltaRelease?.version);
|
|
580
|
+
const deltaNpmPublishedAt = canonicalIso(
|
|
581
|
+
configuredDeltaRelease?.npmPublishedAt,
|
|
158
582
|
);
|
|
159
|
-
|
|
160
|
-
|
|
583
|
+
const deltaPolicyConfigured =
|
|
584
|
+
hasExactFields(configuredDeltaRelease, DELTA_RELEASE_FIELDS) &&
|
|
585
|
+
deltaVersion &&
|
|
586
|
+
deltaVersion.prerelease === null &&
|
|
587
|
+
COMMIT_SHA.test(configuredDeltaRelease.commitSha || "") &&
|
|
588
|
+
deltaNpmPublishedAt;
|
|
589
|
+
if (!deltaPolicyConfigured) {
|
|
590
|
+
blockers.push("delta-release-policy-unconfigured");
|
|
591
|
+
} else {
|
|
592
|
+
if (startRelease?.value !== deltaVersion.value) {
|
|
593
|
+
blockers.push("observation-start-release-not-delta-anchor");
|
|
594
|
+
}
|
|
595
|
+
const anchoredRelease = releases.find(
|
|
596
|
+
(release) => release.version === deltaVersion.value,
|
|
597
|
+
);
|
|
598
|
+
if (
|
|
599
|
+
!anchoredRelease ||
|
|
600
|
+
anchoredRelease.commitSha !== configuredDeltaRelease.commitSha ||
|
|
601
|
+
anchoredRelease.npmPublishedAt !== deltaNpmPublishedAt.value
|
|
602
|
+
) {
|
|
603
|
+
blockers.push("delta-release-evidence-mismatch");
|
|
604
|
+
}
|
|
161
605
|
}
|
|
162
606
|
|
|
163
|
-
const
|
|
164
|
-
|
|
607
|
+
const startEvidence = releases.find(
|
|
608
|
+
(release) => release.version === startRelease?.value,
|
|
609
|
+
);
|
|
610
|
+
const endEvidence = releases.find(
|
|
611
|
+
(release) => release.version === endRelease?.value,
|
|
612
|
+
);
|
|
613
|
+
if (!startEvidence || !endEvidence) {
|
|
614
|
+
blockers.push("observation-release-evidence-missing");
|
|
615
|
+
}
|
|
165
616
|
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
617
|
+
startEvidence &&
|
|
618
|
+
startedAt &&
|
|
619
|
+
startEvidence.npmPublishedAt !== startedAt.value
|
|
169
620
|
) {
|
|
170
|
-
blockers.push("observation-
|
|
621
|
+
blockers.push("observation-start-not-bound-to-npm-release");
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
endEvidence &&
|
|
625
|
+
endedAt &&
|
|
626
|
+
endEvidence.npmPublishedAtMs > endedAt.milliseconds
|
|
627
|
+
) {
|
|
628
|
+
blockers.push("observation-end-precedes-npm-release");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const observedMinorCycles = [
|
|
632
|
+
...new Set(releases.map((release) => release.minor)),
|
|
633
|
+
];
|
|
634
|
+
if (observedMinorCycles.length < policy.minimumObservedMinorCycles) {
|
|
635
|
+
blockers.push("release-observation-window-incomplete");
|
|
171
636
|
}
|
|
172
637
|
|
|
173
638
|
const collector = coverage?.collector || {};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
639
|
+
if (!hasExactFields(collector, COLLECTOR_FIELDS)) {
|
|
640
|
+
blockers.push("collector-fields-invalid");
|
|
641
|
+
}
|
|
642
|
+
const eligibleInstallations = collector.eligibleInstallations;
|
|
643
|
+
const reportingInstallations = collector.reportingInstallations;
|
|
644
|
+
const sampleRate = collector.sampleRate;
|
|
177
645
|
const coverageRatio =
|
|
178
|
-
Number.
|
|
646
|
+
Number.isSafeInteger(eligibleInstallations) &&
|
|
647
|
+
eligibleInstallations > 0 &&
|
|
648
|
+
Number.isSafeInteger(reportingInstallations) &&
|
|
649
|
+
reportingInstallations >= 0
|
|
179
650
|
? reportingInstallations / eligibleInstallations
|
|
180
651
|
: 0;
|
|
181
652
|
if (
|
|
@@ -195,6 +666,7 @@ function validateCoverage(coverage, policy) {
|
|
|
195
666
|
blockers.push("collector-coverage-too-low");
|
|
196
667
|
}
|
|
197
668
|
if (
|
|
669
|
+
typeof sampleRate !== "number" ||
|
|
198
670
|
!Number.isFinite(sampleRate) ||
|
|
199
671
|
sampleRate < policy.minimumSampleRate ||
|
|
200
672
|
sampleRate > 1
|
|
@@ -203,26 +675,49 @@ function validateCoverage(coverage, policy) {
|
|
|
203
675
|
}
|
|
204
676
|
|
|
205
677
|
const platforms = collector.platforms || {};
|
|
678
|
+
if (
|
|
679
|
+
!hasExactFields(platforms, new Set(policy.requiredPlatforms)) ||
|
|
680
|
+
Object.keys(platforms).some(
|
|
681
|
+
(platform) => !policy.requiredPlatforms.includes(platform),
|
|
682
|
+
)
|
|
683
|
+
) {
|
|
684
|
+
blockers.push("collector-platform-set-invalid");
|
|
685
|
+
}
|
|
686
|
+
let platformInstallationTotal = 0;
|
|
206
687
|
for (const platform of policy.requiredPlatforms) {
|
|
207
688
|
if (
|
|
208
689
|
!Number.isSafeInteger(platforms[platform]) ||
|
|
209
690
|
platforms[platform] <= 0
|
|
210
691
|
) {
|
|
211
692
|
blockers.push(`platform-coverage-missing:${platform}`);
|
|
693
|
+
} else {
|
|
694
|
+
platformInstallationTotal += platforms[platform];
|
|
212
695
|
}
|
|
213
696
|
}
|
|
697
|
+
if (
|
|
698
|
+
!Number.isSafeInteger(reportingInstallations) ||
|
|
699
|
+
platformInstallationTotal !== reportingInstallations
|
|
700
|
+
) {
|
|
701
|
+
blockers.push("platform-reporting-installation-total-mismatch");
|
|
702
|
+
}
|
|
214
703
|
|
|
215
704
|
const knownBiases = Array.isArray(collector.knownBiases)
|
|
216
705
|
? collector.knownBiases
|
|
217
706
|
: [];
|
|
707
|
+
if (!Array.isArray(collector.knownBiases)) {
|
|
708
|
+
blockers.push("collector-bias-list-invalid");
|
|
709
|
+
}
|
|
218
710
|
if (knownBiases.length === 0) {
|
|
219
711
|
blockers.push("collector-bias-not-documented");
|
|
220
712
|
}
|
|
221
713
|
for (const bias of knownBiases) {
|
|
222
714
|
if (
|
|
223
|
-
!bias ||
|
|
715
|
+
!hasExactFields(bias, BIAS_FIELDS) ||
|
|
224
716
|
typeof bias.id !== "string" ||
|
|
225
|
-
|
|
717
|
+
!EVIDENCE_ID.test(bias.id) ||
|
|
718
|
+
typeof bias.description !== "string" ||
|
|
719
|
+
bias.description.trim().length === 0 ||
|
|
720
|
+
typeof bias.blocking !== "boolean"
|
|
226
721
|
) {
|
|
227
722
|
blockers.push("collector-bias-entry-invalid");
|
|
228
723
|
continue;
|
|
@@ -235,17 +730,47 @@ function validateCoverage(coverage, policy) {
|
|
|
235
730
|
return {
|
|
236
731
|
ready: blockers.length === 0,
|
|
237
732
|
blockers: [...new Set(blockers)].sort(),
|
|
733
|
+
reportSchema:
|
|
734
|
+
coverage?.reportSchema === COMMAND_LIFECYCLE_REPORT_SCHEMA
|
|
735
|
+
? coverage.reportSchema
|
|
736
|
+
: null,
|
|
238
737
|
decisionVersion: decisionVersion?.value || null,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
738
|
+
observation: {
|
|
739
|
+
id: observationId,
|
|
740
|
+
startedAt: startedAt?.value || null,
|
|
741
|
+
endedAt: endedAt?.value || null,
|
|
742
|
+
startRelease: startRelease?.value || null,
|
|
743
|
+
endRelease: endRelease?.value || null,
|
|
744
|
+
},
|
|
745
|
+
publicReleases: releases.map((release) => ({
|
|
746
|
+
version: release.version,
|
|
747
|
+
minor: release.minor,
|
|
748
|
+
commitSha: release.commitSha,
|
|
749
|
+
tag: release.tag,
|
|
750
|
+
tagPublishedAt: release.tagPublishedAt,
|
|
751
|
+
npmPublishedAt: release.npmPublishedAt,
|
|
752
|
+
})),
|
|
753
|
+
releaseByVersion: new Map(
|
|
754
|
+
releases.map((release) => [release.version, release]),
|
|
755
|
+
),
|
|
756
|
+
observedMinorCycles,
|
|
757
|
+
deltaReleasePolicy: {
|
|
758
|
+
configured: Boolean(deltaPolicyConfigured),
|
|
759
|
+
firstPublicRelease: deltaPolicyConfigured
|
|
760
|
+
? {
|
|
761
|
+
version: deltaVersion.value,
|
|
762
|
+
commitSha: configuredDeltaRelease.commitSha,
|
|
763
|
+
npmPublishedAt: deltaNpmPublishedAt.value,
|
|
764
|
+
}
|
|
246
765
|
: null,
|
|
247
|
-
|
|
766
|
+
incompatibleReleases: [
|
|
767
|
+
...COMMAND_LIFECYCLE_DELTA_RELEASE_POLICY.incompatibleReleases,
|
|
768
|
+
],
|
|
248
769
|
},
|
|
770
|
+
windowMs:
|
|
771
|
+
startedAt && endedAt
|
|
772
|
+
? { startedAt: startedAt.milliseconds, endedAt: endedAt.milliseconds }
|
|
773
|
+
: null,
|
|
249
774
|
collector: {
|
|
250
775
|
eligibleInstallations:
|
|
251
776
|
Number.isSafeInteger(eligibleInstallations) &&
|
|
@@ -267,6 +792,7 @@ function validateCoverage(coverage, policy) {
|
|
|
267
792
|
: 0,
|
|
268
793
|
]),
|
|
269
794
|
),
|
|
795
|
+
platformInstallationTotal,
|
|
270
796
|
knownBiases: knownBiases
|
|
271
797
|
.filter(
|
|
272
798
|
(bias) =>
|
|
@@ -283,7 +809,613 @@ function validateCoverage(coverage, policy) {
|
|
|
283
809
|
};
|
|
284
810
|
}
|
|
285
811
|
|
|
286
|
-
function
|
|
812
|
+
function validateTelemetryExports({
|
|
813
|
+
telemetryExports,
|
|
814
|
+
exportManifest,
|
|
815
|
+
exportManifestSha256,
|
|
816
|
+
coverage,
|
|
817
|
+
policySha256,
|
|
818
|
+
generatorSha,
|
|
819
|
+
generatorVerification,
|
|
820
|
+
}) {
|
|
821
|
+
const blockers = [];
|
|
822
|
+
if (!hasExactFields(exportManifest, EXPORT_MANIFEST_FIELDS)) {
|
|
823
|
+
blockers.push("export-manifest-fields-invalid");
|
|
824
|
+
}
|
|
825
|
+
if (exportManifest?.schema !== COMMAND_LIFECYCLE_EXPORT_MANIFEST_SCHEMA) {
|
|
826
|
+
blockers.push("export-manifest-schema-invalid");
|
|
827
|
+
}
|
|
828
|
+
if (exportManifest?.reportSchema !== COMMAND_LIFECYCLE_REPORT_SCHEMA) {
|
|
829
|
+
blockers.push("export-manifest-report-schema-unbound");
|
|
830
|
+
}
|
|
831
|
+
if (exportManifest?.observationId !== coverage.observation.id) {
|
|
832
|
+
blockers.push("export-manifest-observation-mismatch");
|
|
833
|
+
}
|
|
834
|
+
if (
|
|
835
|
+
typeof exportManifestSha256 !== "string" ||
|
|
836
|
+
!SHA256.test(exportManifestSha256)
|
|
837
|
+
) {
|
|
838
|
+
blockers.push("export-manifest-file-digest-invalid");
|
|
839
|
+
}
|
|
840
|
+
if (
|
|
841
|
+
typeof generatorSha !== "string" ||
|
|
842
|
+
!COMMIT_SHA.test(generatorSha) ||
|
|
843
|
+
exportManifest?.generatorSha !== generatorSha
|
|
844
|
+
) {
|
|
845
|
+
blockers.push("export-manifest-generator-sha-mismatch");
|
|
846
|
+
}
|
|
847
|
+
if (
|
|
848
|
+
generatorVerification?.source !== "git-head-clean" ||
|
|
849
|
+
generatorVerification?.verified !== true ||
|
|
850
|
+
generatorVerification?.actualSha !== generatorSha
|
|
851
|
+
) {
|
|
852
|
+
blockers.push("report-generator-source-unverified");
|
|
853
|
+
}
|
|
854
|
+
if (exportManifest?.policySha256 !== policySha256) {
|
|
855
|
+
blockers.push("export-manifest-policy-digest-mismatch");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const partitions = [];
|
|
859
|
+
const ids = new Set();
|
|
860
|
+
const digests = new Set();
|
|
861
|
+
let previousEnd = null;
|
|
862
|
+
const manifestPartitions = Array.isArray(exportManifest?.partitions)
|
|
863
|
+
? exportManifest.partitions
|
|
864
|
+
: [];
|
|
865
|
+
if (!Array.isArray(exportManifest?.partitions)) {
|
|
866
|
+
blockers.push("export-manifest-partitions-invalid");
|
|
867
|
+
}
|
|
868
|
+
if (manifestPartitions.length === 0) {
|
|
869
|
+
blockers.push("export-manifest-partitions-missing");
|
|
870
|
+
}
|
|
871
|
+
for (let index = 0; index < manifestPartitions.length; index += 1) {
|
|
872
|
+
const item = manifestPartitions[index];
|
|
873
|
+
const startedAt = canonicalIso(item?.startedAt);
|
|
874
|
+
const endedAt = canonicalIso(item?.endedAt);
|
|
875
|
+
const valid =
|
|
876
|
+
hasExactFields(item, EXPORT_PARTITION_FIELDS) &&
|
|
877
|
+
item.sequence === index &&
|
|
878
|
+
typeof item.id === "string" &&
|
|
879
|
+
EVIDENCE_ID.test(item.id) &&
|
|
880
|
+
typeof item.sha256 === "string" &&
|
|
881
|
+
SHA256.test(item.sha256) &&
|
|
882
|
+
startedAt &&
|
|
883
|
+
endedAt &&
|
|
884
|
+
startedAt.milliseconds < endedAt.milliseconds &&
|
|
885
|
+
["delta", "cumulative"].includes(item.temporality);
|
|
886
|
+
if (!valid) {
|
|
887
|
+
blockers.push("export-manifest-partition-invalid");
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (ids.has(item.id)) blockers.push("duplicate-telemetry-export-id");
|
|
891
|
+
if (digests.has(item.sha256)) {
|
|
892
|
+
blockers.push("duplicate-telemetry-export-digest");
|
|
893
|
+
}
|
|
894
|
+
if (previousEnd !== null && startedAt.milliseconds !== previousEnd) {
|
|
895
|
+
blockers.push(
|
|
896
|
+
startedAt.milliseconds < previousEnd
|
|
897
|
+
? "export-manifest-partitions-overlap"
|
|
898
|
+
: "export-manifest-partition-gap",
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (
|
|
902
|
+
coverage.windowMs &&
|
|
903
|
+
(startedAt.milliseconds < coverage.windowMs.startedAt ||
|
|
904
|
+
endedAt.milliseconds > coverage.windowMs.endedAt)
|
|
905
|
+
) {
|
|
906
|
+
blockers.push("export-manifest-partition-outside-observation-window");
|
|
907
|
+
}
|
|
908
|
+
ids.add(item.id);
|
|
909
|
+
digests.add(item.sha256);
|
|
910
|
+
previousEnd = endedAt.milliseconds;
|
|
911
|
+
partitions.push({
|
|
912
|
+
sequence: item.sequence,
|
|
913
|
+
id: item.id,
|
|
914
|
+
sha256: item.sha256,
|
|
915
|
+
startedAt: startedAt.value,
|
|
916
|
+
endedAt: endedAt.value,
|
|
917
|
+
startedAtMs: startedAt.milliseconds,
|
|
918
|
+
endedAtMs: endedAt.milliseconds,
|
|
919
|
+
temporality: item.temporality,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (
|
|
923
|
+
coverage.windowMs &&
|
|
924
|
+
(partitions[0]?.startedAtMs !== coverage.windowMs.startedAt ||
|
|
925
|
+
partitions.at(-1)?.endedAtMs !== coverage.windowMs.endedAt)
|
|
926
|
+
) {
|
|
927
|
+
blockers.push("export-manifest-does-not-cover-observation-window");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const exports = Array.isArray(telemetryExports) ? telemetryExports : [];
|
|
931
|
+
if (!Array.isArray(telemetryExports)) {
|
|
932
|
+
blockers.push("telemetry-export-set-invalid");
|
|
933
|
+
}
|
|
934
|
+
if (exports.length !== manifestPartitions.length) {
|
|
935
|
+
blockers.push("telemetry-export-set-does-not-match-manifest");
|
|
936
|
+
}
|
|
937
|
+
const normalized = [];
|
|
938
|
+
for (let index = 0; index < exports.length; index += 1) {
|
|
939
|
+
const item = exports[index];
|
|
940
|
+
const partition = partitions.find((entry) => entry.sequence === index);
|
|
941
|
+
if (
|
|
942
|
+
!hasExactFields(item, TELEMETRY_EXPORT_FIELDS) ||
|
|
943
|
+
typeof item?.id !== "string" ||
|
|
944
|
+
!EVIDENCE_ID.test(item.id) ||
|
|
945
|
+
typeof item?.sha256 !== "string" ||
|
|
946
|
+
!SHA256.test(item.sha256) ||
|
|
947
|
+
!Array.isArray(item?.documents)
|
|
948
|
+
) {
|
|
949
|
+
blockers.push("telemetry-export-evidence-invalid");
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (
|
|
953
|
+
!partition ||
|
|
954
|
+
partition.id !== item.id ||
|
|
955
|
+
partition.sha256 !== item.sha256
|
|
956
|
+
) {
|
|
957
|
+
blockers.push("telemetry-export-set-does-not-match-manifest");
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
normalized.push({
|
|
961
|
+
id: item.id,
|
|
962
|
+
sha256: item.sha256,
|
|
963
|
+
documents: item.documents,
|
|
964
|
+
partition,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
blockers: sortedUnique(blockers),
|
|
969
|
+
exports: normalized,
|
|
970
|
+
manifest: {
|
|
971
|
+
schema:
|
|
972
|
+
exportManifest?.schema === COMMAND_LIFECYCLE_EXPORT_MANIFEST_SCHEMA
|
|
973
|
+
? exportManifest.schema
|
|
974
|
+
: null,
|
|
975
|
+
sha256:
|
|
976
|
+
typeof exportManifestSha256 === "string" &&
|
|
977
|
+
SHA256.test(exportManifestSha256)
|
|
978
|
+
? exportManifestSha256
|
|
979
|
+
: null,
|
|
980
|
+
observationId:
|
|
981
|
+
typeof exportManifest?.observationId === "string"
|
|
982
|
+
? exportManifest.observationId
|
|
983
|
+
: null,
|
|
984
|
+
generatorSha:
|
|
985
|
+
typeof exportManifest?.generatorSha === "string"
|
|
986
|
+
? exportManifest.generatorSha
|
|
987
|
+
: null,
|
|
988
|
+
policySha256:
|
|
989
|
+
typeof exportManifest?.policySha256 === "string"
|
|
990
|
+
? exportManifest.policySha256
|
|
991
|
+
: null,
|
|
992
|
+
generatorVerification: {
|
|
993
|
+
source:
|
|
994
|
+
generatorVerification?.source === "git-head-clean"
|
|
995
|
+
? generatorVerification.source
|
|
996
|
+
: null,
|
|
997
|
+
actualSha: COMMIT_SHA.test(generatorVerification?.actualSha || "")
|
|
998
|
+
? generatorVerification.actualSha
|
|
999
|
+
: null,
|
|
1000
|
+
verified: generatorVerification?.verified === true,
|
|
1001
|
+
},
|
|
1002
|
+
partitions,
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function repositoryApprovalKey(keyId) {
|
|
1008
|
+
if (
|
|
1009
|
+
!SIGNING_KEY_ID.test(keyId || "") ||
|
|
1010
|
+
!Object.hasOwn(REPOSITORY_APPROVAL_PUBLIC_KEYS, keyId)
|
|
1011
|
+
) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
return REPOSITORY_APPROVAL_PUBLIC_KEYS[keyId];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function validateApproval({
|
|
1018
|
+
approval,
|
|
1019
|
+
approvalSha256,
|
|
1020
|
+
coverageSha256,
|
|
1021
|
+
exportManifestSha256,
|
|
1022
|
+
coverage,
|
|
1023
|
+
policySha256,
|
|
1024
|
+
generatorSha,
|
|
1025
|
+
generatedAt,
|
|
1026
|
+
now,
|
|
1027
|
+
}) {
|
|
1028
|
+
const blockers = [];
|
|
1029
|
+
if (!hasExactFields(approval, APPROVAL_FIELDS)) {
|
|
1030
|
+
blockers.push("approval-attestation-missing");
|
|
1031
|
+
if (approval && typeof approval === "object") {
|
|
1032
|
+
blockers.push("approval-fields-invalid");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (approval?.schema !== COMMAND_LIFECYCLE_APPROVAL_SCHEMA) {
|
|
1036
|
+
blockers.push("approval-schema-invalid");
|
|
1037
|
+
}
|
|
1038
|
+
if (approval?.reportSchema !== COMMAND_LIFECYCLE_REPORT_SCHEMA) {
|
|
1039
|
+
blockers.push("approval-report-schema-unbound");
|
|
1040
|
+
}
|
|
1041
|
+
if (approval?.scope !== APPROVAL_SCOPE) {
|
|
1042
|
+
blockers.push("approval-scope-invalid");
|
|
1043
|
+
}
|
|
1044
|
+
if (approval?.status !== "approved") {
|
|
1045
|
+
blockers.push("approval-not-approved");
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof approvalSha256 !== "string" || !SHA256.test(approvalSha256)) {
|
|
1048
|
+
blockers.push("approval-file-digest-invalid");
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof coverageSha256 !== "string" || !SHA256.test(coverageSha256)) {
|
|
1051
|
+
blockers.push("coverage-file-digest-invalid");
|
|
1052
|
+
}
|
|
1053
|
+
if (approval?.coverageSha256 !== coverageSha256) {
|
|
1054
|
+
blockers.push("approval-coverage-digest-mismatch");
|
|
1055
|
+
}
|
|
1056
|
+
if (approval?.exportManifestSha256 !== exportManifestSha256) {
|
|
1057
|
+
blockers.push("approval-export-manifest-digest-mismatch");
|
|
1058
|
+
}
|
|
1059
|
+
if (approval?.policySha256 !== policySha256) {
|
|
1060
|
+
blockers.push("approval-policy-digest-mismatch");
|
|
1061
|
+
}
|
|
1062
|
+
if (
|
|
1063
|
+
typeof generatorSha !== "string" ||
|
|
1064
|
+
!COMMIT_SHA.test(generatorSha) ||
|
|
1065
|
+
approval?.generatorSha !== generatorSha
|
|
1066
|
+
) {
|
|
1067
|
+
blockers.push("approval-generator-sha-mismatch");
|
|
1068
|
+
}
|
|
1069
|
+
if (approval?.observationId !== coverage.observation.id) {
|
|
1070
|
+
blockers.push("approval-observation-mismatch");
|
|
1071
|
+
}
|
|
1072
|
+
if (approval?.decisionVersion !== coverage.decisionVersion) {
|
|
1073
|
+
blockers.push("approval-decision-version-mismatch");
|
|
1074
|
+
}
|
|
1075
|
+
if (
|
|
1076
|
+
typeof approval?.attestationId !== "string" ||
|
|
1077
|
+
!EVIDENCE_ID.test(approval.attestationId)
|
|
1078
|
+
) {
|
|
1079
|
+
blockers.push("approval-attestation-id-invalid");
|
|
1080
|
+
}
|
|
1081
|
+
const approvedAt = canonicalIso(approval?.approvedAt);
|
|
1082
|
+
if (!approvedAt) {
|
|
1083
|
+
blockers.push("approval-timestamp-invalid");
|
|
1084
|
+
} else {
|
|
1085
|
+
const endedAt = canonicalIso(coverage.observation.endedAt);
|
|
1086
|
+
if (endedAt && approvedAt.milliseconds < endedAt.milliseconds) {
|
|
1087
|
+
blockers.push("approval-precedes-observation-window-end");
|
|
1088
|
+
}
|
|
1089
|
+
if (generatedAt && approvedAt.milliseconds > generatedAt.milliseconds) {
|
|
1090
|
+
blockers.push("approval-after-report-generation");
|
|
1091
|
+
}
|
|
1092
|
+
if (now && approvedAt.milliseconds > now.milliseconds) {
|
|
1093
|
+
blockers.push("approval-timestamp-in-future");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (
|
|
1097
|
+
!hasExactFields(approval?.approvedBy, APPROVED_BY_FIELDS) ||
|
|
1098
|
+
typeof approval?.approvedBy?.id !== "string" ||
|
|
1099
|
+
approval.approvedBy.id.trim().length === 0 ||
|
|
1100
|
+
typeof approval?.approvedBy?.role !== "string" ||
|
|
1101
|
+
approval.approvedBy.role.trim().length === 0
|
|
1102
|
+
) {
|
|
1103
|
+
blockers.push("approval-reviewer-invalid");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const signature = approval?.signature;
|
|
1107
|
+
const signatureShapeValid =
|
|
1108
|
+
hasExactFields(signature, SIGNATURE_FIELDS) &&
|
|
1109
|
+
signature.algorithm === "ed25519" &&
|
|
1110
|
+
SIGNING_KEY_ID.test(signature.keyId || "") &&
|
|
1111
|
+
typeof signature.value === "string";
|
|
1112
|
+
if (!signatureShapeValid) {
|
|
1113
|
+
blockers.push("approval-signature-invalid");
|
|
1114
|
+
} else if (Object.keys(REPOSITORY_APPROVAL_PUBLIC_KEYS).length === 0) {
|
|
1115
|
+
blockers.push("approval-signature-trust-unavailable");
|
|
1116
|
+
} else {
|
|
1117
|
+
const publicKey = repositoryApprovalKey(signature.keyId);
|
|
1118
|
+
if (!publicKey) {
|
|
1119
|
+
blockers.push("approval-signing-key-untrusted");
|
|
1120
|
+
} else {
|
|
1121
|
+
try {
|
|
1122
|
+
verifyPackUpdateManifest(approval, publicKey);
|
|
1123
|
+
} catch {
|
|
1124
|
+
blockers.push("approval-signature-invalid");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (generatedAt && now && generatedAt.milliseconds > now.milliseconds) {
|
|
1129
|
+
blockers.push("report-generated-in-future");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return {
|
|
1133
|
+
ready: blockers.length === 0,
|
|
1134
|
+
blockers: [...new Set(blockers)].sort(),
|
|
1135
|
+
sha256:
|
|
1136
|
+
typeof approvalSha256 === "string" && SHA256.test(approvalSha256)
|
|
1137
|
+
? approvalSha256
|
|
1138
|
+
: null,
|
|
1139
|
+
schema:
|
|
1140
|
+
approval?.schema === COMMAND_LIFECYCLE_APPROVAL_SCHEMA
|
|
1141
|
+
? approval.schema
|
|
1142
|
+
: null,
|
|
1143
|
+
status: typeof approval?.status === "string" ? approval.status : null,
|
|
1144
|
+
scope: approval?.scope === APPROVAL_SCOPE ? approval.scope : null,
|
|
1145
|
+
attestationId:
|
|
1146
|
+
typeof approval?.attestationId === "string"
|
|
1147
|
+
? approval.attestationId
|
|
1148
|
+
: null,
|
|
1149
|
+
approvedAt: approvedAt?.value || null,
|
|
1150
|
+
approvedBy:
|
|
1151
|
+
typeof approval?.approvedBy?.id === "string" &&
|
|
1152
|
+
typeof approval?.approvedBy?.role === "string"
|
|
1153
|
+
? { id: approval.approvedBy.id, role: approval.approvedBy.role }
|
|
1154
|
+
: null,
|
|
1155
|
+
signature: signatureShapeValid
|
|
1156
|
+
? {
|
|
1157
|
+
algorithm: signature.algorithm,
|
|
1158
|
+
keyId: signature.keyId,
|
|
1159
|
+
verified: !blockers.some((blocker) =>
|
|
1160
|
+
[
|
|
1161
|
+
"approval-signature-invalid",
|
|
1162
|
+
"approval-signature-trust-unavailable",
|
|
1163
|
+
"approval-signing-key-untrusted",
|
|
1164
|
+
].includes(blocker),
|
|
1165
|
+
),
|
|
1166
|
+
trustSource:
|
|
1167
|
+
Object.keys(REPOSITORY_APPROVAL_PUBLIC_KEYS).length > 0
|
|
1168
|
+
? REPOSITORY_TRUST_SOURCE
|
|
1169
|
+
: null,
|
|
1170
|
+
}
|
|
1171
|
+
: null,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function pointRecord({ point, metric, command, coverage, partition }) {
|
|
1176
|
+
const attributes = attributeMap(point?.attributes, { lifecycle: true });
|
|
1177
|
+
const temporality = metric?.sum?.aggregationTemporality;
|
|
1178
|
+
const count = pointCount(point, { allowZero: temporality === 2 });
|
|
1179
|
+
const version = attributes?.["cli.version"];
|
|
1180
|
+
const release = coverage.releaseByVersion.get(version);
|
|
1181
|
+
const time = parseNano(point?.timeUnixNano);
|
|
1182
|
+
const startTime = parseNano(point?.startTimeUnixNano);
|
|
1183
|
+
const valid =
|
|
1184
|
+
attributes &&
|
|
1185
|
+
REQUIRED_ATTRIBUTES.every((key) => Object.hasOwn(attributes, key)) &&
|
|
1186
|
+
Object.keys(attributes).length === REQUIRED_ATTRIBUTES.length &&
|
|
1187
|
+
REQUIRED_ATTRIBUTES.every((key) => typeof attributes[key] === "string") &&
|
|
1188
|
+
count !== null &&
|
|
1189
|
+
command &&
|
|
1190
|
+
COMMAND_NAME.test(attributes["command.name"]) &&
|
|
1191
|
+
["legacy", "replacement"].includes(attributes["command.route"]) &&
|
|
1192
|
+
["completed", "error"].includes(attributes["command.outcome"]) &&
|
|
1193
|
+
release &&
|
|
1194
|
+
attributes["command.deprecated_since"] === command?.deprecatedSince &&
|
|
1195
|
+
attributes["command.removal_not_before"] === command?.removalNotBefore &&
|
|
1196
|
+
metric?.sum?.isMonotonic === true &&
|
|
1197
|
+
[1, 2].includes(temporality) &&
|
|
1198
|
+
partition &&
|
|
1199
|
+
partition.temporality === (temporality === 1 ? "delta" : "cumulative") &&
|
|
1200
|
+
time &&
|
|
1201
|
+
coverage.windowMs &&
|
|
1202
|
+
time.milliseconds >= coverage.windowMs.startedAt &&
|
|
1203
|
+
time.milliseconds <= coverage.windowMs.endedAt &&
|
|
1204
|
+
time.milliseconds >= partition.startedAtMs &&
|
|
1205
|
+
time.milliseconds < partition.endedAtMs &&
|
|
1206
|
+
time.milliseconds >= release?.npmPublishedAtMs &&
|
|
1207
|
+
(temporality !== 2 ||
|
|
1208
|
+
(startTime &&
|
|
1209
|
+
startTime.nanoseconds <= time.nanoseconds &&
|
|
1210
|
+
startTime.milliseconds >= release?.npmPublishedAtMs));
|
|
1211
|
+
if (!valid) return null;
|
|
1212
|
+
|
|
1213
|
+
const identity = {
|
|
1214
|
+
metric: metric.name,
|
|
1215
|
+
attributes,
|
|
1216
|
+
temporality,
|
|
1217
|
+
startTimeUnixNano: startTime?.nanoseconds.toString() || null,
|
|
1218
|
+
timeUnixNano: time.nanoseconds.toString(),
|
|
1219
|
+
};
|
|
1220
|
+
return {
|
|
1221
|
+
command,
|
|
1222
|
+
route: attributes["command.route"],
|
|
1223
|
+
outcome: attributes["command.outcome"],
|
|
1224
|
+
version,
|
|
1225
|
+
count,
|
|
1226
|
+
temporality,
|
|
1227
|
+
startTimeUnixNano: startTime?.nanoseconds || null,
|
|
1228
|
+
timeUnixNano: time.nanoseconds,
|
|
1229
|
+
pointKey: sha256Json(identity),
|
|
1230
|
+
fingerprint: sha256Json({ ...identity, count }),
|
|
1231
|
+
series: sha256Json({
|
|
1232
|
+
metric: metric.name,
|
|
1233
|
+
attributes,
|
|
1234
|
+
startTimeUnixNano: startTime?.nanoseconds.toString() || null,
|
|
1235
|
+
}),
|
|
1236
|
+
baseSeries: sha256Json({
|
|
1237
|
+
metric: metric.name,
|
|
1238
|
+
attributes,
|
|
1239
|
+
}),
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function aggregateTelemetry({ exports: telemetryExports, commands, coverage }) {
|
|
1244
|
+
const violations = [];
|
|
1245
|
+
const records = [];
|
|
1246
|
+
const pointOccurrences = new Map();
|
|
1247
|
+
let payloadCount = 0;
|
|
1248
|
+
let acceptedPoints = 0;
|
|
1249
|
+
let rejectedPoints = 0;
|
|
1250
|
+
let duplicatePoints = 0;
|
|
1251
|
+
|
|
1252
|
+
for (const source of telemetryExports) {
|
|
1253
|
+
for (const document of source.documents) {
|
|
1254
|
+
for (const payload of metricPayloads(document)) {
|
|
1255
|
+
payloadCount += 1;
|
|
1256
|
+
for (const resource of payload.resourceMetrics || []) {
|
|
1257
|
+
for (const scope of resource.scopeMetrics || []) {
|
|
1258
|
+
for (const metric of scope.metrics || []) {
|
|
1259
|
+
if (metric?.name !== COMMAND_LIFECYCLE_INVOCATION_METRIC)
|
|
1260
|
+
continue;
|
|
1261
|
+
if (!metric.sum || !Array.isArray(metric.sum.dataPoints)) {
|
|
1262
|
+
rejectedPoints += 1;
|
|
1263
|
+
violations.push("invalid-lifecycle-metric-shape");
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
for (const point of metric.sum.dataPoints) {
|
|
1267
|
+
const attributes = attributeMap(point?.attributes, {
|
|
1268
|
+
lifecycle: true,
|
|
1269
|
+
});
|
|
1270
|
+
const command = attributes
|
|
1271
|
+
? commands.get(attributes["command.name"])
|
|
1272
|
+
: null;
|
|
1273
|
+
const record = pointRecord({
|
|
1274
|
+
point,
|
|
1275
|
+
metric,
|
|
1276
|
+
command,
|
|
1277
|
+
coverage,
|
|
1278
|
+
partition: source.partition,
|
|
1279
|
+
});
|
|
1280
|
+
if (!record) {
|
|
1281
|
+
rejectedPoints += 1;
|
|
1282
|
+
violations.push("invalid-lifecycle-metric-point");
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const occurrence = pointOccurrences.get(record.pointKey);
|
|
1286
|
+
if (occurrence) {
|
|
1287
|
+
occurrence.occurrences += 1;
|
|
1288
|
+
if (occurrence.record.fingerprint !== record.fingerprint) {
|
|
1289
|
+
occurrence.conflicting = true;
|
|
1290
|
+
} else {
|
|
1291
|
+
duplicatePoints += 1;
|
|
1292
|
+
if (record.temporality === 1) {
|
|
1293
|
+
occurrence.conflicting = true;
|
|
1294
|
+
occurrence.duplicateDelta = true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
pointOccurrences.set(record.pointKey, {
|
|
1300
|
+
record,
|
|
1301
|
+
occurrences: 1,
|
|
1302
|
+
conflicting: false,
|
|
1303
|
+
duplicateDelta: false,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
for (const occurrence of pointOccurrences.values()) {
|
|
1314
|
+
if (occurrence.conflicting) {
|
|
1315
|
+
rejectedPoints += occurrence.occurrences;
|
|
1316
|
+
violations.push(
|
|
1317
|
+
occurrence.duplicateDelta
|
|
1318
|
+
? "duplicate-delta-lifecycle-metric-point"
|
|
1319
|
+
: "ambiguous-lifecycle-metric-point",
|
|
1320
|
+
);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
records.push(occurrence.record);
|
|
1324
|
+
acceptedPoints += 1;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
let countedInvocations = 0;
|
|
1328
|
+
for (const record of records.filter((item) => item.temporality === 1)) {
|
|
1329
|
+
addInvocation(record.command, record, record.count);
|
|
1330
|
+
countedInvocations += record.count;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const cumulativeSeries = new Map();
|
|
1334
|
+
for (const record of records.filter((item) => item.temporality === 2)) {
|
|
1335
|
+
if (!cumulativeSeries.has(record.series))
|
|
1336
|
+
cumulativeSeries.set(record.series, []);
|
|
1337
|
+
cumulativeSeries.get(record.series).push(record);
|
|
1338
|
+
}
|
|
1339
|
+
const cumulativeSegments = [];
|
|
1340
|
+
for (const series of cumulativeSeries.values()) {
|
|
1341
|
+
let previous = null;
|
|
1342
|
+
let latest = null;
|
|
1343
|
+
let invalid = false;
|
|
1344
|
+
for (const record of series) {
|
|
1345
|
+
if (
|
|
1346
|
+
previous &&
|
|
1347
|
+
(record.timeUnixNano <= previous.timeUnixNano ||
|
|
1348
|
+
record.count < previous.count)
|
|
1349
|
+
) {
|
|
1350
|
+
invalid = true;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
previous = record;
|
|
1354
|
+
latest = record;
|
|
1355
|
+
}
|
|
1356
|
+
if (invalid || !latest) {
|
|
1357
|
+
rejectedPoints += series.length;
|
|
1358
|
+
acceptedPoints -= series.length;
|
|
1359
|
+
violations.push("cumulative-lifecycle-series-invalid");
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
cumulativeSegments.push({
|
|
1363
|
+
baseSeries: latest.baseSeries,
|
|
1364
|
+
startTimeUnixNano: latest.startTimeUnixNano,
|
|
1365
|
+
lastTimeUnixNano: latest.timeUnixNano,
|
|
1366
|
+
latest,
|
|
1367
|
+
pointCount: series.length,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const segmentsByBaseSeries = new Map();
|
|
1372
|
+
for (const segment of cumulativeSegments) {
|
|
1373
|
+
if (!segmentsByBaseSeries.has(segment.baseSeries)) {
|
|
1374
|
+
segmentsByBaseSeries.set(segment.baseSeries, []);
|
|
1375
|
+
}
|
|
1376
|
+
segmentsByBaseSeries.get(segment.baseSeries).push(segment);
|
|
1377
|
+
}
|
|
1378
|
+
for (const segments of segmentsByBaseSeries.values()) {
|
|
1379
|
+
let previous = null;
|
|
1380
|
+
const resetOverlap = segments.some((segment) => {
|
|
1381
|
+
const overlaps =
|
|
1382
|
+
previous && segment.startTimeUnixNano <= previous.lastTimeUnixNano;
|
|
1383
|
+
previous = segment;
|
|
1384
|
+
return overlaps;
|
|
1385
|
+
});
|
|
1386
|
+
if (resetOverlap) {
|
|
1387
|
+
const points = segments.reduce(
|
|
1388
|
+
(total, segment) => total + segment.pointCount,
|
|
1389
|
+
0,
|
|
1390
|
+
);
|
|
1391
|
+
rejectedPoints += points;
|
|
1392
|
+
acceptedPoints -= points;
|
|
1393
|
+
violations.push("cumulative-lifecycle-reset-overlap");
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
for (const segment of segments) {
|
|
1397
|
+
addInvocation(
|
|
1398
|
+
segment.latest.command,
|
|
1399
|
+
segment.latest,
|
|
1400
|
+
segment.latest.count,
|
|
1401
|
+
);
|
|
1402
|
+
countedInvocations += segment.latest.count;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (acceptedPoints === 0) violations.push("no-lifecycle-metric-points");
|
|
1407
|
+
if (countedInvocations === 0) violations.push("no-lifecycle-invocations");
|
|
1408
|
+
return {
|
|
1409
|
+
payloadCount,
|
|
1410
|
+
acceptedPoints,
|
|
1411
|
+
rejectedPoints,
|
|
1412
|
+
duplicatePoints,
|
|
1413
|
+
countedInvocations,
|
|
1414
|
+
violations: [...new Set(violations)].sort(),
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function decisionFor(command, coverage, evidenceReady, ingestionReady, policy) {
|
|
287
1419
|
const total =
|
|
288
1420
|
command.legacy.invocations + command.replacementRoute.invocations;
|
|
289
1421
|
const legacyShare = total > 0 ? command.legacy.invocations / total : null;
|
|
@@ -293,25 +1425,20 @@ function decisionFor(command, coverage, ingestionReady, policy) {
|
|
|
293
1425
|
command.removalNotBefore,
|
|
294
1426
|
);
|
|
295
1427
|
if (!coverage.ready) reasons.push(...coverage.blockers);
|
|
1428
|
+
if (!evidenceReady) reasons.push("evidence-bundle-unapproved");
|
|
296
1429
|
if (!ingestionReady) reasons.push("telemetry-input-invalid");
|
|
297
1430
|
if (removalComparison === null || removalComparison < 0) {
|
|
298
1431
|
reasons.push("removal-version-not-reached");
|
|
299
1432
|
}
|
|
300
1433
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
observedForCommand.map((version) => {
|
|
310
|
-
const parsed = parseRelease(version);
|
|
311
|
-
return `${parsed.major}.${parsed.minor}`;
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
if (observedCycles.size < command.minimumReleaseCycles) {
|
|
1434
|
+
const observedCycles = [...command.minors.values()]
|
|
1435
|
+
.filter(
|
|
1436
|
+
(minor) =>
|
|
1437
|
+
minor.legacy.invocations + minor.replacementRoute.invocations > 0,
|
|
1438
|
+
)
|
|
1439
|
+
.map((minor) => minor.minor)
|
|
1440
|
+
.sort();
|
|
1441
|
+
if (observedCycles.length < command.minimumReleaseCycles) {
|
|
315
1442
|
reasons.push("command-release-window-incomplete");
|
|
316
1443
|
}
|
|
317
1444
|
if (total < policy.minimumCommandInvocations) {
|
|
@@ -321,20 +1448,22 @@ function decisionFor(command, coverage, ingestionReady, policy) {
|
|
|
321
1448
|
if (reasons.length > 0) {
|
|
322
1449
|
return {
|
|
323
1450
|
decision: "insufficient-data",
|
|
1451
|
+
aliasAction: "retain",
|
|
324
1452
|
reasons: [...new Set(reasons)].sort(),
|
|
325
1453
|
totalInvocations: total,
|
|
326
1454
|
legacyShare,
|
|
327
|
-
observedMinorCycles:
|
|
1455
|
+
observedMinorCycles: observedCycles,
|
|
328
1456
|
};
|
|
329
1457
|
}
|
|
330
1458
|
|
|
331
1459
|
if (legacyShare > policy.maximumLegacyShareForRemoval) {
|
|
332
1460
|
return {
|
|
333
1461
|
decision: "retain",
|
|
1462
|
+
aliasAction: "retain",
|
|
334
1463
|
reasons: ["legacy-usage-above-removal-threshold"],
|
|
335
1464
|
totalInvocations: total,
|
|
336
1465
|
legacyShare,
|
|
337
|
-
observedMinorCycles:
|
|
1466
|
+
observedMinorCycles: observedCycles,
|
|
338
1467
|
};
|
|
339
1468
|
}
|
|
340
1469
|
if (
|
|
@@ -342,97 +1471,97 @@ function decisionFor(command, coverage, ingestionReady, policy) {
|
|
|
342
1471
|
) {
|
|
343
1472
|
return {
|
|
344
1473
|
decision: "insufficient-data",
|
|
1474
|
+
aliasAction: "retain",
|
|
345
1475
|
reasons: ["replacement-sample-too-small"],
|
|
346
1476
|
totalInvocations: total,
|
|
347
1477
|
legacyShare,
|
|
348
|
-
observedMinorCycles:
|
|
1478
|
+
observedMinorCycles: observedCycles,
|
|
349
1479
|
};
|
|
350
1480
|
}
|
|
351
1481
|
return {
|
|
352
1482
|
decision: "remove",
|
|
1483
|
+
aliasAction: "remove",
|
|
353
1484
|
reasons: ["compatibility-window-and-usage-thresholds-satisfied"],
|
|
354
1485
|
totalInvocations: total,
|
|
355
1486
|
legacyShare,
|
|
356
|
-
observedMinorCycles:
|
|
1487
|
+
observedMinorCycles: observedCycles,
|
|
357
1488
|
};
|
|
358
1489
|
}
|
|
359
1490
|
|
|
360
1491
|
export function buildCommandLifecycleReport({
|
|
361
|
-
|
|
1492
|
+
telemetryExports = [],
|
|
362
1493
|
manifest,
|
|
363
1494
|
coverage,
|
|
1495
|
+
coverageSha256,
|
|
1496
|
+
exportManifest,
|
|
1497
|
+
exportManifestSha256,
|
|
1498
|
+
generatorSha,
|
|
1499
|
+
approval,
|
|
1500
|
+
approvalSha256,
|
|
364
1501
|
generatedAt = new Date().toISOString(),
|
|
365
|
-
policy = COMMAND_ALIAS_DECISION_POLICY,
|
|
366
1502
|
} = {}) {
|
|
367
1503
|
const entries = lifecycleEntries(manifest);
|
|
368
1504
|
if (entries.length === 0) {
|
|
369
1505
|
throw new Error("command manifest has no deprecated compatibility entries");
|
|
370
1506
|
}
|
|
1507
|
+
const generated = canonicalIso(generatedAt);
|
|
1508
|
+
if (!generated)
|
|
1509
|
+
throw new Error("generatedAt must be a canonical ISO timestamp");
|
|
1510
|
+
const currentTime = canonicalIso(new Date().toISOString());
|
|
1511
|
+
const policy = COMMAND_ALIAS_DECISION_POLICY;
|
|
1512
|
+
const policySha256 = COMMAND_ALIAS_DECISION_POLICY_SHA256;
|
|
1513
|
+
const generatorVerification = verifyRepositoryGenerator(generatorSha);
|
|
1514
|
+
|
|
371
1515
|
const byCommand = new Map(
|
|
372
1516
|
entries.map((entry) => [entry.name, blankCommand(entry)]),
|
|
373
1517
|
);
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
rejectedPoints += 1;
|
|
409
|
-
violations.push("invalid-lifecycle-metric-point");
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
acceptedPoints += 1;
|
|
414
|
-
const bucket =
|
|
415
|
-
attributes["command.route"] === "legacy"
|
|
416
|
-
? command.legacy
|
|
417
|
-
: command.replacementRoute;
|
|
418
|
-
bucket.invocations += count;
|
|
419
|
-
bucket[attributes["command.outcome"]] += count;
|
|
420
|
-
command.versions.add(attributes["cli.version"]);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const coverageResult = validateCoverage(coverage, policy);
|
|
428
|
-
const ingestionReady = rejectedPoints === 0 && acceptedPoints > 0;
|
|
429
|
-
if (acceptedPoints === 0) violations.push("no-lifecycle-metric-points");
|
|
1518
|
+
const coverageResult = validateCoverage(coverage, policy, generated);
|
|
1519
|
+
const exportResult = validateTelemetryExports({
|
|
1520
|
+
telemetryExports,
|
|
1521
|
+
exportManifest,
|
|
1522
|
+
exportManifestSha256,
|
|
1523
|
+
coverage: coverageResult,
|
|
1524
|
+
policySha256,
|
|
1525
|
+
generatorSha,
|
|
1526
|
+
generatorVerification,
|
|
1527
|
+
});
|
|
1528
|
+
const approvalResult = validateApproval({
|
|
1529
|
+
approval,
|
|
1530
|
+
approvalSha256,
|
|
1531
|
+
coverageSha256,
|
|
1532
|
+
exportManifestSha256,
|
|
1533
|
+
coverage: coverageResult,
|
|
1534
|
+
policySha256,
|
|
1535
|
+
generatorSha,
|
|
1536
|
+
generatedAt: generated,
|
|
1537
|
+
now: currentTime,
|
|
1538
|
+
});
|
|
1539
|
+
const ingestion = aggregateTelemetry({
|
|
1540
|
+
exports: exportResult.exports,
|
|
1541
|
+
commands: byCommand,
|
|
1542
|
+
coverage: coverageResult,
|
|
1543
|
+
});
|
|
1544
|
+
ingestion.violations = [
|
|
1545
|
+
...new Set([...exportResult.blockers, ...ingestion.violations]),
|
|
1546
|
+
].sort();
|
|
1547
|
+
ingestion.ready =
|
|
1548
|
+
exportResult.blockers.length === 0 &&
|
|
1549
|
+
ingestion.rejectedPoints === 0 &&
|
|
1550
|
+
ingestion.acceptedPoints > 0 &&
|
|
1551
|
+
ingestion.countedInvocations > 0;
|
|
430
1552
|
|
|
1553
|
+
const evidenceReady =
|
|
1554
|
+
exportResult.blockers.length === 0 && approvalResult.ready;
|
|
1555
|
+
const formalObservation =
|
|
1556
|
+
coverageResult.ready && evidenceReady && ingestion.ready
|
|
1557
|
+
? "approved-evidence"
|
|
1558
|
+
: "insufficient-data";
|
|
431
1559
|
const decisions = [...byCommand.values()].map((command) => {
|
|
432
1560
|
const recommendation = decisionFor(
|
|
433
1561
|
command,
|
|
434
1562
|
coverageResult,
|
|
435
|
-
|
|
1563
|
+
evidenceReady,
|
|
1564
|
+
ingestion.ready,
|
|
436
1565
|
policy,
|
|
437
1566
|
);
|
|
438
1567
|
return {
|
|
@@ -442,6 +1571,19 @@ export function buildCommandLifecycleReport({
|
|
|
442
1571
|
removalNotBefore: command.removalNotBefore,
|
|
443
1572
|
legacy: command.legacy,
|
|
444
1573
|
replacementRoute: command.replacementRoute,
|
|
1574
|
+
minorCycles: coverageResult.observedMinorCycles.map((minor) => {
|
|
1575
|
+
const value = command.minors.get(minor);
|
|
1576
|
+
return {
|
|
1577
|
+
minor,
|
|
1578
|
+
versions: value
|
|
1579
|
+
? [...value.versions].sort((left, right) =>
|
|
1580
|
+
compareReleases(left, right),
|
|
1581
|
+
)
|
|
1582
|
+
: [],
|
|
1583
|
+
legacy: value?.legacy || blankCounts(),
|
|
1584
|
+
replacementRoute: value?.replacementRoute || blankCounts(),
|
|
1585
|
+
};
|
|
1586
|
+
}),
|
|
445
1587
|
observedVersions: [...command.versions].sort((left, right) =>
|
|
446
1588
|
compareReleases(left, right),
|
|
447
1589
|
),
|
|
@@ -450,28 +1592,61 @@ export function buildCommandLifecycleReport({
|
|
|
450
1592
|
});
|
|
451
1593
|
const summary = decisions.reduce(
|
|
452
1594
|
(result, item) => {
|
|
453
|
-
result[item.decision] += 1;
|
|
1595
|
+
result.decisions[item.decision] += 1;
|
|
1596
|
+
result.aliasActions[item.aliasAction] += 1;
|
|
454
1597
|
return result;
|
|
455
1598
|
},
|
|
456
|
-
{
|
|
1599
|
+
{
|
|
1600
|
+
decisions: { retain: 0, remove: 0, "insufficient-data": 0 },
|
|
1601
|
+
aliasActions: { retain: 0, remove: 0 },
|
|
1602
|
+
},
|
|
457
1603
|
);
|
|
458
1604
|
|
|
459
1605
|
return {
|
|
460
1606
|
schema: COMMAND_LIFECYCLE_REPORT_SCHEMA,
|
|
461
|
-
generatedAt:
|
|
1607
|
+
generatedAt: generated.value,
|
|
1608
|
+
formalObservation,
|
|
462
1609
|
manifestSchema: manifest?.schema || null,
|
|
1610
|
+
observation: coverageResult.observation,
|
|
463
1611
|
policy: {
|
|
464
1612
|
...policy,
|
|
465
1613
|
requiredPlatforms: [...policy.requiredPlatforms],
|
|
1614
|
+
deltaRelease: coverageResult.deltaReleasePolicy,
|
|
1615
|
+
sha256: policySha256,
|
|
466
1616
|
},
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1617
|
+
evidence: {
|
|
1618
|
+
ready: evidenceReady,
|
|
1619
|
+
coverage: {
|
|
1620
|
+
sha256:
|
|
1621
|
+
typeof coverageSha256 === "string" && SHA256.test(coverageSha256)
|
|
1622
|
+
? coverageSha256
|
|
1623
|
+
: null,
|
|
1624
|
+
},
|
|
1625
|
+
exportManifest: exportResult.manifest,
|
|
1626
|
+
telemetryExports: exportResult.exports.map(
|
|
1627
|
+
({ id, sha256, documents, partition }) => ({
|
|
1628
|
+
sequence: partition.sequence,
|
|
1629
|
+
id,
|
|
1630
|
+
sha256,
|
|
1631
|
+
documentCount: documents.length,
|
|
1632
|
+
startedAt: partition.startedAt,
|
|
1633
|
+
endedAt: partition.endedAt,
|
|
1634
|
+
temporality: partition.temporality,
|
|
1635
|
+
}),
|
|
1636
|
+
),
|
|
1637
|
+
approval: approvalResult,
|
|
1638
|
+
},
|
|
1639
|
+
releases: coverageResult.publicReleases,
|
|
1640
|
+
coverage: {
|
|
1641
|
+
ready: coverageResult.ready,
|
|
1642
|
+
blockers: coverageResult.blockers,
|
|
1643
|
+
reportSchema: coverageResult.reportSchema,
|
|
1644
|
+
decisionVersion: coverageResult.decisionVersion,
|
|
1645
|
+
observedMinorCycles: coverageResult.observedMinorCycles,
|
|
1646
|
+
deltaReleasePolicy: coverageResult.deltaReleasePolicy,
|
|
1647
|
+
collector: coverageResult.collector,
|
|
474
1648
|
},
|
|
1649
|
+
ingestion,
|
|
475
1650
|
summary,
|
|
476
1651
|
decisions,
|
|
477
1652
|
};
|
|
@@ -481,11 +1656,16 @@ export function renderCommandLifecycleReportMarkdown(report) {
|
|
|
481
1656
|
const lines = [
|
|
482
1657
|
"# CLI command lifecycle decision report",
|
|
483
1658
|
"",
|
|
1659
|
+
`Schema: \`${report.schema}\``,
|
|
484
1660
|
`Generated: ${report.generatedAt}`,
|
|
1661
|
+
`Observation: \`${report.observation.id || "invalid"}\` (${report.observation.startedAt || "invalid"} to ${report.observation.endedAt || "invalid"})`,
|
|
485
1662
|
"",
|
|
486
1663
|
`Coverage ready: **${report.coverage.ready ? "yes" : "no"}**`,
|
|
1664
|
+
`Evidence approved: **${report.evidence.ready ? "yes" : "no"}**`,
|
|
487
1665
|
`Telemetry input ready: **${report.ingestion.ready ? "yes" : "no"}**`,
|
|
488
|
-
`
|
|
1666
|
+
`Adoption evidence: **${report.formalObservation}**`,
|
|
1667
|
+
`Decisions: ${report.summary.decisions.remove} remove, ${report.summary.decisions.retain} retain, ${report.summary.decisions["insufficient-data"]} insufficient-data.`,
|
|
1668
|
+
`Alias actions: ${report.summary.aliasActions.remove} remove, ${report.summary.aliasActions.retain} retain.`,
|
|
489
1669
|
"",
|
|
490
1670
|
];
|
|
491
1671
|
if (report.coverage.blockers.length > 0) {
|
|
@@ -495,6 +1675,15 @@ export function renderCommandLifecycleReportMarkdown(report) {
|
|
|
495
1675
|
"",
|
|
496
1676
|
);
|
|
497
1677
|
}
|
|
1678
|
+
if (report.evidence.approval.blockers.length > 0) {
|
|
1679
|
+
lines.push(
|
|
1680
|
+
"Evidence blockers: " +
|
|
1681
|
+
report.evidence.approval.blockers
|
|
1682
|
+
.map((item) => `\`${item}\``)
|
|
1683
|
+
.join(", "),
|
|
1684
|
+
"",
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
498
1687
|
if (report.ingestion.violations.length > 0) {
|
|
499
1688
|
lines.push(
|
|
500
1689
|
"Input violations: " +
|
|
@@ -503,12 +1692,18 @@ export function renderCommandLifecycleReportMarkdown(report) {
|
|
|
503
1692
|
);
|
|
504
1693
|
}
|
|
505
1694
|
lines.push(
|
|
506
|
-
"| Legacy entry | Replacement | Legacy | Replacement | Legacy share | Decision | Reasons |",
|
|
507
|
-
"| --- | --- | ---: | ---: | ---: | --- | --- |",
|
|
1695
|
+
"| Legacy entry | Replacement | Legacy | Replacement | Per-minor L/R | Legacy share | Decision | Alias action | Reasons |",
|
|
1696
|
+
"| --- | --- | ---: | ---: | --- | ---: | --- | --- | --- |",
|
|
508
1697
|
);
|
|
509
1698
|
for (const item of report.decisions) {
|
|
1699
|
+
const minorMetrics = item.minorCycles
|
|
1700
|
+
.map(
|
|
1701
|
+
(minor) =>
|
|
1702
|
+
`${minor.minor}: ${minor.legacy.invocations}/${minor.replacementRoute.invocations}`,
|
|
1703
|
+
)
|
|
1704
|
+
.join("; ");
|
|
510
1705
|
lines.push(
|
|
511
|
-
`| \`${item.command}\` | \`${item.replacement}\` | ${item.legacy.invocations} | ${item.replacementRoute.invocations} | ${item.legacyShare === null ? "n/a" : `${(item.legacyShare * 100).toFixed(2)}%`} | **${item.decision}** | ${item.reasons.join(", ")} |`,
|
|
1706
|
+
`| \`${item.command}\` | \`${item.replacement}\` | ${item.legacy.invocations} | ${item.replacementRoute.invocations} | ${minorMetrics || "n/a"} | ${item.legacyShare === null ? "n/a" : `${(item.legacyShare * 100).toFixed(2)}%`} | **${item.decision}** | **${item.aliasAction}** | ${item.reasons.join(", ")} |`,
|
|
512
1707
|
);
|
|
513
1708
|
}
|
|
514
1709
|
lines.push("");
|