@remnic/core 9.63.6 → 9.63.7

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 (262) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +27 -25
  3. package/dist/access-authorization-probe.d.ts +1 -1
  4. package/dist/access-authorization-probe.js +28 -26
  5. package/dist/access-boundary.d.ts +1 -1
  6. package/dist/access-boundary.js +27 -25
  7. package/dist/access-cli.js +83 -81
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +1 -1
  10. package/dist/access-extraction-force-flush.js +27 -25
  11. package/dist/access-http-lcm-compaction.d.ts +1 -1
  12. package/dist/access-http-lifecycle-flush.d.ts +1 -1
  13. package/dist/access-http-offline-stream.d.ts +1 -1
  14. package/dist/access-http-query.js +29 -27
  15. package/dist/access-http.d.ts +2 -2
  16. package/dist/access-http.js +43 -41
  17. package/dist/access-identity-continuity-surface.js +27 -25
  18. package/dist/access-lcm-surface.d.ts +1 -1
  19. package/dist/access-lcm-surface.js +27 -25
  20. package/dist/access-mcp.d.ts +1 -1
  21. package/dist/access-mcp.js +34 -32
  22. package/dist/access-namespace-preflight.js +27 -25
  23. package/dist/access-observe-write-surface.d.ts +1 -1
  24. package/dist/access-observe-write-surface.js +27 -25
  25. package/dist/access-offline-manifest.js +6 -5
  26. package/dist/access-operations-batch.js +29 -27
  27. package/dist/access-operations.d.ts +4 -4
  28. package/dist/access-operations.js +33 -31
  29. package/dist/access-recall-concurrency.d.ts +1 -1
  30. package/dist/access-recall-concurrency.js +27 -25
  31. package/dist/access-recall-response.d.ts +1 -1
  32. package/dist/access-recall-response.js +27 -25
  33. package/dist/access-recall-surface.d.ts +1 -1
  34. package/dist/access-recall-surface.js +27 -25
  35. package/dist/access-schema.d.ts +88 -88
  36. package/dist/access-schema.js +6 -5
  37. package/dist/access-service-helpers.d.ts +1 -1
  38. package/dist/access-service.d.ts +1 -1
  39. package/dist/access-service.js +27 -25
  40. package/dist/access-surface-catalog.d.ts +1 -1
  41. package/dist/access-surface-catalog.js +29 -27
  42. package/dist/access-surface-catalog.js.map +1 -1
  43. package/dist/active-recall.js +5 -3
  44. package/dist/active-recall.js.map +1 -1
  45. package/dist/adapters/index.js +4 -4
  46. package/dist/adapters/registry.js +2 -2
  47. package/dist/{auto-sync-BKRJDMKX.js → auto-sync-3SHMZVQS.js} +5 -3
  48. package/dist/{auto-sync-BKRJDMKX.js.map → auto-sync-3SHMZVQS.js.map} +1 -1
  49. package/dist/briefing.js +10 -10
  50. package/dist/calibration.d.ts +2 -1
  51. package/dist/calibration.js +7 -0
  52. package/dist/calibration.js.map +1 -1
  53. package/dist/causal-consolidation.js +11 -11
  54. package/dist/census-validation.d.ts +19 -0
  55. package/dist/census-validation.js +14 -0
  56. package/dist/census-validation.js.map +1 -0
  57. package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
  58. package/dist/{chunk-Z4N7SVVF.js → chunk-2L7DKNVE.js} +12 -7
  59. package/dist/chunk-2L7DKNVE.js.map +1 -0
  60. package/dist/{chunk-3Z5TGRFA.js → chunk-3NZAVWU5.js} +9 -9
  61. package/dist/{chunk-I5FLIB2J.js → chunk-3S3PNR5H.js} +2 -2
  62. package/dist/{chunk-R6UMNP5C.js → chunk-3T6HIIKA.js} +2 -2
  63. package/dist/{chunk-77QZW2D6.js → chunk-3TDAA7VA.js} +2 -2
  64. package/dist/{chunk-TWTUGCTY.js → chunk-3YTTAGPB.js} +6 -1
  65. package/dist/chunk-3YTTAGPB.js.map +1 -0
  66. package/dist/{chunk-DXDAQZ2K.js → chunk-42TIIO2M.js} +2 -2
  67. package/dist/{chunk-Y7COCXY2.js → chunk-4F67JJPG.js} +2 -2
  68. package/dist/{chunk-PB4WVYGC.js → chunk-5S5DUJFJ.js} +2 -2
  69. package/dist/{chunk-KYTRF64R.js → chunk-5SCFDQEJ.js} +2 -2
  70. package/dist/{chunk-GG4DN63W.js → chunk-62Y5AGCM.js} +2 -2
  71. package/dist/{chunk-KHCUVYCY.js → chunk-6UKSOXAO.js} +3 -3
  72. package/dist/{chunk-F6JYBVL6.js → chunk-7ZAC2C2U.js} +24 -24
  73. package/dist/{chunk-GF7RQWXC.js → chunk-AJRJDXYR.js} +2 -2
  74. package/dist/{chunk-YEQCMA5L.js → chunk-BOSIHHOH.js} +2 -2
  75. package/dist/{chunk-7LF2OHIJ.js → chunk-BRHKW7GY.js} +2 -2
  76. package/dist/{chunk-LX4TNSAE.js → chunk-CBY54QYW.js} +4 -4
  77. package/dist/chunk-FEHIOYML.js +168 -0
  78. package/dist/chunk-FEHIOYML.js.map +1 -0
  79. package/dist/{chunk-6YMEU6OQ.js → chunk-G72IPMR7.js} +2 -2
  80. package/dist/{chunk-7AF243WZ.js → chunk-GBVRVYF4.js} +1 -1
  81. package/dist/{chunk-GTD4Q62X.js → chunk-H4GQ2Y24.js} +3 -3
  82. package/dist/{chunk-QI5RDDEL.js → chunk-HXZNTZ3Y.js} +6 -6
  83. package/dist/{chunk-CIAI24TQ.js → chunk-IBZQQOM5.js} +3 -3
  84. package/dist/{chunk-RYST72N5.js → chunk-IINVXO3U.js} +4 -154
  85. package/dist/chunk-IINVXO3U.js.map +1 -0
  86. package/dist/{chunk-R7TZ2HCS.js → chunk-JG22O53B.js} +58 -3
  87. package/dist/chunk-JG22O53B.js.map +1 -0
  88. package/dist/{chunk-VPHSMJ2E.js → chunk-JPWSYHLB.js} +2 -2
  89. package/dist/{chunk-X7LEDA74.js → chunk-KFB4YNXM.js} +2 -2
  90. package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
  91. package/dist/chunk-LQWOFU6L.js +17 -0
  92. package/dist/chunk-LQWOFU6L.js.map +1 -0
  93. package/dist/{chunk-ZUPKNWRZ.js → chunk-MIUL6AUZ.js} +3 -3
  94. package/dist/{chunk-EJY3HJMT.js → chunk-MYI3RKI6.js} +2 -2
  95. package/dist/{chunk-NA56NSKL.js → chunk-OOJCXJBN.js} +2 -2
  96. package/dist/{chunk-7KQB4EBS.js → chunk-OPPBTPUU.js} +4 -4
  97. package/dist/{chunk-ERPBRXVZ.js → chunk-PZKWPXRF.js} +2 -2
  98. package/dist/{chunk-FHKXVTM5.js → chunk-Q5XKKDFQ.js} +16 -16
  99. package/dist/{chunk-QRWMS6SI.js → chunk-Q6OL23WM.js} +2 -2
  100. package/dist/{chunk-OW7NVMGQ.js → chunk-QIZOU3FN.js} +2 -2
  101. package/dist/{chunk-V3KIRICY.js → chunk-QQYHYW2N.js} +3 -3
  102. package/dist/{chunk-YFCEBB5P.js → chunk-QTXNS3HB.js} +2 -2
  103. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  104. package/dist/{chunk-E4KMY4KR.js → chunk-SNEUAZ6B.js} +2 -2
  105. package/dist/{chunk-TSTI6A3A.js → chunk-SYJW273H.js} +11 -11
  106. package/dist/{chunk-EFKZT6AY.js → chunk-T42L3S6A.js} +6 -11
  107. package/dist/chunk-T42L3S6A.js.map +1 -0
  108. package/dist/{chunk-YXMHUBN2.js → chunk-TOP3FSDW.js} +25 -102
  109. package/dist/chunk-TOP3FSDW.js.map +1 -0
  110. package/dist/{chunk-PBRCYYO7.js → chunk-ULZFZL7K.js} +2 -2
  111. package/dist/{chunk-UGHCZS6K.js → chunk-VFZQXYN5.js} +2 -2
  112. package/dist/{chunk-IVXVLDNT.js → chunk-WKJTMXME.js} +37 -54
  113. package/dist/chunk-WKJTMXME.js.map +1 -0
  114. package/dist/{chunk-NKB43PGI.js → chunk-WZDTSBGQ.js} +2 -2
  115. package/dist/{chunk-2UM6WHME.js → chunk-XE2QCCT2.js} +2 -2
  116. package/dist/{chunk-FPEZ74F7.js → chunk-XNJQZLAP.js} +3 -3
  117. package/dist/{chunk-SYVGVK4S.js → chunk-XTMYB2HY.js} +2 -2
  118. package/dist/{chunk-OETHJBAL.js → chunk-XV6PUXJ5.js} +71 -71
  119. package/dist/{chunk-UCLMKNHA.js → chunk-YIMV2KYM.js} +2 -2
  120. package/dist/{chunk-DYZR3Z6D.js → chunk-YMQKPSB2.js} +4 -4
  121. package/dist/{chunk-AYVCKGA6.js → chunk-YTXNDEUS.js} +2 -2
  122. package/dist/{chunk-L3CHRBHE.js → chunk-ZRUEP4IK.js} +17 -11
  123. package/dist/chunk-ZRUEP4IK.js.map +1 -0
  124. package/dist/{cli-BFBDYV0D.d.ts → cli-BTNj5DzK.d.ts} +1 -1
  125. package/dist/cli.d.ts +2 -2
  126. package/dist/cli.js +59 -57
  127. package/dist/compounding/engine.js +10 -10
  128. package/dist/config.js +5 -3
  129. package/dist/connectors/codex-materialize-runner.js +10 -10
  130. package/dist/connectors/index.js +10 -10
  131. package/dist/consolidation-provenance-check.js +2 -2
  132. package/dist/convergence-refresh.js +12 -12
  133. package/dist/conversation-index/backend.js +2 -2
  134. package/dist/corpus-watermark.js +12 -12
  135. package/dist/entity-retrieval.js +10 -10
  136. package/dist/external-wiki-access.d.ts +1 -1
  137. package/dist/external-wiki-access.js +28 -26
  138. package/dist/external-wiki-mcp-tools.d.ts +1 -1
  139. package/dist/extraction-judge.d.ts +2 -1
  140. package/dist/extraction-judge.js +6 -1
  141. package/dist/extraction-normalization.js +2 -2
  142. package/dist/extraction-prompt.d.ts +20 -1
  143. package/dist/extraction-prompt.js +11 -1
  144. package/dist/extraction.js +13 -13
  145. package/dist/{first-start-migration-4NETATFF.js → first-start-migration-Q2VB2E2D.js} +4 -4
  146. package/dist/harmonic-retrieval.js +1 -1
  147. package/dist/index.d.ts +680 -680
  148. package/dist/index.js +92 -90
  149. package/dist/maintenance/memory-governance.js +10 -10
  150. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  151. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  152. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  153. package/dist/namespaces/migrate.js +11 -11
  154. package/dist/namespaces/search.js +4 -4
  155. package/dist/namespaces/storage.js +10 -10
  156. package/dist/offline-sync.js +3 -2
  157. package/dist/operator-doctor-corpus.js +13 -13
  158. package/dist/operator-toolkit.js +24 -22
  159. package/dist/orchestration/maintenance.js +16 -16
  160. package/dist/orchestrator.js +85 -83
  161. package/dist/{public-http-8PcrDIYi.d.ts → public-http-BmPktTe3.d.ts} +1 -1
  162. package/dist/reconcile/cursor.js +7 -3
  163. package/dist/reconcile/cursor.js.map +1 -1
  164. package/dist/reconcile/manifest.js +3 -7
  165. package/dist/reconcile/plan.d.ts +15 -1
  166. package/dist/reconcile/plan.js +6 -10
  167. package/dist/resume-bundles.js +6 -4
  168. package/dist/schemas.d.ts +108 -108
  169. package/dist/search/factory.js +3 -3
  170. package/dist/search/index.js +3 -3
  171. package/dist/semantic-consolidation.js +16 -16
  172. package/dist/semantic-rule-promotion.js +10 -10
  173. package/dist/semantic-rule-verifier.js +10 -10
  174. package/dist/shared-context/manager.d.ts +8 -8
  175. package/dist/source-agent-qualifier.js +10 -10
  176. package/dist/storage.js +9 -9
  177. package/dist/support-passport/index.d.ts +4 -4
  178. package/dist/support-passport/index.js +30 -28
  179. package/dist/transfer/autodetect.js +1 -1
  180. package/dist/transfer/backup.js +1 -1
  181. package/dist/transfer/capsule-export.js +2 -2
  182. package/dist/transfer/types.d.ts +66 -66
  183. package/dist/verified-recall.js +10 -10
  184. package/package.json +2 -2
  185. package/src/calibration.ts +3 -1
  186. package/src/census-validation.ts +25 -0
  187. package/src/coding/architecture-surfaces.test.ts +9 -10
  188. package/src/coding/architecture-surfaces.ts +2 -35
  189. package/src/coding/codegraph-runtime-loader.test.ts +228 -0
  190. package/src/coding/codegraph-runtime.ts +30 -50
  191. package/src/coding/codegraph-surfaces.ts +2 -2
  192. package/src/coding/coding-knowledge-config.ts +35 -1
  193. package/src/coding/decision-surfaces.test.ts +9 -10
  194. package/src/coding/decision-surfaces.ts +2 -40
  195. package/src/coding/session-delta-surfaces.test.ts +9 -10
  196. package/src/coding/session-delta-surfaces.ts +2 -33
  197. package/src/extraction-judge.ts +3 -1
  198. package/src/extraction-output-language.test.ts +116 -0
  199. package/src/extraction-prompt.ts +70 -1
  200. package/src/extraction.ts +23 -95
  201. package/src/offline-sync.ts +4 -4
  202. package/src/reconcile/census-parity.test.ts +152 -0
  203. package/src/reconcile/cursor.ts +5 -5
  204. package/src/reconcile/manifest.ts +4 -11
  205. package/src/reconcile/plan.ts +18 -15
  206. package/dist/chunk-EFKZT6AY.js.map +0 -1
  207. package/dist/chunk-IVXVLDNT.js.map +0 -1
  208. package/dist/chunk-L3CHRBHE.js.map +0 -1
  209. package/dist/chunk-R7TZ2HCS.js.map +0 -1
  210. package/dist/chunk-RYST72N5.js.map +0 -1
  211. package/dist/chunk-TWTUGCTY.js.map +0 -1
  212. package/dist/chunk-YXMHUBN2.js.map +0 -1
  213. package/dist/chunk-Z4N7SVVF.js.map +0 -1
  214. package/dist/{access-service-cKQrte9Z.d.ts → access-service-CocW74uA.d.ts} +64 -64
  215. /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
  216. /package/dist/{chunk-3Z5TGRFA.js.map → chunk-3NZAVWU5.js.map} +0 -0
  217. /package/dist/{chunk-I5FLIB2J.js.map → chunk-3S3PNR5H.js.map} +0 -0
  218. /package/dist/{chunk-R6UMNP5C.js.map → chunk-3T6HIIKA.js.map} +0 -0
  219. /package/dist/{chunk-77QZW2D6.js.map → chunk-3TDAA7VA.js.map} +0 -0
  220. /package/dist/{chunk-DXDAQZ2K.js.map → chunk-42TIIO2M.js.map} +0 -0
  221. /package/dist/{chunk-Y7COCXY2.js.map → chunk-4F67JJPG.js.map} +0 -0
  222. /package/dist/{chunk-PB4WVYGC.js.map → chunk-5S5DUJFJ.js.map} +0 -0
  223. /package/dist/{chunk-KYTRF64R.js.map → chunk-5SCFDQEJ.js.map} +0 -0
  224. /package/dist/{chunk-GG4DN63W.js.map → chunk-62Y5AGCM.js.map} +0 -0
  225. /package/dist/{chunk-KHCUVYCY.js.map → chunk-6UKSOXAO.js.map} +0 -0
  226. /package/dist/{chunk-F6JYBVL6.js.map → chunk-7ZAC2C2U.js.map} +0 -0
  227. /package/dist/{chunk-GF7RQWXC.js.map → chunk-AJRJDXYR.js.map} +0 -0
  228. /package/dist/{chunk-YEQCMA5L.js.map → chunk-BOSIHHOH.js.map} +0 -0
  229. /package/dist/{chunk-7LF2OHIJ.js.map → chunk-BRHKW7GY.js.map} +0 -0
  230. /package/dist/{chunk-LX4TNSAE.js.map → chunk-CBY54QYW.js.map} +0 -0
  231. /package/dist/{chunk-6YMEU6OQ.js.map → chunk-G72IPMR7.js.map} +0 -0
  232. /package/dist/{chunk-7AF243WZ.js.map → chunk-GBVRVYF4.js.map} +0 -0
  233. /package/dist/{chunk-GTD4Q62X.js.map → chunk-H4GQ2Y24.js.map} +0 -0
  234. /package/dist/{chunk-QI5RDDEL.js.map → chunk-HXZNTZ3Y.js.map} +0 -0
  235. /package/dist/{chunk-CIAI24TQ.js.map → chunk-IBZQQOM5.js.map} +0 -0
  236. /package/dist/{chunk-VPHSMJ2E.js.map → chunk-JPWSYHLB.js.map} +0 -0
  237. /package/dist/{chunk-X7LEDA74.js.map → chunk-KFB4YNXM.js.map} +0 -0
  238. /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
  239. /package/dist/{chunk-ZUPKNWRZ.js.map → chunk-MIUL6AUZ.js.map} +0 -0
  240. /package/dist/{chunk-EJY3HJMT.js.map → chunk-MYI3RKI6.js.map} +0 -0
  241. /package/dist/{chunk-NA56NSKL.js.map → chunk-OOJCXJBN.js.map} +0 -0
  242. /package/dist/{chunk-7KQB4EBS.js.map → chunk-OPPBTPUU.js.map} +0 -0
  243. /package/dist/{chunk-ERPBRXVZ.js.map → chunk-PZKWPXRF.js.map} +0 -0
  244. /package/dist/{chunk-FHKXVTM5.js.map → chunk-Q5XKKDFQ.js.map} +0 -0
  245. /package/dist/{chunk-QRWMS6SI.js.map → chunk-Q6OL23WM.js.map} +0 -0
  246. /package/dist/{chunk-OW7NVMGQ.js.map → chunk-QIZOU3FN.js.map} +0 -0
  247. /package/dist/{chunk-V3KIRICY.js.map → chunk-QQYHYW2N.js.map} +0 -0
  248. /package/dist/{chunk-YFCEBB5P.js.map → chunk-QTXNS3HB.js.map} +0 -0
  249. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  250. /package/dist/{chunk-E4KMY4KR.js.map → chunk-SNEUAZ6B.js.map} +0 -0
  251. /package/dist/{chunk-TSTI6A3A.js.map → chunk-SYJW273H.js.map} +0 -0
  252. /package/dist/{chunk-PBRCYYO7.js.map → chunk-ULZFZL7K.js.map} +0 -0
  253. /package/dist/{chunk-UGHCZS6K.js.map → chunk-VFZQXYN5.js.map} +0 -0
  254. /package/dist/{chunk-NKB43PGI.js.map → chunk-WZDTSBGQ.js.map} +0 -0
  255. /package/dist/{chunk-2UM6WHME.js.map → chunk-XE2QCCT2.js.map} +0 -0
  256. /package/dist/{chunk-FPEZ74F7.js.map → chunk-XNJQZLAP.js.map} +0 -0
  257. /package/dist/{chunk-SYVGVK4S.js.map → chunk-XTMYB2HY.js.map} +0 -0
  258. /package/dist/{chunk-OETHJBAL.js.map → chunk-XV6PUXJ5.js.map} +0 -0
  259. /package/dist/{chunk-UCLMKNHA.js.map → chunk-YIMV2KYM.js.map} +0 -0
  260. /package/dist/{chunk-DYZR3Z6D.js.map → chunk-YMQKPSB2.js.map} +0 -0
  261. /package/dist/{chunk-AYVCKGA6.js.map → chunk-YTXNDEUS.js.map} +0 -0
  262. /package/dist/{first-start-migration-4NETATFF.js.map → first-start-migration-Q2VB2E2D.js.map} +0 -0
