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.
Files changed (198) hide show
  1. package/package.json +3 -2
  2. package/scripts/command-lifecycle-report.mjs +330 -38
  3. package/src/assets/web-panel/assets/{AIOps-DIY6KxV-.js → AIOps-DB5X7qqV.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BXH0muPt.js → ActionButton-DxHRtKLk.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D39w8vmg.js → Analytics-DIH5QrxT.js} +2 -2
  6. package/src/assets/web-panel/assets/{AppLayout-CUU1zvLs.js → AppLayout-uu0oxVz7.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-D8azHO3T.js → Artifacts-BNSMiW9Y.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-D0tAwXTg.js → Audit-DcAzB4_x.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-C29mhJll.js → BackgroundAgents-ClSsvjIc.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D6Aa1Gro.js → Backup-BMciY0js.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-B1uoHbPQ.js → BaseInput-Hn3b57Sh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-Bs4DLOR5.js → Chat-CoSlC53V.js} +5 -5
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DUYIBSle.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-CNcBPEa4.js → Checkbox-B4L4Eu4w.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CIY89763.js → Codegen-C77FgRxF.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-CsPYFmgo.js → Col-BrLtJLH3.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-BBocEge4.js → Community-Be7pXXhA.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-TQ8Ux0Lf.js → Compact-DuGPecjo.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-DfBsZTft.js → Compliance-CzS66mjP.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-Cg-I6FVH.js → Cowork-CEhjCoyK.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-DTQzfRTp.js → Cron-D7ir8Vja.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DGxEKGpR.js → Crosschain-BcXmuLxf.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-DLZ1RDSr.js → DID-Cto1dtBy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-XneICKNI.js → Dashboard-B7KjgwOX.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BRU00J1-.js → Dropdown-COMqxijg.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-BzCbJuAy.js → EmailListRenderer-BZrya-nf.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D7BRyNBG.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-D9DeFIhW.js → Federation-kZU8c-27.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-6rAtA9Ko.js → FormItemContext-sOV22v2-.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-8Hqy80pU.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-Cmvkj1rt.js → Git-K-ZxsLsH.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-BRiQUjjw.js → Governance-D3OEnawI.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CKN_EzVf.js → Inference-Bll0gRmP.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-BCDHwpwQ.js → KnowledgeGraph-DBXwjr53.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DK9ovhaB.js → Logs-CEzPwEfP.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-CLmCbHx6.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-BILPGakp.js → Marketplace-BYyRyOPz.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DTVCOr9-.js → McpTools-DVu8w_rM.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-Hzc1iow4.js → Memory-tenuZ_hO.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-GXfgnGYV.js → MobileBridge-Bxgy4ozS.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-B-3TJWsS.js → MobileProjects-Dgx1D6sc.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-DwSJMWtK.js → Mtc-CRfJ8r8j.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-D2uxf-DA.js → MtcAudit-BBJq4Aiz.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-DG6kbKK0.js → Multisig-B3ii9lai.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-f5p49EDU.js → NLProgramming-FIi-Cubv.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-FUgHyzSD.js → Notes-DIq8MPf1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DCHGee7e.js → NotificationSettings-BFOD9eGd.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-Dm6LlIoa.js → OrderTableRenderer--41u7vre.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-BawA7qAg.js → Organization-B_0AWztW.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-D6a5wZU1.js → Overflow-B6I7TuXv.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-K8WavwnA.js → P2P-Cdf6qens.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-4JAJYaqx.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-BDWvi8PD.js → Permissions-6nFBjsQp.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-DF_K4E9i.js → PersonalDataHub-nQYV7xpn.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-BYeZlesI.js → Pipeline-CBVod5jm.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-BIHlgnIc.js → Privacy-D52jW1Nt.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-Cvf-6B5P.js → ProjectInit-Bdx3BYhf.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-CnwfJrRz.js → ProjectSettings-b7xr4DUa.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-b-jPXzF4.js → Projects-DhfifCKx.js} +1 -1
  60. package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
  61. package/src/assets/web-panel/assets/{QrScannerModal-VLf3gAfL.js → QrScannerModal-FQKSmc3e.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-B5vGjRiF.js → QuickAsk-CSTrhzcF.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend--UW4iYs-.js → Recommend-CxMABOUh.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DMSPcfKR.js → RemoteSession-Cv2T8yRa.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-kQXlEHmj.js → Reputation-CE1k4Vvu.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-CxGu3pit.js → Row-BNgNcAV_.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-Bi5lo8MZ.js → RssFeed-5ZiBZ3ph.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-Cq_H5RTh.js → Search-mDOIF1UV.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-YFbmkaQS.js → Security-CtuCTC-e.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-DLOGdaLm.js → Services-DxFO2Zg7.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-CteIxb3D.js → Skeleton-CKmKkH9y.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-CccopaIZ.js → Skills-Bmzl9qkk.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-AzfEQKX1.js → Sla-9RqaWHOC.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-ekGo5l77.js → SpeechSettings-CGdzFIYw.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-BmZmAgYH.js → SyncSettings-CvNuCeBT.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-D7q3Z4uL.js → Tasks-B_sLdcnw.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-CX1Tgeka.js → Templates-D9_YijdK.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-Zc59jF4i.js → Tenant-Cuc7BUSP.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-CXOD7y83.js → Terminal-6VYRnJgd.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-B1aBHPBo.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-zALlOq_U.js → Tokens-BZ94Lt5W.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CQfJMXaq.js → Trigger-DEcZgDaX.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BU0RcM-w.js → Trust-CC3z9THe.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-IFZ3HsbH.js → UkeySign-Cvtw8cV3.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-BzslgW0v.js → VideoEditing-CL4e6Bn3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-BZAcQNS6.js → Wallet-ycEemP27.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CBK1DMi0.js → WebAuthn-DgZ4X_aw.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BlMTPzO-.js → WorkflowEditor-D0qMhSv7.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-B51-8QVn.js → chat-DsAg01Zn.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-504RTByU.js → colors-B0oqAhrQ.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-C-GP6IQM.js → compact-item-CmX0Rabz.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-DOkIj7ID.js → createContext-Be4cn1Ub.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DN7V-_FW.js → hasIn-D5v12kVq.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-kt_5rfcZ.js → index-0eWLkQKT.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CUhQpiYv.js → index-301ogenv.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-f4Bo4KXm.js → index-5LGY0JbG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index--n-ZciV3.js → index-7VKDn-t1.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D1phyca8.js → index-8WvYGnON.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-7jIqHbzw.js → index-B1YGjOWC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DEMPUVa3.js → index-B3mUHuVc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Bkxhaeta.js → index-BA4oulaP.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CYopNjcJ.js → index-BBREnn1V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-P2mfY27Z.js → index-BBvyHguy.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DA2f1UV3.js → index-BKmsS2jj.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CP2A226L.js → index-BRIqLy1m.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CfiEM64n.js → index-BXGSbelg.js} +3 -3
  108. package/src/assets/web-panel/assets/{index-B2-x9bS8.js → index-BaE0Ui07.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CySTJKeq.js → index-BoAIAZ5C.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-kAqQXfP6.js → index-BqltWojQ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DAOpQvl_.js → index-CGiIyTZw.js} +1 -1
  112. package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-C4DV_5uA.js → index-CZ2wUEgr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-yYTQZypF.js → index-CZpfzcuy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-et5JIPHO.js → index-Ckrk3pIV.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DaA__-q1.js → index-CqoCZ5vR.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-By6u5IV1.js → index-CyWBir7N.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CKTfZyfU.js → index-CyyGTMKd.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B_vGU-Js.js → index-D0BSkxYQ.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Cao5DZy7.js → index-D5YVopyr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-C3TSohzc.js → index-DKUBj36_.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BFIFJ5Dr.js → index-DM724NgU.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-ConjaJro.js → index-DQRsQen0.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
  125. package/src/assets/web-panel/assets/{index-C9_48OzU.js → index-DikJ0Qqe.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-C0k_Nqg9.js → index-Dn2dSdXx.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DE-hmI2l.js → index-DyUaFWEk.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-so8eIIxF.js → index-Hfe_dEGx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-FcV8J_Qz.js → index-_JjRLSD0.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-D9LyiN-L.js → index-frSojpHj.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CX78k-qf.js → index-jK3ax6Z6.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-C8UltbXK.js → index-qMuZ-NRR.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CqcLo5WV.js → index-q_fQ94SE.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CPYEIdpV.js → initDefaultProps-D4VdhJ9_.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-Cjim9pxs.js → motion-CZJA0MQW.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DuU-cgz3.js → move-BqEs7Sb1.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-DJHNAkta.js → mtc-parser-BpHEtsu1.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-DSQBgdBj.js → omit-BW94tY9W.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-CTmT_j5m.js → pickAttrs-BnBjPWP9.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-Lfm6v-r4.js → placementArrow-uUkjQ_hu.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-CgrcPcDi.js → responsiveObserve-B-klUqXK.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-CuE3110i.js → slide-CvY1cNAs.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-DiK5nbpf.js → statusUtils-Ix5sEDT2.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-DPEJdQaZ.js → styleChecker-C8qcXOne.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-CnnodGmp.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-DWF84Yav.js → useFs-DGJr345n.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-a5b5Dw2f.js → usePersonalDataHub-BboN99rI.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DAluzXOq.js → vnode-Bgt8Vgte.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-Dokg7uAZ.js → zoom-BhJtErgt.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +1 -1
  152. package/src/commands/artifacts.js +98 -0
  153. package/src/commands/mcp.js +1 -1
  154. package/src/commands/review.js +221 -22
  155. package/src/commands/session-mcp-recovery.js +5 -5
  156. package/src/data/changelog.json +14 -1
  157. package/src/gateways/ws/ws-agent-handler.js +492 -14
  158. package/src/harness/jsonl-session-store.js +39 -1
  159. package/src/harness/mcp-client.js +172 -29
  160. package/src/harness/prompt-compressor.js +24 -0
  161. package/src/harness/provider-backed-compaction.js +205 -0
  162. package/src/lib/app-preview.js +3 -0
  163. package/src/lib/command-lifecycle-report.js +1340 -145
  164. package/src/lib/delivery-coordinator.js +30 -5
  165. package/src/lib/delivery-production-adapter.js +1777 -0
  166. package/src/lib/delivery-production-runner.js +240 -0
  167. package/src/lib/durable-security-store.js +29 -17
  168. package/src/lib/execution-trace/trace-context.cjs +179 -0
  169. package/src/lib/execution-trace/trace-context.js +8 -175
  170. package/src/lib/mcp-call-ledger-store.js +58 -6
  171. package/src/lib/mcp-host-recovery-runtime.js +2 -0
  172. package/src/lib/mcp-ledger-recovery-admission.js +39 -3
  173. package/src/lib/mcp-recovery-adjudication.js +256 -19
  174. package/src/lib/mcp-sandbox-policy.js +29 -0
  175. package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
  176. package/src/lib/mcp-stdio-executable-identity.js +33 -2
  177. package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
  178. package/src/lib/mcp-stdio-package-materialization.js +721 -333
  179. package/src/lib/observability/index.js +2 -0
  180. package/src/lib/observability/otlp-exporter.js +2 -2
  181. package/src/lib/packer/pack-update-applier.js +8 -1
  182. package/src/lib/packer/pkg-config-generator.js +34 -0
  183. package/src/lib/process-execution-broker/index.js +34 -5
  184. package/src/lib/process-execution-broker/platform-sandbox.js +82 -1
  185. package/src/lib/scheduler-kernel/contract.js +315 -0
  186. package/src/lib/scheduler-kernel/store.js +1345 -0
  187. package/src/lib/session-host-lease.js +403 -30
  188. package/src/lib/session-persistence-failure.js +4 -1
  189. package/src/lib/sub-agent-context.js +8 -0
  190. package/src/repl/agent-repl.js +285 -38
  191. package/src/runtime/agent-core.js +1044 -90
  192. package/src/runtime/headless-runner.js +260 -166
  193. package/src/runtime/headless-stream.js +254 -25
  194. package/src/assets/web-panel/assets/MarkdownRenderer-D53pHiHy.js +0 -1
  195. package/src/assets/web-panel/assets/Providers-CxC7JuYv.js +0 -1
  196. package/src/assets/web-panel/assets/devWarning-kB0nCNwN.js +0 -1
  197. package/src/assets/web-panel/assets/index-Bya64sZE.js +0 -1
  198. 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
- * The runtime emits one content-free OTLP metric for each migrated command
5
- * invocation. This module consumes those OTLP payloads and combines them with
6
- * an explicit collector coverage statement. Alias removal is fail-closed:
7
- * missing release cycles, weak coverage, undocumented sampling bias or invalid
8
- * metric points can only produce `insufficient-data`.
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.v1";
19
+ "chainlesschain.command-lifecycle-report.v2";
13
20
  export const COMMAND_LIFECYCLE_COVERAGE_SCHEMA =
14
- "chainlesschain.command-lifecycle-coverage.v1";
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 attributesFor(point) {
77
- if (!Array.isArray(point?.attributes)) return null;
336
+ function attributeMap(items, { lifecycle = false } = {}) {
337
+ if (!Array.isArray(items)) return lifecycle ? null : {};
78
338
  const attributes = {};
79
- for (const item of point.attributes) {
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
- attributes[item.key] = otlpValue(item.value);
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 raw = Object.hasOwn(point || {}, "asInt")
95
- ? point.asInt
96
- : point?.asDouble;
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 > 0 ? value : null;
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: { invocations: 0, completed: 0, error: 0 },
131
- replacementRoute: { invocations: 0, completed: 0, error: 0 },
409
+ legacy: blankCounts(),
410
+ replacementRoute: blankCounts(),
132
411
  versions: new Set(),
412
+ minors: new Map(),
133
413
  };
134
414
  }
135
415
 
136
- function validateCoverage(coverage, policy) {
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) blockers.push("decision-version-invalid");
459
+ if (!decisionVersion || decisionVersion.prerelease !== null) {
460
+ blockers.push("decision-version-invalid");
461
+ }
144
462
 
145
- const observedReleases = Array.isArray(coverage?.observedReleases)
146
- ? coverage.observedReleases.map(parseRelease)
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
- observedReleases.length === 0 ||
150
- observedReleases.some((release) => !release)
491
+ !startRelease ||
492
+ !endRelease ||
493
+ startRelease.prerelease !== null ||
494
+ endRelease.prerelease !== null ||
495
+ compareReleases(startRelease, endRelease) > 0
151
496
  ) {
152
- blockers.push("observed-releases-invalid");
497
+ blockers.push("observation-release-bounds-invalid");
153
498
  }
154
- const observedMinorCycles = new Set(
155
- observedReleases
156
- .filter(Boolean)
157
- .map((release) => `${release.major}.${release.minor}`),
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
- if (observedMinorCycles.size < policy.minimumObservedMinorCycles) {
160
- blockers.push("release-observation-window-incomplete");
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 startMs = Date.parse(coverage?.window?.startedAt || "");
164
- const endMs = Date.parse(coverage?.window?.endedAt || "");
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
- !Number.isFinite(startMs) ||
167
- !Number.isFinite(endMs) ||
168
- endMs <= startMs
617
+ startEvidence &&
618
+ startedAt &&
619
+ startEvidence.npmPublishedAt !== startedAt.value
169
620
  ) {
170
- blockers.push("observation-window-invalid");
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
- const eligibleInstallations = Number(collector.eligibleInstallations);
175
- const reportingInstallations = Number(collector.reportingInstallations);
176
- const sampleRate = Number(collector.sampleRate);
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.isFinite(eligibleInstallations) && eligibleInstallations > 0
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
- typeof bias.description !== "string"
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
- observedReleases: observedReleases
240
- .filter(Boolean)
241
- .map((release) => release.value),
242
- observedMinorCycles: [...observedMinorCycles].sort(),
243
- window: {
244
- startedAt: Number.isFinite(startMs)
245
- ? new Date(startMs).toISOString()
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
- endedAt: Number.isFinite(endMs) ? new Date(endMs).toISOString() : null,
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 decisionFor(command, coverage, ingestionReady, policy) {
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 observedForCommand = coverage.observedReleases.filter((version) => {
302
- const afterDeprecation = compareReleases(version, command.deprecatedSince);
303
- const beforeDecision = compareReleases(version, coverage.decisionVersion);
304
- return (
305
- afterDeprecation !== null && afterDeprecation >= 0 && beforeDecision <= 0
306
- );
307
- });
308
- const observedCycles = new Set(
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: [...observedCycles].sort(),
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: [...observedCycles].sort(),
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: [...observedCycles].sort(),
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: [...observedCycles].sort(),
1487
+ observedMinorCycles: observedCycles,
357
1488
  };
358
1489
  }
359
1490
 
360
1491
  export function buildCommandLifecycleReport({
361
- documents = [],
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 violations = [];
375
- let acceptedPoints = 0;
376
- let rejectedPoints = 0;
377
- let payloadCount = 0;
378
-
379
- for (const payload of metricPayloads(documents)) {
380
- payloadCount += 1;
381
- for (const resource of payload.resourceMetrics || []) {
382
- for (const scope of resource.scopeMetrics || []) {
383
- for (const metric of scope.metrics || []) {
384
- if (metric?.name !== COMMAND_LIFECYCLE_INVOCATION_METRIC) continue;
385
- for (const point of metric?.sum?.dataPoints || []) {
386
- const attributes = attributesFor(point);
387
- const count = pointCount(point);
388
- const command = attributes
389
- ? byCommand.get(attributes["command.name"])
390
- : null;
391
- const valid =
392
- attributes &&
393
- REQUIRED_ATTRIBUTES.every((key) =>
394
- Object.hasOwn(attributes, key),
395
- ) &&
396
- Object.keys(attributes).length === REQUIRED_ATTRIBUTES.length &&
397
- count !== null &&
398
- command &&
399
- COMMAND_NAME.test(attributes["command.name"]) &&
400
- ["legacy", "replacement"].includes(attributes["command.route"]) &&
401
- ["completed", "error"].includes(attributes["command.outcome"]) &&
402
- parseRelease(attributes["cli.version"]) &&
403
- attributes["command.deprecated_since"] ===
404
- command?.deprecatedSince &&
405
- attributes["command.removal_not_before"] ===
406
- command?.removalNotBefore;
407
- if (!valid) {
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
- ingestionReady,
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
- { retain: 0, remove: 0, "insufficient-data": 0 },
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: new Date(generatedAt).toISOString(),
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
- coverage: coverageResult,
468
- ingestion: {
469
- payloadCount,
470
- acceptedPoints,
471
- rejectedPoints,
472
- ready: ingestionReady,
473
- violations: [...new Set(violations)].sort(),
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
- `Decisions: ${report.summary.remove} remove, ${report.summary.retain} retain, ${report.summary["insufficient-data"]} insufficient-data.`,
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("");