chainlesschain 0.162.101 → 0.162.102

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 (210) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-NeD6mA1O.js → AIOps-C1k00Gsz.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Bhp7A7vO.js → ActionButton-BinI6ZDq.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DgdI84Jy.js → Analytics-Cg0jo_cd.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Qw29caa7.js → AppLayout-CClMs_AA.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-DgkzhBjz.js → Audit-BtQgvFdM.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-CiWEQoW5.js → Backup-43FRi7ZI.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-BWNbkeDF.js → BaseInput-Cfza-FZU.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-CUcj4Vyd.js → Chat-7AjHSiSC.js} +6 -6
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-bAMNCgEj.js → ChatBubbleRenderer-Bh1YNdtx.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-B0uZTW8f.js → Checkbox-DgjfqlWW.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-BPk7b-Xa.js → Codegen-CkwOBThj.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-C2cV_dw4.js → Col-Cohk5WYM.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-CDN6Yggb.js → Community-D3LMGh3F.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-C4WPtW_V.js → Compact-DqoMJmh0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-BGUfeH-3.js → Compliance-BVjWZlEj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CsnVgF-o.js → Cowork-C0lqQPqu.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron--rGanASk.js → Cron-DHd_PJZS.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-D_CLrd-0.js → Crosschain-BphK2Q9u.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-iYkbVc8W.js → DID-CWIw0aG4.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-CuFMU98t.js → Dashboard-CKgDU9U6.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-BPSjv_Ye.js → Dropdown-DkIlDUXF.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-DBmk7czO.js → EmailListRenderer-5sXA9hwZ.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BaCArN11.js → FamilyGuardDashboard-CgcdJLH7.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-DsTV-8Ca.js → Federation-DPOcJISA.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-C6fRr_6F.js → FormItemContext-DxOutBMJ.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-AfuZCfyv.js → GenericCardRenderer-BYVNtkcd.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-CAFIrGhb.js → Git-B37LKxOu.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-C-NaxDs4.js → Governance-CnKcx5un.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-BPfeNZP7.js → Inference-CbRAWN3_.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-DDUBte_m.js → KnowledgeGraph-D0w6RfE_.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-DWysA6JP.js → Logs-DxBDKpzq.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-CAK68hXc.js → Marketplace-DNLi6O_3.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-CNmmoDAk.js → McpTools-s0sBCPXw.js} +4 -4
  35. package/src/assets/web-panel/assets/{Memory-D_KmFoui.js → Memory-BdSEgpmP.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-CaY-6kjj.js → MobileBridge-Dz7nz4K9.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileProjects-DakdA2PC.js +1 -0
  38. package/src/assets/web-panel/assets/{Mtc-YnLrtuXT.js → Mtc-Be9OBypk.js} +6 -6
  39. package/src/assets/web-panel/assets/{MtcAudit-D38v836u.js → MtcAudit-KI2ih4Tl.js} +4 -4
  40. package/src/assets/web-panel/assets/{Multisig-Dv3ad9S4.js → Multisig-Cg3jHFjL.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-MYTh2eWC.js → NLProgramming-BepHx8fU.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DYO-0Hva.js → Notes-2f05HyqN.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-D39gOb3h.js → NotificationSettings-Blwio_G5.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-CXdO0z3I.js → OrderTableRenderer-D7HJfvo_.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-DOYTwQwt.js → Organization-Uihlz1cI.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-Cm-go0yU.js → Overflow-gZx8kADL.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-CakGtorw.js → P2P-DqEEGqba.js} +2 -2
  48. package/src/assets/web-panel/assets/PdhVaultBrowser-BHUi1nb5.js +7 -0
  49. package/src/assets/web-panel/assets/{Permissions-C_podo04.js → Permissions-ERF2ZQ3G.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-nBbehIKp.js → PersonalDataHub-CzDy9CFB.js} +3 -3
  51. package/src/assets/web-panel/assets/{Pipeline-CGzQUoKv.js → Pipeline-Dy4x9Ejw.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-BuD-Dd9m.js → Privacy-C6UJys52.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-CLcFmGXU.js → ProjectInit-Bmi027fa.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-BxXTR0dH.js → ProjectSettings-NinSZmGA.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-DkpBjAfd.js → Projects-DeW-p-hT.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DwHK42ht.js → Providers-4hxrBTLo.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-idJPKErk.js → QuickAsk-B5inZlW8.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-BvJj0Nu2.js → Recommend-7lpBm00-.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-BK9l4Rcf.js → Reputation-QfdGZxYO.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-rLxg7xk5.js → Row-DqHC7BCT.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-Cw6ZcHBY.js → RssFeed-B3F1aGP6.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-D8mKp506.js → Search-BXtjuoYO.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-CXunojmQ.js → Security-Iy0MOV-A.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-BE5DgmPl.js → Services-C_puMp_6.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-CssL5wmE.js → Skeleton-Bg6mTVZU.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-zyS_1YrT.js → Skills-D4wXhanR.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-DIVn2Roa.js → Sla-Cgr2B9R6.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-CjNdZWdj.js → SpeechSettings-ctYqZEY5.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-BCbu5WQT.js → SyncSettings-C3qWh9y4.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-BjFrgXge.js → Tasks-D3AAgUx1.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-Cl4Y3lKN.js → Templates-Bm_Dyz8T.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-k1T0F_eH.js → Tenant-DOSrOw9X.js} +1 -1
  73. package/src/assets/web-panel/assets/Terminal-DrG0bEOS.js +3 -0
  74. package/src/assets/web-panel/assets/TimelineRenderer-BB8Qq6Cx.js +1 -0
  75. package/src/assets/web-panel/assets/{Tokens-BUWEiyab.js → Tokens-DqyF_Flh.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-9srMXwpo.js → Trigger-ClwQC2hP.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-hmGor_R2.js → Trust-DGQSLPQ0.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-Dst7K10a.js → UkeySign-BAkeYsUO.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-D_utUv1u.js → VideoEditing-WyUxS65Y.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-BpYTTseI.js → Wallet-Br7GdjEg.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-CEIw8QrF.js → WebAuthn-bKs1dahj.js} +4 -4
  82. package/src/assets/web-panel/assets/{WorkflowEditor-Bkb4bUZ_.js → WorkflowEditor-CVqAALYe.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-CTqDgGRH.js → chat-BDGDRbab.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-CnJHVFLk.js → colors-DIUxZwT8.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-D4-v0ahl.js → compact-item-C2iM4GmN.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-CYDbnHZX.js → createContext-D27NeHwO.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-Daz0PHhS.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-CbYwlEz5.js → hasIn-D1lXdsFj.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-BR4facv0.js → index-B5ZB5lsV.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-DMKNVIqJ.js → index-B6kc4-nD.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-Cl7-mbsb.js → index-B8POWlbO.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-CVc_5aBF.js → index-BE9bNvdn.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BCQ4kcL3.js → index-BIVYPlpl.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B8uhiRML.js → index-BTUFhfHs.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-qNwkSspX.js → index-BTedVe_v.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-OqmUv5QO.js → index-Bi8u5GsP.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CP-UKwu8.js → index-Bil9t91t.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CfshMjFN.js → index-BqbjsJcS.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-tmNsJzQU.js → index-CAnMUhHO.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BOUSaCsn.js → index-CDzSUTpa.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Dl_NdY39.js → index-CEO9vhv7.js} +3 -3
  102. package/src/assets/web-panel/assets/{index-DxvFrsxl.js → index-CFEwtE3q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-C2fICr5d.js → index-CFY1KjB1.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-5NXdAIZK.js → index-CKMDgjfh.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BR0FfrCL.js → index-CNk98bb1.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DaCCLe7i.js → index-Ca8n97h9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BGUOVPjO.js → index-Cb9u9MMo.js} +1 -1
  108. package/src/assets/web-panel/assets/index-Cbe_eJAR.js +1 -0
  109. package/src/assets/web-panel/assets/{index-CxKdSB-p.js → index-Cbqr8kW6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CY971z_9.js → index-CfR3Pvsu.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DVKfa-z4.js → index-Cgf3zMwq.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-4767GEls.js → index-CiA-L5ld.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CK8nROWH.js → index-CoygRreE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B-hiXmVK.js → index-CsIw-YQF.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-wxz2s6uW.js → index-DFhdJm7M.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-8cmtr1jC.js → index-DK0jgmJg.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BygyTsjr.js → index-Dc4k4V1u.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-D8MYVsVB.js → index-DfSav-yQ.js} +1 -1
  119. package/src/assets/web-panel/assets/index-DgcPjW1c.js +1 -0
  120. package/src/assets/web-panel/assets/{index-xDe5BkjU.js → index-DkOu8er7.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-C4chfOjw.js → index-DnCVI295.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BNz3yht2.js → index-Do13fQ5W.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DdRtpQFq.js → index-DrS-Mrm-.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B78d_-pR.js → index-ET5LYk9B.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-YuO8gxKb.js → index-HZGu1woY.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DjFQuno4.js → index-Y1rkRas5.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-dKpwhR5x.js → index-yIM-KxVm.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-qCQgpAty.js → initDefaultProps-C0Yrc6yY.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-SkVSGdwu.js → motion-DnOI85ek.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-BpWxaW_x.js → move-DT9zQPZp.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-DlZWM2H4.js → omit-DFtDi5Wd.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-DobtUnVe.js → pickAttrs-Cobkb_rd.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-CD6mYRZi.js → placementArrow-DWQDu4i-.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-DvIIMTD_.js → responsiveObserve-daoQcWcX.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-CVgyUfaA.js → slide-BLkzd2OV.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-DrUDnjy_.js → statusUtils-Uyxi81M0.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-BNYQS60m.js → styleChecker-Dtw5lUcs.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-DYv7Tvk0.js → useFlexGapSupport-_JpMgpdt.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-DmAD2atJ.js → useFs-huwAlPD-.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-DbLVKA3I.js → usePersonalDataHub-7cafj8_f.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-C3WEt6_P.js → vnode-DYUSfRt7.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-CRoSX_oZ.js → zoom-B-tjvmer.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/a2a.js +2 -1
  145. package/src/commands/bi.js +4 -3
  146. package/src/commands/browse.js +3 -2
  147. package/src/commands/codegen.js +7 -6
  148. package/src/commands/collab.js +2 -1
  149. package/src/commands/crosschain.js +5 -4
  150. package/src/commands/dao.js +10 -10
  151. package/src/commands/dbevo.js +3 -2
  152. package/src/commands/economy.js +8 -6
  153. package/src/commands/evolution.js +10 -8
  154. package/src/commands/export.js +2 -1
  155. package/src/commands/federation.js +8 -7
  156. package/src/commands/fusion.js +4 -3
  157. package/src/commands/git.js +2 -1
  158. package/src/commands/governance.js +6 -5
  159. package/src/commands/hardening.js +3 -2
  160. package/src/commands/hmemory.js +2 -1
  161. package/src/commands/inference.js +9 -8
  162. package/src/commands/infra.js +9 -8
  163. package/src/commands/instinct.js +66 -0
  164. package/src/commands/learning.js +66 -0
  165. package/src/commands/marketplace.js +5 -4
  166. package/src/commands/matrix.js +2 -1
  167. package/src/commands/memory.js +66 -0
  168. package/src/commands/nlprog.js +4 -3
  169. package/src/commands/nostr.js +4 -3
  170. package/src/commands/note.js +2 -1
  171. package/src/commands/ops.js +3 -2
  172. package/src/commands/perception.js +6 -5
  173. package/src/commands/privacy.js +13 -12
  174. package/src/commands/quantization.js +3 -2
  175. package/src/commands/recommend.js +8 -7
  176. package/src/commands/reputation.js +4 -3
  177. package/src/commands/runtime.js +6 -5
  178. package/src/commands/sandbox.js +5 -4
  179. package/src/commands/session.js +2 -1
  180. package/src/commands/sla.js +6 -5
  181. package/src/commands/social.js +6 -5
  182. package/src/commands/stress.js +7 -6
  183. package/src/commands/sync.js +2 -1
  184. package/src/commands/tenant.js +3 -2
  185. package/src/commands/trust.js +7 -6
  186. package/src/commands/wallet.js +2 -1
  187. package/src/lib/__tests__/agents.test.js +225 -0
  188. package/src/lib/__tests__/cowork-observe-html.test.js +141 -0
  189. package/src/lib/__tests__/logger.test.js +110 -0
  190. package/src/lib/__tests__/project-inventory.test.js +280 -0
  191. package/src/lib/__tests__/project-root.test.js +96 -0
  192. package/src/lib/__tests__/provider-stream.test.js +190 -0
  193. package/src/lib/cli-arg.js +41 -0
  194. package/src/lib/cli-numeric.js +12 -3
  195. package/src/lib/instinct-manager.js +64 -0
  196. package/src/lib/learning/learning-tables.js +83 -0
  197. package/src/lib/mcp-oauth.js +10 -2
  198. package/src/lib/memory-manager.js +67 -0
  199. package/src/lib/plan-mode.js +3 -2
  200. package/src/repl/__tests__/mcp-prompt.test.js +155 -0
  201. package/src/repl/__tests__/slash-macro.test.js +81 -0
  202. package/src/runtime/__tests__/coding-agent-contract-shared.test.js +136 -0
  203. package/src/runtime/__tests__/coding-agent-events.test.js +263 -0
  204. package/src/assets/web-panel/assets/MobileProjects-DhDxrWze.js +0 -1
  205. package/src/assets/web-panel/assets/PdhVaultBrowser-BlmbHJnb.js +0 -7
  206. package/src/assets/web-panel/assets/Terminal-i0tHF8fy.js +0 -3
  207. package/src/assets/web-panel/assets/TimelineRenderer-FmPaNyqK.js +0 -1
  208. package/src/assets/web-panel/assets/devWarning-OhxeajH6.js +0 -1
  209. package/src/assets/web-panel/assets/index-Bbtoi_47.js +0 -1
  210. package/src/assets/web-panel/assets/index-Cb8RWwaB.js +0 -1
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * project-inventory tests — the offline census behind `cc init --memory`
5
+ * (previously untested). Covers readmeSynopsis (markdown synopsis extraction),
6
+ * inventoryProject (package.json / lockfile→PM / config markers / CI count /
7
+ * language census / SKIP_DIRS / workspaces both shapes / existingMemory) and
8
+ * renderMemoryFile (the generated cc.md). Driven by an in-memory fake fs over
9
+ * POSIX paths via the deps seam — no real I/O.
10
+ */
11
+
12
+ import { describe, it, expect } from "vitest";
13
+ import pathDefault from "path";
14
+ import {
15
+ readmeSynopsis,
16
+ inventoryProject,
17
+ renderMemoryFile,
18
+ SKIP_DIRS,
19
+ } from "../project-inventory.js";
20
+
21
+ const P = pathDefault.posix;
22
+
23
+ /** Build a fake fs from a { absPosixPath: content } map. Dirs are derived. */
24
+ function makeFs(files) {
25
+ const contents = new Map(Object.entries(files));
26
+ const fileSet = new Set(contents.keys());
27
+ const dirSet = new Set(["/"]);
28
+ for (const f of fileSet) {
29
+ let d = P.dirname(f);
30
+ while (d && !dirSet.has(d)) {
31
+ dirSet.add(d);
32
+ d = P.dirname(d);
33
+ }
34
+ }
35
+ const childrenOf = (dir) => {
36
+ const out = new Map(); // name -> isDir
37
+ for (const f of fileSet)
38
+ if (P.dirname(f) === dir) out.set(P.basename(f), false);
39
+ for (const d of dirSet)
40
+ if (d !== "/" && P.dirname(d) === dir) out.set(P.basename(d), true);
41
+ return out;
42
+ };
43
+ return {
44
+ readFileSync(p) {
45
+ if (!fileSet.has(p)) {
46
+ const e = new Error("ENOENT");
47
+ e.code = "ENOENT";
48
+ throw e;
49
+ }
50
+ return contents.get(p);
51
+ },
52
+ existsSync(p) {
53
+ return fileSet.has(p) || dirSet.has(p);
54
+ },
55
+ readdirSync(p, opts) {
56
+ if (!dirSet.has(p)) {
57
+ const e = new Error("ENOENT");
58
+ e.code = "ENOENT";
59
+ throw e;
60
+ }
61
+ const kids = childrenOf(p);
62
+ if (opts && opts.withFileTypes) {
63
+ return [...kids].map(([name, isDir]) => ({
64
+ name,
65
+ isDirectory: () => isDir,
66
+ isFile: () => !isDir,
67
+ }));
68
+ }
69
+ return [...kids.keys()];
70
+ },
71
+ };
72
+ }
73
+
74
+ const inv = (files, opts = {}) =>
75
+ inventoryProject("/proj", { deps: { fs: makeFs(files), path: P }, ...opts });
76
+
77
+ // --------------------------------------------------------------------------- //
78
+ // readmeSynopsis
79
+ // --------------------------------------------------------------------------- //
80
+ describe("readmeSynopsis", () => {
81
+ it("returns null for empty input", () => {
82
+ expect(readmeSynopsis(null)).toBe(null);
83
+ expect(readmeSynopsis("")).toBe(null);
84
+ });
85
+
86
+ it("skips headings, badges and bare HTML, returns first real paragraph", () => {
87
+ const md = [
88
+ "# My Project",
89
+ "![badge](x.svg)",
90
+ "[![ci](a)](b)",
91
+ "<p>html</p>",
92
+ "",
93
+ "This is the real summary line.",
94
+ "Second line of the same paragraph.",
95
+ "",
96
+ "Later paragraph ignored.",
97
+ ].join("\n");
98
+ expect(readmeSynopsis(md)).toBe(
99
+ "This is the real summary line. Second line of the same paragraph.",
100
+ );
101
+ });
102
+
103
+ it("returns null when there is no prose (only headings)", () => {
104
+ expect(readmeSynopsis("# Title\n## Sub")).toBe(null);
105
+ });
106
+
107
+ it("truncates to maxChars with an ellipsis", () => {
108
+ const out = readmeSynopsis("x".repeat(50), 10);
109
+ expect(out).toBe("xxxxxxxxxx…");
110
+ expect(out.length).toBe(11);
111
+ });
112
+ });
113
+
114
+ // --------------------------------------------------------------------------- //
115
+ // inventoryProject
116
+ // --------------------------------------------------------------------------- //
117
+ describe("inventoryProject", () => {
118
+ it("reads name/description from package.json", () => {
119
+ const r = inv({
120
+ "/proj/package.json": JSON.stringify({ name: "acme", description: "d" }),
121
+ });
122
+ expect(r.name).toBe("acme");
123
+ expect(r.description).toBe("d");
124
+ });
125
+
126
+ it("falls back to folder basename when no package.json", () => {
127
+ expect(inv({ "/proj/foo.txt": "x" }).name).toBe("proj");
128
+ });
129
+
130
+ it("detects package manager from lockfile (first match wins)", () => {
131
+ expect(inv({ "/proj/pnpm-lock.yaml": "" }).packageManager).toBe("pnpm");
132
+ expect(inv({ "/proj/yarn.lock": "" }).packageManager).toBe("yarn");
133
+ expect(inv({ "/proj/foo.txt": "" }).packageManager).toBe(null);
134
+ });
135
+
136
+ it("collects deduped tooling config markers", () => {
137
+ const r = inv({
138
+ "/proj/tsconfig.json": "{}",
139
+ "/proj/vite.config.js": "",
140
+ "/proj/vite.config.ts": "", // same label "Vite" — deduped
141
+ });
142
+ expect(r.configs).toContain("TypeScript");
143
+ expect(r.configs.filter((c) => c === "Vite").length).toBe(1);
144
+ });
145
+
146
+ it("counts CI workflow files", () => {
147
+ const r = inv({
148
+ "/proj/.github/workflows/ci.yml": "",
149
+ "/proj/.github/workflows/release.yaml": "",
150
+ "/proj/.github/workflows/notes.md": "", // not yaml
151
+ });
152
+ expect(r.ciWorkflows).toBe(2);
153
+ });
154
+
155
+ it("censuses languages by extension, sorted desc, skipping SKIP_DIRS", () => {
156
+ const r = inv({
157
+ "/proj/a.ts": "",
158
+ "/proj/src/b.ts": "",
159
+ "/proj/src/c.js": "",
160
+ "/proj/node_modules/d.ts": "", // skipped
161
+ });
162
+ const langs = Object.fromEntries(r.languages);
163
+ expect(langs.TypeScript).toBe(2); // a.ts + src/b.ts, NOT node_modules
164
+ expect(langs.JavaScript).toBe(1);
165
+ expect(r.languages[0][0]).toBe("TypeScript"); // sorted desc
166
+ });
167
+
168
+ it("tracks top-level directories with file counts", () => {
169
+ const r = inv({
170
+ "/proj/src/a.js": "",
171
+ "/proj/src/b.js": "",
172
+ "/proj/docs/c.md": "",
173
+ });
174
+ const dirs = Object.fromEntries(r.topDirs);
175
+ expect(dirs.src).toBe(2);
176
+ expect(dirs.docs).toBe(1);
177
+ });
178
+
179
+ it("reads workspaces in both array and {packages} shapes", () => {
180
+ expect(
181
+ inv({ "/proj/package.json": JSON.stringify({ workspaces: ["a", "b"] }) })
182
+ .workspaces,
183
+ ).toEqual(["a", "b"]);
184
+ expect(
185
+ inv({
186
+ "/proj/package.json": JSON.stringify({
187
+ workspaces: { packages: ["x"] },
188
+ }),
189
+ }).workspaces,
190
+ ).toEqual(["x"]);
191
+ });
192
+
193
+ it("lists scripts and existing memory files", () => {
194
+ const r = inv({
195
+ "/proj/package.json": JSON.stringify({
196
+ scripts: { build: "tsc", test: "vitest" },
197
+ }),
198
+ "/proj/CLAUDE.md": "x",
199
+ });
200
+ expect(r.scripts).toEqual([
201
+ ["build", "tsc"],
202
+ ["test", "vitest"],
203
+ ]);
204
+ expect(r.existingMemory).toContain("CLAUDE.md");
205
+ });
206
+
207
+ it("derives synopsis from README.md", () => {
208
+ const r = inv({ "/proj/README.md": "# T\n\nHello world synopsis." });
209
+ expect(r.synopsis).toBe("Hello world synopsis.");
210
+ });
211
+
212
+ it("respects maxDepth (deep files beyond cap not counted)", () => {
213
+ const r = inv({ "/proj/a/b/c/deep.ts": "" }, { maxDepth: 1 });
214
+ expect(Object.fromEntries(r.languages).TypeScript).toBeUndefined();
215
+ });
216
+
217
+ it("SKIP_DIRS includes the usual heavy directories", () => {
218
+ expect(SKIP_DIRS.has("node_modules")).toBe(true);
219
+ expect(SKIP_DIRS.has("target")).toBe(true);
220
+ });
221
+ });
222
+
223
+ // --------------------------------------------------------------------------- //
224
+ // renderMemoryFile
225
+ // --------------------------------------------------------------------------- //
226
+ describe("renderMemoryFile", () => {
227
+ const base = {
228
+ name: "acme",
229
+ description: null,
230
+ synopsis: "A cool tool.",
231
+ packageManager: "npm",
232
+ configs: ["TypeScript", "Vitest"],
233
+ ciWorkflows: 3,
234
+ workspaces: ["packages/*"],
235
+ scripts: [
236
+ ["build", "tsc"],
237
+ ["test", "vitest"],
238
+ ],
239
+ languages: [
240
+ ["TypeScript", 10],
241
+ ["JavaScript", 2],
242
+ ],
243
+ topDirs: [["src", 10]],
244
+ truncated: false,
245
+ existingMemory: ["CLAUDE.md"],
246
+ };
247
+
248
+ it("renders title, overview, stack, commands and layout", () => {
249
+ const md = renderMemoryFile(base, { date: "2026-06-22" });
250
+ expect(md).toContain("# acme — Project Memory");
251
+ expect(md).toContain("2026-06-22");
252
+ expect(md).toContain("A cool tool.");
253
+ expect(md).toContain("TypeScript (10), JavaScript (2)");
254
+ expect(md).toContain("Package manager: npm");
255
+ expect(md).toContain("CI: GitHub Actions (3 workflows)");
256
+ expect(md).toContain("`npm run build` — `tsc`");
257
+ expect(md).toContain("Workspaces: packages/*");
258
+ expect(md).toContain("`src/` — 10 files");
259
+ });
260
+
261
+ it("imports existing memory files via @-include (excluding cc.md)", () => {
262
+ const md = renderMemoryFile({
263
+ ...base,
264
+ existingMemory: ["cc.md", "CLAUDE.md"],
265
+ });
266
+ expect(md).toContain("@CLAUDE.md");
267
+ expect(md).not.toContain("@cc.md");
268
+ });
269
+
270
+ it("caps the script list at 12 with a remainder note", () => {
271
+ const scripts = Array.from({ length: 15 }, (_, i) => [`s${i}`, `cmd${i}`]);
272
+ const md = renderMemoryFile({ ...base, scripts });
273
+ expect(md).toContain("… 3 more scripts");
274
+ });
275
+
276
+ it("notes a truncated scan", () => {
277
+ const md = renderMemoryFile({ ...base, truncated: true });
278
+ expect(md).toContain("scan truncated");
279
+ });
280
+ });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * project-root tests (previously untested) — the git-root walk shared by the
5
+ * .claude/.chainlesschain discovery layers (permission rules, output-styles,
6
+ * slash-commands, sub-agents). A bug here means a subdir run silently drops the
7
+ * project's config (a safety gap), so the walk + defensive bail-to-null branches
8
+ * are pinned here. Fake fs/path via the deps seam; no real I/O.
9
+ */
10
+
11
+ import { describe, it, expect } from "vitest";
12
+ import pathDefault from "path";
13
+ import { createRequire } from "module";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { findGitProjectRoot, projectRootBase } = require("../project-root.cjs");
17
+
18
+ const P = pathDefault.posix;
19
+
20
+ /** fs whose existsSync(`${dir}/.git`) is true for dirs in the given set. */
21
+ function fsWithGit(gitDirs, { throwAt } = {}) {
22
+ const set = new Set(gitDirs);
23
+ return {
24
+ existsSync(p) {
25
+ if (throwAt && p === throwAt) throw new Error("EACCES");
26
+ return p.endsWith("/.git") && set.has(p.slice(0, -"/.git".length));
27
+ },
28
+ };
29
+ }
30
+
31
+ describe("findGitProjectRoot", () => {
32
+ it("returns cwd when .git is right there", () => {
33
+ expect(
34
+ findGitProjectRoot("/proj", { fs: fsWithGit(["/proj"]), path: P }),
35
+ ).toBe("/proj");
36
+ });
37
+
38
+ it("walks up to the nearest ancestor containing .git", () => {
39
+ expect(
40
+ findGitProjectRoot("/proj/src/app", {
41
+ fs: fsWithGit(["/proj"]),
42
+ path: P,
43
+ }),
44
+ ).toBe("/proj");
45
+ });
46
+
47
+ it("returns null when no .git is found", () => {
48
+ expect(findGitProjectRoot("/a/b/c", { fs: fsWithGit([]), path: P })).toBe(
49
+ null,
50
+ );
51
+ });
52
+
53
+ it("keeps walking when existsSync throws on an intermediate dir", () => {
54
+ // .git lives at /proj; checking /proj/src/.git throws but the walk continues
55
+ const fs = fsWithGit(["/proj"], { throwAt: "/proj/src/.git" });
56
+ expect(findGitProjectRoot("/proj/src/app", { fs, path: P })).toBe("/proj");
57
+ });
58
+
59
+ it("bails to null when fs has no existsSync", () => {
60
+ expect(findGitProjectRoot("/proj", { fs: {}, path: P })).toBe(null);
61
+ });
62
+
63
+ it("bails to null when path lacks dirname/join", () => {
64
+ expect(
65
+ findGitProjectRoot("/proj", { fs: fsWithGit(["/proj"]), path: {} }),
66
+ ).toBe(null);
67
+ });
68
+
69
+ it("works without path.resolve (falls back to String(cwd))", () => {
70
+ const noResolve = { dirname: P.dirname, join: P.join };
71
+ expect(
72
+ findGitProjectRoot("/proj/sub", {
73
+ fs: fsWithGit(["/proj"]),
74
+ path: noResolve,
75
+ }),
76
+ ).toBe("/proj");
77
+ });
78
+ });
79
+
80
+ describe("projectRootBase", () => {
81
+ it("returns the root when cwd is a strict subdirectory", () => {
82
+ expect(
83
+ projectRootBase("/proj/src", { fs: fsWithGit(["/proj"]), path: P }),
84
+ ).toBe("/proj");
85
+ });
86
+
87
+ it("returns null when cwd IS the root (own dirs already cover it)", () => {
88
+ expect(
89
+ projectRootBase("/proj", { fs: fsWithGit(["/proj"]), path: P }),
90
+ ).toBe(null);
91
+ });
92
+
93
+ it("returns null when no root is found", () => {
94
+ expect(projectRootBase("/x/y", { fs: fsWithGit([]), path: P })).toBe(null);
95
+ });
96
+ });
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * provider-stream tests (previously untested) — the token-stream parsers behind
5
+ * `cc stream` and the Hosted Session stream.run route. Pins the bug-prone
6
+ * streaming parse logic: Ollama NDJSON (yield response, stop on done, line
7
+ * buffering across chunk boundaries, skip malformed) and OpenAI-style SSE
8
+ * (yield delta.content, stop on [DONE], skip non-data/empty), error on non-ok,
9
+ * plus buildProviderSource routing (ollama defaults / known-provider endpoint +
10
+ * Authorization / unsupported / missing-key). fetch is stubbed; no network.
11
+ */
12
+
13
+ import { describe, it, expect, afterEach, vi } from "vitest";
14
+ import {
15
+ ollamaTokenStream,
16
+ openAIStream,
17
+ buildProviderSource,
18
+ } from "../provider-stream.js";
19
+
20
+ afterEach(() => {
21
+ vi.unstubAllGlobals();
22
+ vi.unstubAllEnvs();
23
+ });
24
+
25
+ /** A fake fetch whose response body streams the given string chunks. */
26
+ function mockFetch(
27
+ chunks,
28
+ { ok = true, status = 200, statusText = "OK" } = {},
29
+ ) {
30
+ return vi.fn(async () => {
31
+ const enc = new TextEncoder();
32
+ let i = 0;
33
+ return {
34
+ ok,
35
+ status,
36
+ statusText,
37
+ body: {
38
+ getReader: () => ({
39
+ read: async () =>
40
+ i < chunks.length
41
+ ? { value: enc.encode(chunks[i++]), done: false }
42
+ : { value: undefined, done: true },
43
+ }),
44
+ },
45
+ };
46
+ });
47
+ }
48
+
49
+ async function collect(gen) {
50
+ const out = [];
51
+ for await (const t of gen) out.push(t);
52
+ return out;
53
+ }
54
+
55
+ // --------------------------------------------------------------------------- //
56
+ // ollamaTokenStream
57
+ // --------------------------------------------------------------------------- //
58
+ describe("ollamaTokenStream", () => {
59
+ it("yields responses across NDJSON lines and stops on done", async () => {
60
+ vi.stubGlobal(
61
+ "fetch",
62
+ mockFetch([
63
+ '{"response":"He"}\n{"response":"llo"}\n',
64
+ '{"response":"!"}\n{"done":true}\n',
65
+ '{"response":"AFTER"}\n', // never read — returned on done
66
+ ]),
67
+ );
68
+ const out = await collect(
69
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
70
+ );
71
+ expect(out).toEqual(["He", "llo", "!"]);
72
+ });
73
+
74
+ it("buffers a JSON line split across chunk boundaries", async () => {
75
+ vi.stubGlobal(
76
+ "fetch",
77
+ mockFetch(['{"resp', 'onse":"hi"}\n{"done":true}\n']),
78
+ );
79
+ expect(
80
+ await collect(
81
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
82
+ ),
83
+ ).toEqual(["hi"]);
84
+ });
85
+
86
+ it("skips malformed JSON lines", async () => {
87
+ vi.stubGlobal(
88
+ "fetch",
89
+ mockFetch(['not json\n{"response":"ok"}\n{"done":true}\n']),
90
+ );
91
+ expect(
92
+ await collect(
93
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
94
+ ),
95
+ ).toEqual(["ok"]);
96
+ });
97
+
98
+ it("throws on a non-ok response", async () => {
99
+ vi.stubGlobal(
100
+ "fetch",
101
+ mockFetch([], { ok: false, status: 500, statusText: "Err" }),
102
+ );
103
+ await expect(
104
+ collect(
105
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
106
+ ),
107
+ ).rejects.toThrow(/500/);
108
+ });
109
+ });
110
+
111
+ // --------------------------------------------------------------------------- //
112
+ // openAIStream
113
+ // --------------------------------------------------------------------------- //
114
+ describe("openAIStream", () => {
115
+ const args = { baseUrl: "http://x", apiKey: "k", model: "m", prompt: "p" };
116
+
117
+ it("yields delta.content from data lines and stops on [DONE]", async () => {
118
+ vi.stubGlobal(
119
+ "fetch",
120
+ mockFetch([
121
+ 'data: {"choices":[{"delta":{"content":"A"}}]}\n',
122
+ 'data: {"choices":[{"delta":{"content":"B"}}]}\ndata: [DONE]\n',
123
+ 'data: {"choices":[{"delta":{"content":"AFTER"}}]}\n',
124
+ ]),
125
+ );
126
+ expect(await collect(openAIStream(args))).toEqual(["A", "B"]);
127
+ });
128
+
129
+ it("skips non-data, empty and content-less delta lines", async () => {
130
+ vi.stubGlobal(
131
+ "fetch",
132
+ mockFetch([
133
+ ": comment\n\n",
134
+ 'data: {"choices":[{"delta":{}}]}\n', // no content
135
+ 'data: {"choices":[{"delta":{"content":"x"}}]}\ndata: [DONE]\n',
136
+ ]),
137
+ );
138
+ expect(await collect(openAIStream(args))).toEqual(["x"]);
139
+ });
140
+
141
+ it("throws on a non-ok response", async () => {
142
+ vi.stubGlobal(
143
+ "fetch",
144
+ mockFetch([], { ok: false, status: 401, statusText: "No" }),
145
+ );
146
+ await expect(collect(openAIStream(args))).rejects.toThrow(/401/);
147
+ });
148
+ });
149
+
150
+ // --------------------------------------------------------------------------- //
151
+ // buildProviderSource
152
+ // --------------------------------------------------------------------------- //
153
+ describe("buildProviderSource", () => {
154
+ it("routes ollama to /api/generate with localhost default", async () => {
155
+ const fetchMock = mockFetch(['{"done":true}\n']);
156
+ vi.stubGlobal("fetch", fetchMock);
157
+ await collect(buildProviderSource("ollama", { prompt: "hi" }));
158
+ expect(fetchMock).toHaveBeenCalledWith(
159
+ "http://localhost:11434/api/generate",
160
+ expect.objectContaining({ method: "POST" }),
161
+ );
162
+ });
163
+
164
+ it("routes a known provider to /chat/completions with a Bearer key", async () => {
165
+ const fetchMock = mockFetch(["data: [DONE]\n"]);
166
+ vi.stubGlobal("fetch", fetchMock);
167
+ await collect(
168
+ buildProviderSource("deepseek", { apiKey: "sk-x", prompt: "hi" }),
169
+ );
170
+ expect(fetchMock).toHaveBeenCalledWith(
171
+ "https://api.deepseek.com/v1/chat/completions",
172
+ expect.objectContaining({
173
+ headers: expect.objectContaining({ Authorization: "Bearer sk-x" }),
174
+ }),
175
+ );
176
+ });
177
+
178
+ it("throws for an unsupported provider", () => {
179
+ expect(() => buildProviderSource("nope", { prompt: "x" })).toThrow(
180
+ /Unsupported provider/,
181
+ );
182
+ });
183
+
184
+ it("throws when a known provider has no api key", () => {
185
+ vi.stubEnv("DEEPSEEK_API_KEY", "");
186
+ expect(() => buildProviderSource("deepseek", { prompt: "x" })).toThrow(
187
+ /API key/,
188
+ );
189
+ });
190
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * cli-arg — validating commander option coercers.
3
+ *
4
+ * Bare `parseInt` / `parseFloat` / `Number` used as a commander coercer
5
+ * (`.option("--limit <n>", "...", parseInt)`) silently yield NaN on bad input
6
+ * (`--limit abc`), threading NaN into whatever the action does with the option.
7
+ * These factories validate at parse time and raise a commander
8
+ * `InvalidArgumentError`, so a typo fails with the standard
9
+ * `error: option '--limit <n>' argument 'abc' is invalid` message and exit 1 —
10
+ * instead of a silently-corrupt run.
11
+ *
12
+ * Drop-in replacements:
13
+ * .option("--limit <n>", "...", parseInt) -> intArg("--limit")
14
+ * .option("--rate <n>", "...", parseFloat) -> floatArg("--rate")
15
+ * .option("--eps <n>", "...", Number) -> floatArg("--eps")
16
+ *
17
+ * Behaviour for valid and omitted values is identical to the bare coercer
18
+ * (the coercer only runs when the option is supplied with a value); only the
19
+ * NaN-on-bad-input case changes.
20
+ *
21
+ * @param {string} name option label for the message (e.g. "--limit")
22
+ * @param {{min?: number, max?: number}} [opts]
23
+ */
24
+ import { InvalidArgumentError } from "commander";
25
+ import { numericOption } from "./cli-numeric.js";
26
+
27
+ function makeArg(integer) {
28
+ return (name, opts = {}) =>
29
+ (raw) => {
30
+ try {
31
+ return numericOption(raw, { ...opts, name, integer });
32
+ } catch (err) {
33
+ throw new InvalidArgumentError(err.message);
34
+ }
35
+ };
36
+ }
37
+
38
+ export const intArg = makeArg(true);
39
+ export const floatArg = makeArg(false);
40
+
41
+ export default { intArg, floatArg };
@@ -10,7 +10,9 @@
10
10
  * @param {*} raw the option value (string from commander, or undefined)
11
11
  * @param {object} [opts]
12
12
  * @param {string} [opts.name="value"] label for error messages (e.g. "--limit")
13
- * @param {boolean} [opts.integer=false] parse as integer (parseInt) vs float (Number)
13
+ * @param {boolean} [opts.integer=false] require a whole number (rejects 1.5);
14
+ * parsing is strict either way (Number,
15
+ * not parseInt — "12abc" is rejected)
14
16
  * @param {number} [opts.min] inclusive lower bound
15
17
  * @param {number} [opts.max] inclusive upper bound
16
18
  * @param {number} [opts.fallback] returned for missing/invalid input; when
@@ -26,8 +28,15 @@ export function numericOption(raw, opts = {}) {
26
28
  };
27
29
 
28
30
  if (raw == null || raw === "") return fail(`${name} is required`);
29
- const n = integer ? parseInt(raw, 10) : Number(raw);
30
- if (!Number.isFinite(n)) {
31
+ if (typeof raw === "string" && raw.trim() === "")
32
+ return fail(`${name} is required`);
33
+ // Number() (NOT parseInt) so trailing junk is rejected instead of silently
34
+ // truncated: parseInt("12abc") === 12 and parseInt("0x10", 10) === 0 would
35
+ // slip a corrupt value through. Number("12abc") is NaN; whole numbers given
36
+ // via exponent ("1e3" -> 1000) still parse, and a fractional value is
37
+ // rejected in integer mode.
38
+ const n = Number(raw);
39
+ if (!Number.isFinite(n) || (integer && !Number.isInteger(n))) {
31
40
  return fail(
32
41
  `${name} must be a ${integer ? "whole " : ""}number (got ${JSON.stringify(String(raw))})`,
33
42
  );