@@ -0,0 +1,228 @@
1
+ // Regression tests for issue #2478: the runtime bridge must resolve
2
+ // @remnic/coding-graph through the shared loader
3
+ // (optional-coding-graph.ts) -- ONE import, ONE cache, ONE contract
4
+ // check -- and the miss path must surface the canonical install hint,
5
+ // never a raw ERR_MODULE_NOT_FOUND.
6
+ //
7
+ // Both tests drive the REAL dynamic-import machinery in a child process
8
+ // with a registered resolve hook (the established technique from
9
+ // packages/remnic-cli/src/openclaw-managed-upgrade.test.ts):
10
+ //
11
+ // 1. pass-through hook that COUNTS resolutions of the specifier --
12
+ // the shared loader caches success, so a probe + a store open must
13
+ // resolve "@remnic/coding-graph" exactly once. A parallel loader
14
+ // in codegraph-runtime.ts resolves it a second time and fails.
15
+ // 2. throwing hook that simulates the absent package -- the store
16
+ // open must reject with the tagged CodegraphRuntimeError whose
17
+ // message is the canonical install hint (buildCodingGraphInstallHint).
18
+
19
+ import assert from "node:assert/strict";
20
+ import { spawnSync } from "node:child_process";
21
+ import fs from "node:fs";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import test from "node:test";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
+
27
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
28
+ const tsxCli = path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
29
+ const optionalLoaderSource = path.join(
30
+ repoRoot,
31
+ "packages",
32
+ "remnic-core",
33
+ "src",
34
+ "coding",
35
+ "optional-coding-graph.ts",
36
+ );
37
+ const runtimeSource = path.join(
38
+ repoRoot,
39
+ "packages",
40
+ "remnic-core",
41
+ "src",
42
+ "coding",
43
+ "codegraph-runtime.ts",
44
+ );
45
+
46
+ interface DriverReport {
47
+ opened?: boolean;
48
+ threw?: boolean;
49
+ name?: string;
50
+ code?: unknown;
51
+ message?: string;
52
+ }
53
+
54
+ function makeScratch(prefix: string): string {
55
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
56
+ }
57
+
58
+ function writeCountingResolveHook(root: string): { hookPath: string; logPath: string } {
59
+ const logPath = path.join(root, "resolves.log");
60
+ const hookPath = path.join(root, "count-resolves.mjs");
61
+ fs.writeFileSync(
62
+ hookPath,
63
+ `import { appendFileSync } from "node:fs";
64
+ import { pathToFileURL } from "node:url";
65
+ export async function resolve(specifier, context, nextResolve) {
66
+ if (specifier === "@remnic/coding-graph") {
67
+ if (process.env.CODEGRAPH_RESOLVE_LOG) {
68
+ appendFileSync(process.env.CODEGRAPH_RESOLVE_LOG, specifier + "\\n");
69
+ }
70
+ try {
71
+ return await nextResolve(specifier, context);
72
+ } catch (err) {
73
+ if (err && err.code === "ERR_MODULE_NOT_FOUND" && process.env.CODEGRAPH_SOURCE_FALLBACK) {
74
+ // Worktree/base install where the optional peer is not linked:
75
+ // map the specifier to the workspace source so the import
76
+ // SUCCEEDS and the shared loader's success cache is observable.
77
+ return { url: pathToFileURL(process.env.CODEGRAPH_SOURCE_FALLBACK).href, shortCircuit: true };
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+ return nextResolve(specifier, context);
83
+ }
84
+ `,
85
+ "utf8",
86
+ );
87
+ return { hookPath, logPath };
88
+ }
89
+
90
+ function writeMissingPackageResolveHook(root: string): string {
91
+ const hookPath = path.join(root, "missing-coding-graph.mjs");
92
+ fs.writeFileSync(
93
+ hookPath,
94
+ `export async function resolve(specifier, context, nextResolve) {
95
+ if (specifier === "@remnic/coding-graph") {
96
+ const error = new Error("Cannot find package '@remnic/coding-graph' imported from test");
97
+ error.code = "ERR_MODULE_NOT_FOUND";
98
+ throw error;
99
+ }
100
+ return nextResolve(specifier, context);
101
+ }
102
+ `,
103
+ "utf8",
104
+ );
105
+ return hookPath;
106
+ }
107
+
108
+ function writeStoreOpenDriver(root: string): string {
109
+ const driverPath = path.join(root, "store-open.mts");
110
+ fs.writeFileSync(
111
+ driverPath,
112
+ `import { mkdirSync } from "node:fs";
113
+ import path from "node:path";
114
+ import { pathToFileURL } from "node:url";
115
+
116
+ const [, , runtimePath, optionalLoaderPath, scratch] = process.argv;
117
+ // Prime the shared loader exactly like the real surfaces do (the
118
+ // runtime-availability probe runs before any store open).
119
+ const optional = await import(pathToFileURL(optionalLoaderPath).href);
120
+ await optional.isCodingGraphInstalled();
121
+ const runtime = await import(pathToFileURL(runtimePath).href);
122
+ const memoryDir = path.join(scratch, "memory");
123
+ mkdirSync(path.join(memoryDir, "codegraph", "loader-principal"), { recursive: true });
124
+ const config = {
125
+ codingKnowledge: { enabled: true, codegraphTools: true, codegraphDbDir: "" },
126
+ };
127
+ const request = {
128
+ config,
129
+ memoryDir,
130
+ principal: "loader-principal",
131
+ projectId: "loader-test",
132
+ };
133
+ try {
134
+ const store = await runtime.getCodegraphStore(request);
135
+ const schemaVersion = store.schemaVersion();
136
+ await store.close();
137
+ console.log(JSON.stringify({ opened: true, schemaVersion }));
138
+ } catch (err) {
139
+ const e = err instanceof Error ? err : new Error(String(err));
140
+ console.log(
141
+ JSON.stringify({ threw: true, name: e.name, code: e.code, message: e.message }),
142
+ );
143
+ }
144
+ `,
145
+ "utf8",
146
+ );
147
+ return driverPath;
148
+ }
149
+
150
+
151
+ function runChild(
152
+ scriptPath: string,
153
+ args: string[],
154
+ env: NodeJS.ProcessEnv,
155
+ ): { status: number | null; stdout: string; stderr: string } {
156
+ const result = spawnSync(process.execPath, [tsxCli, scriptPath, ...args], {
157
+ encoding: "utf8",
158
+ cwd: repoRoot,
159
+ env: { ...process.env, ...env },
160
+ timeout: 60_000,
161
+ });
162
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
163
+ }
164
+
165
+ function lastJsonLine(stdout: string): DriverReport {
166
+ const lines = stdout.trim().split(/\n/).filter(Boolean);
167
+ let line: string | undefined;
168
+ for (let i = lines.length - 1; i >= 0; i--) {
169
+ if (lines[i].startsWith("{")) {
170
+ line = lines[i];
171
+ break;
172
+ }
173
+ }
174
+ assert.ok(line, `expected a JSON line in child stdout, got: ${stdout}`);
175
+ return JSON.parse(line) as DriverReport;
176
+ }
177
+
178
+ test("codegraph-runtime resolves @remnic/coding-graph exactly once (shared loader, no parallel import)", async () => {
179
+ // The counting hook maps the specifier to the workspace source when the
180
+ // optional peer is not linked, so the import SUCCEEDS in every install
181
+ // state. The shared loader then caches the success: a probe + a store
182
+ // open must resolve the specifier exactly once. A parallel loader in
183
+ // codegraph-runtime.ts imports the package again and fails this count.
184
+ const root = makeScratch("remnic-2478-single-load-");
185
+ try {
186
+ const { hookPath, logPath } = writeCountingResolveHook(root);
187
+ const driver = writeStoreOpenDriver(root);
188
+ const result = runChild(driver, [runtimeSource, optionalLoaderSource, root], {
189
+ CODEGRAPH_RESOLVE_LOG: logPath,
190
+ CODEGRAPH_SOURCE_FALLBACK: path.join(repoRoot, "packages", "coding-graph", "src", "index.ts"),
191
+ NODE_OPTIONS: `--experimental-loader=${pathToFileURL(hookPath).href} --conditions=remnic-source`,
192
+ });
193
+ assert.equal(result.status, 0, result.stderr);
194
+ const report = lastJsonLine(result.stdout);
195
+ assert.equal(report.opened, true, `store must open against the loaded module: ${JSON.stringify(report)}`);
196
+ const resolves = fs.existsSync(logPath)
197
+ ? fs.readFileSync(logPath, "utf8").trim().split(/\n/).filter(Boolean)
198
+ : [];
199
+ assert.equal(
200
+ resolves.length,
201
+ 1,
202
+ `the shared loader must resolve "@remnic/coding-graph" exactly once; saw ${resolves.length} resolutions -- a second resolve means codegraph-runtime still imports the package in parallel`,
203
+ );
204
+ } finally {
205
+ fs.rmSync(root, { recursive: true, force: true });
206
+ }
207
+ });
208
+
209
+ test("codegraph-runtime miss path throws the canonical install hint, not raw MODULE_NOT_FOUND", async () => {
210
+ // The throwing resolve hook simulates the absent package even on a dev
211
+ // workspace where it is installed, so this runs everywhere.
212
+ const root = makeScratch("remnic-2478-miss-");
213
+ try {
214
+ const hookPath = writeMissingPackageResolveHook(root);
215
+ const driver = writeStoreOpenDriver(root);
216
+ const result = runChild(driver, [runtimeSource, optionalLoaderSource, root], {
217
+ NODE_OPTIONS: `--experimental-loader=${pathToFileURL(hookPath).href} --conditions=remnic-source`,
218
+ });
219
+ assert.equal(result.status, 0, result.stderr);
220
+ const report = lastJsonLine(result.stdout);
221
+ assert.equal(report.threw, true, `store open must fail when the package is absent: ${JSON.stringify(report)}`);
222
+ assert.equal(report.code, "package_missing");
223
+ assert.match(report.message ?? "", /npm install @remnic\/coding-graph/);
224
+ assert.doesNotMatch(report.message ?? "", /ERR_MODULE_NOT_FOUND|Cannot find module/);
225
+ } finally {
226
+ fs.rmSync(root, { recursive: true, force: true });
227
+ }
228
+ });
@@ -9,9 +9,11 @@
9
9
  * dependency that hosts opt into. Every code path that needs a GraphStore
10
10
  * calls {@link getCodegraphStore} here; this module owns:
11
11
  *
12
- * - the loader boundary (one place that dynamically imports the optional
13
- * package; CLAUDE.md rule 57 / AGENTS.md rule 44 documented exception --
14
- * the optional peer genuinely does not exist on every install);
12
+ * - the store-module boundary @remnic/coding-graph is imported by the
13
+ * ONE shared loader (optional-coding-graph.ts; single cache and
14
+ * contract check per process). This module narrows that shared module
15
+ * to the runtime's GraphStore view; it never imports the package
16
+ * itself;
15
17
  * - per-project GraphStore caching (one open SQLite handle per
16
18
  * `(principal, projectId)` pair, lifecycle-tracked so a host process can
17
19
  * close them on shutdown);
@@ -30,20 +32,14 @@ import { createHash } from "node:crypto";
30
32
  import { expandTildePath } from "../utils/path.js";
31
33
 
32
34
  import type { PluginConfig, CodingKnowledgeConfig, CodingContext } from "../types.js";
33
- import { isCodingGraphInstalled } from "./optional-coding-graph.js";
35
+ import { buildCodingGraphInstallHint, isCodingGraphInstalled, tryLoadCodingGraphModule } from "./optional-coding-graph.js";
34
36
  import { displayErrorDetail } from "../runtime/better-sqlite.js";
35
37
  // Type-only reference to the surface context shape (ts-import-type rule).
36
38
  // Erased at compile time, so the runtime → surfaces → runtime import cycle is
37
39
  // type-only and has no runtime effect.
38
40
  import type { CodegraphSurfaceContext } from "./codegraph-surfaces.js";
39
41
 
40
- // Lazy-loaded optional package -- see file header. The dynamic import below
41
- // is the documented exception to the static-import rule: @remnic/coding-graph
42
- // is an optional peer dependency that is genuinely absent on base installs,
43
- // so a static import would either fail the base install or be bundled into
44
- // @remnic/core's dist. The specifier is composed from string literals to
45
- // keep this a runtime-only reference.
46
- const CODEGRAPH_SPECIFIER = "@remnic/" + "coding-graph";
42
+
47
43
 
48
44
  /**
49
45
  * Minimal structural shape of the @remnic/coding-graph public surface this
@@ -270,8 +266,6 @@ interface CachedStore {
270
266
  }
271
267
 
272
268
  const storeCache = new Map<string, CachedStore>();
273
- let cachedModule: CodegraphModule | null | undefined;
274
- let moduleLoadPromise: Promise<CodegraphModule | null> | null = null;
275
269
 
276
270
  function storeCacheKey(principalSafe: string, projectSafe: string, dbPath: string): string {
277
271
  // Thread 8 (issue #1554): include the resolved DB path so two configs
@@ -282,29 +276,24 @@ function storeCacheKey(principalSafe: string, projectSafe: string, dbPath: strin
282
276
  }
283
277
 
284
278
  /**
285
- * Dynamically import the optional package ONCE per process. Subsequent
286
- * callers await the same promise. Missing/incompatible installs collapse
287
- * to `null` (graceful degradation -- surfaces translate to a clean hint).
279
+ * Narrow the module served by the shared loader (optional-coding-graph.ts)
280
+ * to this runtime's structural view. The shared loader owns the single
281
+ * dynamic import, cache, and miss/incompatible diagnostics; this predicate
282
+ * only checks the GraphStore capability the runtime needs. A missing,
283
+ * broken, or engine-incompatible install collapses to `null` (graceful
284
+ * degradation -- surfaces translate to the install hint).
288
285
  */
289
- async function loadCodegraphModule(): Promise<CodegraphModule | null> {
290
- if (cachedModule !== undefined) return cachedModule;
291
- if (moduleLoadPromise === null) {
292
- moduleLoadPromise = (async () => {
293
- try {
294
- const mod = (await import(CODEGRAPH_SPECIFIER)) as Partial<CodegraphModule>;
295
- if (mod && typeof mod === "object" && mod.GraphStore && typeof mod.GraphStore.open === "function") {
296
- cachedModule = mod as CodegraphModule;
297
- return cachedModule;
298
- }
299
- cachedModule = null;
300
- return null;
301
- } catch {
302
- cachedModule = null;
303
- return null;
304
- }
305
- })();
306
- }
307
- return moduleLoadPromise;
286
+ function isCodegraphModule(value: object): value is CodegraphModule {
287
+ if (!value || typeof value !== "object") return false;
288
+ const candidate = value as Record<string, unknown>;
289
+ const graphStore = candidate.GraphStore as { open?: unknown } | undefined;
290
+ return graphStore !== undefined && typeof graphStore.open === "function";
291
+ }
292
+
293
+ async function resolveCodegraphModule(): Promise<CodegraphModule | null> {
294
+ const mod = await tryLoadCodingGraphModule();
295
+ if (mod === null || !isCodegraphModule(mod)) return null;
296
+ return mod;
308
297
  }
309
298
 
310
299
  /**
@@ -332,12 +321,9 @@ export async function getCodegraphStore(params: {
332
321
  "codegraph tools are disabled (codingKnowledge.enabled or codingKnowledge.codegraphTools is false)",
333
322
  );
334
323
  }
335
- const mod = await loadCodegraphModule();
324
+ const mod = await resolveCodegraphModule();
336
325
  if (mod === null) {
337
- throw new CodegraphRuntimeError(
338
- "package_missing",
339
- "The @remnic/coding-graph package is not installed; install it to use codegraph tools.",
340
- );
326
+ throw new CodegraphRuntimeError("package_missing", buildCodingGraphInstallHint());
341
327
  }
342
328
  // Key the cache on SANITIZED values AND the resolved DB path (thread 8):
343
329
  // two different raw identifiers that sanitize to the same path segment
@@ -533,7 +519,7 @@ export async function runCodegraphReindex(params: {
533
519
  readonly repoRoot: string;
534
520
  readonly mode: string;
535
521
  }): Promise<CodegraphDelegateOutcome<{ mode: string; filesIngested: number; head: string | null }>> {
536
- const mod = await loadCodegraphModule();
522
+ const mod = await resolveCodegraphModule();
537
523
  if (mod === null) {
538
524
  return { ok: false, code: "package_missing", message: "The @remnic/coding-graph package is not installed." };
539
525
  }
@@ -676,7 +662,7 @@ export async function runCodegraphLspResolution(params: {
676
662
  }
677
663
  | { ok: false; code: string; message: string }
678
664
  > {
679
- const mod = await loadCodegraphModule();
665
+ const mod = await resolveCodegraphModule();
680
666
  if (mod === null) {
681
667
  return { ok: false, code: "package_missing", message: "@remnic/coding-graph is not installed." };
682
668
  }
@@ -1112,7 +1098,7 @@ export async function reportCodegraphIndexStatus(params: {
1112
1098
  readonly store: CodegraphStore;
1113
1099
  readonly repoRoot: string;
1114
1100
  }): Promise<CodegraphDelegateOutcome<{ status: Record<string, unknown> }>> {
1115
- const mod = await loadCodegraphModule();
1101
+ const mod = await resolveCodegraphModule();
1116
1102
  if (mod === null) {
1117
1103
  return { ok: false, code: "package_missing", message: "The @remnic/coding-graph package is not installed." };
1118
1104
  }
@@ -1140,7 +1126,7 @@ export async function detectCodegraphChanges(params: {
1140
1126
  readonly repoRoot: string;
1141
1127
  readonly head: string;
1142
1128
  }): Promise<CodegraphDelegateOutcome<{ affected: readonly unknown[] }>> {
1143
- const mod = await loadCodegraphModule();
1129
+ const mod = await resolveCodegraphModule();
1144
1130
  if (mod === null) {
1145
1131
  return { ok: false, code: "package_missing", message: "The @remnic/coding-graph package is not installed." };
1146
1132
  }
@@ -1229,12 +1215,6 @@ export async function ingestCodegraphTraces(params: {
1229
1215
  }
1230
1216
  }
1231
1217
 
1232
- /** Test seam: reset the module cache so a fresh import can be observed. */
1233
- export function __resetCodegraphRuntimeForTest(): void {
1234
- cachedModule = undefined;
1235
- moduleLoadPromise = null;
1236
- storeCache.clear();
1237
- }
1238
1218
 
1239
1219
  /**
1240
1220
  * Derive a stable per-repo project id from an absolute repoRoot (issue #1554
@@ -32,11 +32,11 @@ import {
32
32
  import {
33
33
  DECISION_SUBCOMMANDS,
34
34
  handleCodingDecision,
35
- isDecisionRecordSurfaceEnabled,
36
35
  type DecisionSurfaceContext,
37
36
  type DecisionSurfaceRequest,
38
37
  type DecisionSurfaceResponse,
39
38
  } from "./decision-surfaces.js";
39
+ import { isCodingKnowledgeFeatureEnabled } from "./coding-knowledge-config.js";
40
40
 
41
41
  // ──────────────────────────────────────────────────────────────────────────
42
42
  // Canonical tool names — mirror the external codebase-memory-mcp surface
@@ -674,7 +674,7 @@ async function handleManageAdr(
674
674
  // done-when) asserts that `manage_adr record` and `coding_decision record`
675
675
  // write byte-identical records -- both routes call handleCodingDecision.
676
676
  const codingContext = resolveCodingContext(request, ctx);
677
- if (!isDecisionRecordSurfaceEnabled(ctx.config.codingKnowledge, codingContext)) {
677
+ if (!isCodingKnowledgeFeatureEnabled(ctx.config.codingKnowledge, "decisionRecords", codingContext)) {
678
678
  return disabledResponse(
679
679
  request.tool,
680
680
  "manage_adr requires codingKnowledge.enabled + decisionRecords + an attached coding context",
@@ -21,7 +21,7 @@
21
21
  * the existing `ENGRAM_*` env vars from earlier releases do not pair them up.
22
22
  */
23
23
 
24
- import type { CodingKnowledgeConfig, CodingGraphLspConfig } from "../types.js";
24
+ import type { CodingContext, CodingGraphLspConfig, CodingKnowledgeConfig } from "../types.js";
25
25
  import { TIER_1_LANGUAGES } from "./coding-graph-types.js";
26
26
  import { coerceBool } from "../connectors/coerce.js";
27
27
 
@@ -81,6 +81,40 @@ export function parseCodingKnowledgeConfig(raw: unknown): CodingKnowledgeConfig
81
81
  };
82
82
  }
83
83
 
84
+ // ──────────────────────────────────────────────────────────────────────────
85
+ // Shared surface gates (rule 39: ONE predicate pair, identical on every
86
+ // surface — issue #2478 dedupe). Each Track A surface used to carry its own
87
+ // copy of this logic; they all resolve here now. The booleans are already
88
+ // strict-coerced by parseCodingKnowledgeConfig, so `=== true` is safe here.
89
+ // ──────────────────────────────────────────────────────────────────────────
90
+
91
+ /** Boolean feature switches governed by the master `codingKnowledge.enabled` gate. */
92
+ export type CodingKnowledgeFeatureFlag = "decisionRecords" | "architectureCard" | "sessionDelta";
93
+
94
+ /**
95
+ * Config-only visibility gate — used by the MCP constructor to decide
96
+ * whether a coding tool is advertised in `tools/list`. True only when the
97
+ * master gate AND the feature switch are both on.
98
+ */
99
+ export function isCodingKnowledgeFeatureVisible(
100
+ config: CodingKnowledgeConfig,
101
+ feature: CodingKnowledgeFeatureFlag,
102
+ ): boolean {
103
+ return config.enabled === true && config[feature] === true;
104
+ }
105
+
106
+ /**
107
+ * Full call-time gate: visibility PLUS an attached coding context
108
+ * (project/branch-scoped session). Handlers check this before dispatch.
109
+ */
110
+ export function isCodingKnowledgeFeatureEnabled(
111
+ config: CodingKnowledgeConfig,
112
+ feature: CodingKnowledgeFeatureFlag,
113
+ codingContext: CodingContext | null | undefined,
114
+ ): boolean {
115
+ return isCodingKnowledgeFeatureVisible(config, feature) && codingContext != null;
116
+ }
117
+
84
118
  /**
85
119
  * Read the optional `codingKnowledge.lsp` sub-object. Absent → empty
86
120
  * spread (no own key). Present-but-malformed (string/number/array) is
@@ -21,12 +21,11 @@ import { EngramMcpServer } from "../access-mcp.js";
21
21
  import {
22
22
  DECISION_SUBCOMMANDS,
23
23
  formatDecisionSubcommands,
24
- isDecisionRecordSurfaceEnabled,
25
- isDecisionRecordSurfaceVisible,
26
24
  isDecisionSubcommand,
27
25
  type DecisionSurfaceRequest,
28
26
  type DecisionSurfaceResponse,
29
27
  } from "./decision-surfaces.js";
28
+ import { isCodingKnowledgeFeatureEnabled, isCodingKnowledgeFeatureVisible } from "./coding-knowledge-config.js";
30
29
  import type { CodingContext, CodingKnowledgeConfig, PluginConfig } from "../types.js";
31
30
 
32
31
  // ──────────────────────────────────────────────────────────────────────────
@@ -104,28 +103,28 @@ function makeMockServiceWithConfig(
104
103
  // ──────────────────────────────────────────────────────────────────────────
105
104
 
106
105
  test("gate: disabled when config.enabled is false", () => {
107
- assert.equal(isDecisionRecordSurfaceEnabled(GATE_OFF_CONFIG, CODING_CONTEXT), false);
106
+ assert.equal(isCodingKnowledgeFeatureEnabled(GATE_OFF_CONFIG, "decisionRecords", CODING_CONTEXT), false);
108
107
  });
109
108
 
110
109
  test("gate: disabled when decisionRecords is false", () => {
111
110
  const cfg: CodingKnowledgeConfig = { ...GATE_ON_CONFIG, decisionRecords: false };
112
- assert.equal(isDecisionRecordSurfaceEnabled(cfg, CODING_CONTEXT), false);
111
+ assert.equal(isCodingKnowledgeFeatureEnabled(cfg, "decisionRecords", CODING_CONTEXT), false);
113
112
  });
114
113
 
115
114
  test("gate: disabled when no coding context attached", () => {
116
- assert.equal(isDecisionRecordSurfaceEnabled(GATE_ON_CONFIG, null), false);
117
- assert.equal(isDecisionRecordSurfaceEnabled(GATE_ON_CONFIG, undefined), false);
115
+ assert.equal(isCodingKnowledgeFeatureEnabled(GATE_ON_CONFIG, "decisionRecords", null), false);
116
+ assert.equal(isCodingKnowledgeFeatureEnabled(GATE_ON_CONFIG, "decisionRecords", undefined), false);
118
117
  });
119
118
 
120
119
  test("gate: enabled only when all three conditions hold", () => {
121
- assert.equal(isDecisionRecordSurfaceEnabled(GATE_ON_CONFIG, CODING_CONTEXT), true);
120
+ assert.equal(isCodingKnowledgeFeatureEnabled(GATE_ON_CONFIG, "decisionRecords", CODING_CONTEXT), true);
122
121
  });
123
122
 
124
123
  test("visibility gate: config-only check for tools/list construction", () => {
125
- assert.equal(isDecisionRecordSurfaceVisible(GATE_OFF_CONFIG), false);
126
- assert.equal(isDecisionRecordSurfaceVisible(GATE_ON_CONFIG), true);
124
+ assert.equal(isCodingKnowledgeFeatureVisible(GATE_OFF_CONFIG, "decisionRecords"), false);
125
+ assert.equal(isCodingKnowledgeFeatureVisible(GATE_ON_CONFIG, "decisionRecords"), true);
127
126
  assert.equal(
128
- isDecisionRecordSurfaceVisible({ ...GATE_ON_CONFIG, decisionRecords: false }),
127
+ isCodingKnowledgeFeatureVisible({ ...GATE_ON_CONFIG, decisionRecords: false }, "decisionRecords"),
129
128
  false,
130
129
  );
131
130
  });
@@ -24,6 +24,7 @@ import {
24
24
  import { log } from "../logger.js";
25
25
  import { composeMemoryEnvelope, type SealedMemoryEnvelope } from "../write-envelope.js";
26
26
  import type { MemoryWriteResult } from "../storage.js";
27
+ import { isCodingKnowledgeFeatureEnabled } from "./coding-knowledge-config.js";
27
28
 
28
29
  // ──────────────────────────────────────────────────────────────────────────
29
30
  // Subcommands
@@ -56,45 +57,6 @@ export function formatDecisionSubcommands(): string {
56
57
  return DECISION_SUBCOMMANDS.join(", ");
57
58
  }
58
59
 
59
- // ──────────────────────────────────────────────────────────────────────────
60
- // Gate predicate — rule 39: one predicate, identical on every surface
61
- // ──────────────────────────────────────────────────────────────────────────
62
-
63
- /**
64
- * The single decision-record surface gate. Returns `true` only when:
65
- * 1. `codingKnowledge.enabled` is on (the master Track A gate),
66
- * 2. `codingKnowledge.decisionRecords` is on (the feature switch), AND
67
- * 3. A coding context is attached (the session is project/branch scoped —
68
- * decision records live *in* the coding namespace, rule 42).
69
- *
70
- * Every surface — MCP `engram.coding_decision`, HTTP
71
- * `POST /engram/v1/coding/decisions`, CLI `engram-access decision` — MUST call
72
- * this predicate (or the handler that embeds it) before dispatching. The
73
- * tool-visibility gate in the MCP constructor checks conditions 1–2 only
74
- * (coding context is per-session and cannot be evaluated at construction
75
- * time); the call-time gate checks all three.
76
- */
77
- export function isDecisionRecordSurfaceEnabled(
78
- config: CodingKnowledgeConfig,
79
- codingContext: CodingContext | null | undefined,
80
- ): boolean {
81
- return (
82
- config.enabled === true &&
83
- config.decisionRecords === true &&
84
- codingContext != null
85
- );
86
- }
87
-
88
- /**
89
- * Config-only visibility gate — used by the MCP constructor to decide whether
90
- * to advertise `engram.coding_decision` in `tools/list`. When this returns
91
- * `false` the tools array is byte-identical to pre-feature (rule 39).
92
- */
93
- export function isDecisionRecordSurfaceVisible(
94
- config: CodingKnowledgeConfig,
95
- ): boolean {
96
- return config.enabled === true && config.decisionRecords === true;
97
- }
98
60
 
99
61
  // ──────────────────────────────────────────────────────────────────────────
100
62
  // Surface request / response shapes
@@ -228,7 +190,7 @@ export async function handleCodingDecision(
228
190
  const codingContext = request.sessionKey
229
191
  ? ctx.getCodingContext(request.sessionKey)
230
192
  : null;
231
- if (!isDecisionRecordSurfaceEnabled(ctx.codingKnowledge, codingContext)) {
193
+ if (!isCodingKnowledgeFeatureEnabled(ctx.codingKnowledge, "decisionRecords", codingContext)) {
232
194
  ctx.throwInputError(
233
195
  "coding_decision requires codingKnowledge.enabled, codingKnowledge.decisionRecords, and an attached coding context",
234
196
  );
@@ -18,11 +18,10 @@ import {
18
18
  formatDeltaSubcommands,
19
19
  handleCodingDelta,
20
20
  isDeltaSubcommand,
21
- isSessionDeltaSurfaceEnabled,
22
- isSessionDeltaSurfaceVisible,
23
21
  type DeltaSurfaceContext,
24
22
  type DeltaSurfaceStorage,
25
23
  } from "./session-delta-surfaces.js";
24
+ import { isCodingKnowledgeFeatureEnabled, isCodingKnowledgeFeatureVisible } from "./coding-knowledge-config.js";
26
25
  import type {
27
26
  CodingKnowledgeConfig,
28
27
  CodingContext,
@@ -111,28 +110,28 @@ test("isDeltaSubcommand: narrows valid + rejects unknown", () => {
111
110
  });
112
111
 
113
112
  test("gate: surface enabled when config + sessionDelta + coding context all true", () => {
114
- assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, CODING_CONTEXT), true);
113
+ assert.equal(isCodingKnowledgeFeatureEnabled(DEFAULT_CONFIG, "sessionDelta", CODING_CONTEXT), true);
115
114
  });
116
115
 
117
116
  test("gate: surface disabled when master gate off", () => {
118
117
  const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, enabled: false };
119
- assert.equal(isSessionDeltaSurfaceEnabled(config, CODING_CONTEXT), false);
118
+ assert.equal(isCodingKnowledgeFeatureEnabled(config, "sessionDelta", CODING_CONTEXT), false);
120
119
  });
121
120
 
122
121
  test("gate: surface disabled when sessionDelta off", () => {
123
122
  const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, sessionDelta: false };
124
- assert.equal(isSessionDeltaSurfaceEnabled(config, CODING_CONTEXT), false);
123
+ assert.equal(isCodingKnowledgeFeatureEnabled(config, "sessionDelta", CODING_CONTEXT), false);
125
124
  });
126
125
 
127
126
  test("gate: surface disabled when no coding context", () => {
128
- assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, null), false);
129
- assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, undefined), false);
127
+ assert.equal(isCodingKnowledgeFeatureEnabled(DEFAULT_CONFIG, "sessionDelta", null), false);
128
+ assert.equal(isCodingKnowledgeFeatureEnabled(DEFAULT_CONFIG, "sessionDelta", undefined), false);
130
129
  });
131
130
 
132
131
  test("visibility gate: config-only check mirrors full gate minus context", () => {
133
- assert.equal(isSessionDeltaSurfaceVisible(DEFAULT_CONFIG), true);
134
- assert.equal(isSessionDeltaSurfaceVisible({ ...DEFAULT_CONFIG, enabled: false }), false);
135
- assert.equal(isSessionDeltaSurfaceVisible({ ...DEFAULT_CONFIG, sessionDelta: false }), false);
132
+ assert.equal(isCodingKnowledgeFeatureVisible(DEFAULT_CONFIG, "sessionDelta"), true);
133
+ assert.equal(isCodingKnowledgeFeatureVisible({ ...DEFAULT_CONFIG, enabled: false }, "sessionDelta"), false);
134
+ assert.equal(isCodingKnowledgeFeatureVisible({ ...DEFAULT_CONFIG, sessionDelta: false }, "sessionDelta"), false);
136
135
  });
137
136
 
138
137
  // ──────────────────────────────────────────────────────────────────────────