@aiwg/cli 2026.10.1 → 2026.10.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 (361) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/capability-matrix.yaml +270 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +3183 -24
  4. package/bin/aiwg.mjs +135 -47
  5. package/dist/src/a2a/codecs.js +9 -0
  6. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  7. package/dist/src/artifacts/browser-export.js +6 -0
  8. package/dist/src/artifacts/checksum-manifest.js +1 -0
  9. package/dist/src/artifacts/citation-parser.js +173 -92
  10. package/dist/src/artifacts/citation-table-schema.js +59 -0
  11. package/dist/src/artifacts/cli.js +57 -18
  12. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  13. package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
  14. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  15. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  16. package/dist/src/artifacts/discovery-eval.js +6 -4
  17. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  18. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  19. package/dist/src/artifacts/framework-capabilities.js +171 -0
  20. package/dist/src/artifacts/framework-index-paths.js +73 -0
  21. package/dist/src/artifacts/framework-source-digest.js +75 -0
  22. package/dist/src/artifacts/index-builder.js +285 -131
  23. package/dist/src/artifacts/index-files.js +132 -18
  24. package/dist/src/artifacts/index-preparation.js +65 -0
  25. package/dist/src/artifacts/index-refresh.js +576 -0
  26. package/dist/src/artifacts/index-worker.mjs +9 -0
  27. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  28. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  29. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  30. package/dist/src/artifacts/query-engine.js +410 -88
  31. package/dist/src/artifacts/relations-builder.js +168 -0
  32. package/dist/src/artifacts/relations.js +148 -0
  33. package/dist/src/artifacts/repair.js +9 -4
  34. package/dist/src/artifacts/stats.js +5 -2
  35. package/dist/src/artifacts/types.js +30 -10
  36. package/dist/src/artifacts/watcher.js +1 -1
  37. package/dist/src/auth/credential-store.js +2 -1
  38. package/dist/src/catalog/cli.d.mts +5 -0
  39. package/dist/src/channel/manager.d.mts +20 -0
  40. package/dist/src/channel/manager.mjs +78 -12
  41. package/dist/src/cli/child-process-result.js +40 -0
  42. package/dist/src/cli/command-log.js +2 -3
  43. package/dist/src/cli/env.js +38 -16
  44. package/dist/src/cli/flag-parsing.js +236 -0
  45. package/dist/src/cli/handlers/artifacts.js +3 -0
  46. package/dist/src/cli/handlers/auth.js +3 -0
  47. package/dist/src/cli/handlers/decision.js +218 -5
  48. package/dist/src/cli/handlers/diagnose.js +13 -5
  49. package/dist/src/cli/handlers/effect.js +4 -2
  50. package/dist/src/cli/handlers/evidence.js +3 -0
  51. package/dist/src/cli/handlers/feedback.js +14 -5
  52. package/dist/src/cli/handlers/help.js +1 -0
  53. package/dist/src/cli/handlers/index.js +3 -1
  54. package/dist/src/cli/handlers/init.js +248 -30
  55. package/dist/src/cli/handlers/lint.js +5 -0
  56. package/dist/src/cli/handlers/mc.js +23 -0
  57. package/dist/src/cli/handlers/models.js +169 -10
  58. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  59. package/dist/src/cli/handlers/ralph.js +3 -0
  60. package/dist/src/cli/handlers/refresh.js +28 -0
  61. package/dist/src/cli/handlers/regenerate.js +14 -0
  62. package/dist/src/cli/handlers/resource-versions.js +3 -0
  63. package/dist/src/cli/handlers/rules.js +195 -0
  64. package/dist/src/cli/handlers/run.js +4 -0
  65. package/dist/src/cli/handlers/runtime-info.js +25 -11
  66. package/dist/src/cli/handlers/script-runner.js +19 -8
  67. package/dist/src/cli/handlers/session.js +406 -62
  68. package/dist/src/cli/handlers/sessions.js +243 -76
  69. package/dist/src/cli/handlers/setup-manifest.js +474 -79
  70. package/dist/src/cli/handlers/setup.js +180 -39
  71. package/dist/src/cli/handlers/steward.js +11 -0
  72. package/dist/src/cli/handlers/subcommands.js +60 -3
  73. package/dist/src/cli/handlers/team.js +18 -8
  74. package/dist/src/cli/handlers/uhp.js +3 -0
  75. package/dist/src/cli/handlers/use.js +307 -55
  76. package/dist/src/cli/handlers/utilities.js +125 -26
  77. package/dist/src/cli/handlers/version.js +8 -0
  78. package/dist/src/cli/prompt-utils.js +72 -0
  79. package/dist/src/cli/provider-resolution.js +11 -0
  80. package/dist/src/cli/router.js +15 -1
  81. package/dist/src/cli/services/deployment-verification.js +19 -9
  82. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  83. package/dist/src/cli/skill-usage.js +2 -3
  84. package/dist/src/config/aiwg-config.js +349 -35
  85. package/dist/src/config/cli.js +256 -23
  86. package/dist/src/config/decision-config.js +106 -0
  87. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  88. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  89. package/dist/src/config/gitignore.js +50 -0
  90. package/dist/src/config/index-graph-overrides.js +36 -0
  91. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  92. package/dist/src/config/project-artifacts-health.mjs +14 -3
  93. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  94. package/dist/src/config/user-config-dir.d.mts +9 -0
  95. package/dist/src/config/user-config.js +38 -11
  96. package/dist/src/config/user-registry.js +23 -15
  97. package/dist/src/config/user-state.js +7 -0
  98. package/dist/src/decision/adapters/jev.js +75 -27
  99. package/dist/src/decision/ask.js +203 -43
  100. package/dist/src/decision/context-live-qualification.js +2 -2
  101. package/dist/src/decision/driver.js +14 -7
  102. package/dist/src/decision/ensemble/aggregate.js +23 -10
  103. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  104. package/dist/src/decision/entry.js +25 -6
  105. package/dist/src/decision/graph-live-qualification.js +1 -1
  106. package/dist/src/decision/graph-run.js +9 -0
  107. package/dist/src/decision/heldout/collector.js +15 -14
  108. package/dist/src/decision/heldout/contract.js +30 -7
  109. package/dist/src/decision/index.js +1 -0
  110. package/dist/src/decision/interop.js +60 -8
  111. package/dist/src/decision/jev-credentials.js +15 -1
  112. package/dist/src/decision/ledger.js +942 -0
  113. package/dist/src/decision/offload-activation.js +137 -0
  114. package/dist/src/decision/offload.js +550 -0
  115. package/dist/src/decision/qualification/load.js +4 -6
  116. package/dist/src/decision/savings.js +28 -0
  117. package/dist/src/decision/validate.js +6 -3
  118. package/dist/src/effects/dispatch-verifier.js +252 -0
  119. package/dist/src/effects/index.js +1 -0
  120. package/dist/src/effects/types.js +5 -1
  121. package/dist/src/effects/verifiers/git.js +2 -1
  122. package/dist/src/effects/verifiers/tracker.js +26 -14
  123. package/dist/src/extensions/commands/definitions.js +19 -0
  124. package/dist/src/extensions/project-local-remove.js +36 -1
  125. package/dist/src/features/installer.js +2 -2
  126. package/dist/src/flow/graph-metadata.js +5 -0
  127. package/dist/src/gates/providers/loader.js +64 -18
  128. package/dist/src/installation/manager-command.d.mts +18 -0
  129. package/dist/src/installation/manager-command.mjs +15 -3
  130. package/dist/src/installation/manager.d.mts +49 -0
  131. package/dist/src/installation/manager.mjs +95 -5
  132. package/dist/src/issues/cli.js +4 -4
  133. package/dist/src/issues/local.js +24 -5
  134. package/dist/src/jobs/runner.js +1 -1
  135. package/dist/src/lint/cli.js +27 -1
  136. package/dist/src/lint/loader.js +2 -1
  137. package/dist/src/lint/reporters.js +25 -1
  138. package/dist/src/lint/rule-activation.js +43 -0
  139. package/dist/src/lint/runner.js +137 -62
  140. package/dist/src/mcp/adapters/codex-runtime.js +366 -56
  141. package/dist/src/mcp/claude-install.d.mts +4 -0
  142. package/dist/src/mcp/claude-install.mjs +41 -0
  143. package/dist/src/mcp/cli.d.mts +1 -0
  144. package/dist/src/mcp/cli.mjs +620 -85
  145. package/dist/src/mcp/config-file.d.mts +28 -0
  146. package/dist/src/mcp/config-file.mjs +279 -0
  147. package/dist/src/mcp/config-layers.d.mts +36 -0
  148. package/dist/src/mcp/config-layers.mjs +348 -0
  149. package/dist/src/mcp/credentials.d.mts +51 -0
  150. package/dist/src/mcp/credentials.mjs +484 -0
  151. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  152. package/dist/src/mcp/grok-build-config.mjs +17 -27
  153. package/dist/src/mcp/helpers.mjs +109 -4
  154. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  155. package/dist/src/mcp/muse-native-config.mjs +1 -0
  156. package/dist/src/mcp/omp-config.d.mts +4 -0
  157. package/dist/src/mcp/omp-config.mjs +50 -29
  158. package/dist/src/mcp/profiles.js +97 -23
  159. package/dist/src/mcp/profiles.mjs +99 -27
  160. package/dist/src/mcp/registry.js +248 -131
  161. package/dist/src/mcp/registry.mjs +169 -100
  162. package/dist/src/mcp/server-config.d.mts +3 -0
  163. package/dist/src/mcp/server-config.mjs +89 -0
  164. package/dist/src/mcp/server.mjs +127 -82
  165. package/dist/src/mcp/test-simple.mjs +2 -1
  166. package/dist/src/mcp/toml-editor.d.mts +4 -0
  167. package/dist/src/mcp/toml-string.d.mts +1 -0
  168. package/dist/src/mcp/toml-string.mjs +9 -0
  169. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  170. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  171. package/dist/src/mcp/tool-filters.d.mts +46 -0
  172. package/dist/src/mcp/tool-filters.mjs +357 -0
  173. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  174. package/dist/src/mcp/tools/decision.mjs +22 -3
  175. package/dist/src/mcp/tools/discovery.mjs +62 -55
  176. package/dist/src/mcp/tools/orchestration.mjs +50 -2
  177. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  178. package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
  179. package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
  180. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  181. package/dist/src/memory/project-registry.js +2 -2
  182. package/dist/src/mission-protocol/codecs.js +647 -1
  183. package/dist/src/mission-protocol/types.js +110 -0
  184. package/dist/src/mission-protocol/worker-status.js +2 -0
  185. package/dist/src/models/config-loader.js +76 -14
  186. package/dist/src/models/grok-build-models.js +93 -41
  187. package/dist/src/models/model-capabilities.v1.json +3183 -24
  188. package/dist/src/models/model-discovery.js +931 -67
  189. package/dist/src/models/provider-policy.js +719 -11
  190. package/dist/src/models/qualification-digest.d.mts +2 -0
  191. package/dist/src/models/qualification-digest.mjs +17 -0
  192. package/dist/src/models/resolver.js +261 -0
  193. package/dist/src/models/worker-direct-readiness.js +550 -0
  194. package/dist/src/models/worker-dispatch-offer.js +169 -0
  195. package/dist/src/models/worker-health-readiness.js +144 -0
  196. package/dist/src/models/worker-installation.d.mts +39 -0
  197. package/dist/src/models/worker-installation.mjs +986 -0
  198. package/dist/src/models/wrapper-route.js +18 -0
  199. package/dist/src/ops/registry.js +9 -14
  200. package/dist/src/plugins/standalone-packager.js +4 -3
  201. package/dist/src/process/bounded-child-process.d.mts +84 -0
  202. package/dist/src/process/bounded-child-process.mjs +430 -0
  203. package/dist/src/process/cli-command.d.mts +16 -0
  204. package/dist/src/process/cli-command.mjs +75 -0
  205. package/dist/src/process/signal-child.d.mts +6 -0
  206. package/dist/src/process/signal-child.mjs +7 -0
  207. package/dist/src/providers/capability-matrix.js +8 -0
  208. package/dist/src/providers/capability-matrix.yaml +270 -0
  209. package/dist/src/providers/deployed-skill-roots.js +20 -0
  210. package/dist/src/providers/grok-build-acp.js +3 -2
  211. package/dist/src/providers/grok-build-headless.js +53 -23
  212. package/dist/src/providers/grok-build-inspect.js +1 -0
  213. package/dist/src/providers/omp-agent.d.mts +8 -0
  214. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  215. package/dist/src/providers/omp-paths.d.mts +13 -0
  216. package/dist/src/providers/provider-definitions.js +9 -1
  217. package/dist/src/providers/provider-definitions.mjs +9 -2
  218. package/dist/src/providers/skill-ownership.d.mts +9 -0
  219. package/dist/src/providers/skill-ownership.mjs +88 -0
  220. package/dist/src/rules/activation.d.mts +21 -0
  221. package/dist/src/rules/activation.mjs +62 -0
  222. package/dist/src/rules/inline-budget.d.mts +26 -0
  223. package/dist/src/rules/inline-budget.mjs +66 -0
  224. package/dist/src/rules/inventory.js +129 -0
  225. package/dist/src/rules/pin-state.js +73 -0
  226. package/dist/src/rules/pins.d.mts +14 -0
  227. package/dist/src/rules/pins.mjs +112 -0
  228. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  229. package/dist/src/serve/executor-registry.js +16 -24
  230. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  231. package/dist/src/serve/mission-conductor.js +47 -12
  232. package/dist/src/serve/shared-host-scheduler.js +2 -0
  233. package/dist/src/serve/worker-invocation.js +558 -0
  234. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  235. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  236. package/dist/src/sessions/adapter-registry.js +49 -0
  237. package/dist/src/sessions/adapters/claude.js +39 -54
  238. package/dist/src/sessions/adapters/codex.js +82 -67
  239. package/dist/src/sessions/adapters/copilot.js +4 -4
  240. package/dist/src/sessions/adapters/cursor.js +40 -57
  241. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  242. package/dist/src/sessions/adapters/factory.js +31 -27
  243. package/dist/src/sessions/adapters/generic.js +43 -9
  244. package/dist/src/sessions/adapters/grok-build.js +11 -11
  245. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  246. package/dist/src/sessions/adapters/grokbot.js +434 -24
  247. package/dist/src/sessions/adapters/hermes.js +27 -29
  248. package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
  249. package/dist/src/sessions/adapters/muse.js +200 -104
  250. package/dist/src/sessions/adapters/omp.js +28 -14
  251. package/dist/src/sessions/adapters/openclaw.js +89 -79
  252. package/dist/src/sessions/adapters/opencode.js +27 -24
  253. package/dist/src/sessions/adapters/openhuman.js +18 -60
  254. package/dist/src/sessions/adapters/pi.js +30 -14
  255. package/dist/src/sessions/adapters/warp.js +3 -3
  256. package/dist/src/sessions/adapters/windsurf.js +19 -48
  257. package/dist/src/sessions/batch-contracts.js +25 -5
  258. package/dist/src/sessions/batch-import.js +72 -51
  259. package/dist/src/sessions/candidates.js +47 -11
  260. package/dist/src/sessions/contracts.js +75 -2
  261. package/dist/src/sessions/coverage-preflight.js +134 -0
  262. package/dist/src/sessions/discovery.js +3 -0
  263. package/dist/src/sessions/export-plan.js +4 -3
  264. package/dist/src/sessions/grok-build-cli.js +67 -5
  265. package/dist/src/sessions/import-exclusions.js +72 -0
  266. package/dist/src/sessions/import-lease.js +34 -18
  267. package/dist/src/sessions/import-limits.js +37 -0
  268. package/dist/src/sessions/importer.js +84 -46
  269. package/dist/src/sessions/index.js +2 -0
  270. package/dist/src/sessions/lifecycle.js +45 -0
  271. package/dist/src/sessions/policy.js +133 -25
  272. package/dist/src/sessions/readers.js +106 -53
  273. package/dist/src/sessions/refresh.js +106 -0
  274. package/dist/src/sessions/repository.js +199 -63
  275. package/dist/src/sessions/workspace-discovery.js +315 -88
  276. package/dist/src/sessions/workspace-matching.js +107 -0
  277. package/dist/src/skills/adapters/clawhub.js +10 -8
  278. package/dist/src/skills/adapters/openclaw.js +10 -7
  279. package/dist/src/skills/importer.js +1 -0
  280. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  281. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  282. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  283. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  284. package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
  285. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  286. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  287. package/dist/src/smiths/mcpsmith/example.js +1 -1
  288. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  289. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  290. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
  291. package/dist/src/storage/backends/fs.js +18 -76
  292. package/dist/src/storage/backends/logseq.js +68 -7
  293. package/dist/src/storage/backends/obsidian.js +1 -0
  294. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  295. package/dist/src/testing/generators/test-code-generator.js +57 -30
  296. package/dist/src/tracker/forge-http.js +2 -1
  297. package/dist/src/uhp/client.js +11 -1
  298. package/dist/src/uhp/mission.js +5 -0
  299. package/dist/src/update/checker.d.mts +2 -0
  300. package/dist/src/update/checker.mjs +5 -5
  301. package/dist/src/update/notifier.mjs +54 -2
  302. package/dist/src/update/service.d.mts +22 -0
  303. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  304. package/dist/src/utils/contained-filesystem.mjs +683 -0
  305. package/dist/src/utils/temporary-directory.js +36 -0
  306. package/dist/src/writing/example-generator.js +1 -1
  307. package/dist/src/writing/fidelity.js +18 -1
  308. package/package.json +32 -2
  309. package/schemas/mission-v1.schema.json +2429 -0
  310. package/schemas/models/model-catalog.v1.schema.json +301 -0
  311. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  312. package/schemas/models/model-policy.v1.schema.json +14 -0
  313. package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
  314. package/tools/_resolve-impl.mjs +39 -2
  315. package/tools/agents/deploy-agents.mjs +64 -23
  316. package/tools/agents/providers/base.mjs +153 -166
  317. package/tools/agents/providers/claude.mjs +8 -24
  318. package/tools/agents/providers/codex.mjs +18 -11
  319. package/tools/agents/providers/cursor.mjs +19 -18
  320. package/tools/agents/providers/grok-build.mjs +3 -1
  321. package/tools/agents/providers/grokbot.mjs +11 -12
  322. package/tools/agents/providers/hermes.mjs +62 -49
  323. package/tools/agents/providers/omp.mjs +5 -2
  324. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  325. package/tools/agents/providers/openclaw.mjs +9 -4
  326. package/tools/agents/providers/pi.mjs +5 -0
  327. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  328. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  329. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  330. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  331. package/tools/agents/providers/warp.mjs +20 -26
  332. package/tools/cli/decision-offload-report.mjs +40 -0
  333. package/tools/cli/workspace-status.mjs +862 -0
  334. package/tools/plugin/package-plugins.mjs +186 -8
  335. package/tools/plugin/plugin-links.mjs +114 -0
  336. package/tools/providers/antigravity-transport.mjs +3 -1
  337. package/tools/providers/deepseek-harness-transport.mjs +360 -0
  338. package/tools/providers/omp-transport.mjs +194 -0
  339. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  340. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  341. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  342. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  343. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  344. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  345. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  346. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  347. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  348. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  349. package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
  350. package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
  351. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  352. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  353. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  354. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  355. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  356. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  357. package/tools/ralph-external/process-monitor.mjs +1017 -0
  358. package/tools/ralph-external/session-launcher.mjs +5057 -0
  359. package/tools/scripts/resource-preflight.mjs +541 -0
  360. package/tools/skills/deploy-skills-codex.mjs +2 -1
  361. package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
@@ -21,6 +21,7 @@
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import { spawnSync } from 'child_process';
24
+ import { boundedSpawnSync, positiveTimeoutMs } from '../../process/bounded-child-process.mjs';
24
25
  /** Strict-transcription prompt. `title` parameterizes the corpus-specific intro. */
25
26
  export function buildPrompt(page, title) {
26
27
  const subject = title ? `from ${title}` : 'from a scanned document';
@@ -53,8 +54,13 @@ const codexAdapter = {
53
54
  try {
54
55
  const r = spawnSync(process.env.AIWG_CODEX_BIN || 'codex', args, {
55
56
  input: prompt,
56
- timeout: ctx.timeoutSec * 1000,
57
+ // ctx.timeoutSec is caller-controlled (a CLI flag); clamp it so a
58
+ // configured 0 (or anything else non-positive) can't disable the
59
+ // bound entirely (#3062 B/C finding 8). 240s matches this
60
+ // function's own documented default (see extractPages() below).
61
+ timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000),
57
62
  encoding: 'utf8',
63
+ killSignal: 'SIGKILL',
58
64
  });
59
65
  if (r.status === 0 && fs.existsSync(tmp)) {
60
66
  const text = fs.readFileSync(tmp, 'utf8');
@@ -70,15 +76,93 @@ const codexAdapter = {
70
76
  }
71
77
  },
72
78
  };
79
+ /** Paths enter the shell as positional arguments, never as shell source. */
80
+ const MAX_VISION_TEMPLATE_LENGTH = 4096;
81
+ function visionCommand(template) {
82
+ const guidance = 'Use "$1"/"$2"/"$3" as data operands or move complex logic to a wrapper script';
83
+ const reject = (construct) => {
84
+ throw new Error(`Vision command rejects ${construct}. ${guidance}`);
85
+ };
86
+ // Bound the checks below; a vision command template is a short command line.
87
+ if (template.length > MAX_VISION_TEMPLATE_LENGTH)
88
+ reject(`templates longer than ${MAX_VISION_TEMPLATE_LENGTH} characters`);
89
+ // A vision template is one command line; a backslash-newline continuation can
90
+ // join an allowed name with following text into a different variable at runtime.
91
+ if (/\\\r?\n|\\\r/.test(template))
92
+ reject('line continuations (backslash-newline)');
93
+ // Recognize literal command names even when quoted or backslash-escaped.
94
+ const syntax = template.replace(/\\([^\n])/g, '$1').replace(/["']/g, '');
95
+ if (syntax.includes('$('))
96
+ reject('command substitution ($(...))');
97
+ if (template.includes('`'))
98
+ reject('backticks');
99
+ if (/\beval\b/.test(syntax))
100
+ reject('eval');
101
+ if (syntax.includes('<<'))
102
+ reject('here-documents and here-strings (<<)');
103
+ // Conservatively reject a shell word together with any short -c option
104
+ // cluster, regardless of order, wrappers, redirections or command boundaries.
105
+ const words = syntax.split(/[\s;&|(){}<>]+/);
106
+ const shellWords = words.map(word => word.slice(word.lastIndexOf('/') + 1)
107
+ .match(/^(?:sh|bash|zsh|dash|ksh|ash|mksh|yash|fish|csh|tcsh|rbash|busybox|toybox)(?:$|(-[A-Za-z]*c))/));
108
+ const hasShell = shellWords.some(Boolean);
109
+ if (shellWords.some(word => word?.[1]) || (hasShell && words.some(word => /^-[A-Za-z]*c/.test(word))))
110
+ reject('nested shell invocation (-c)');
111
+ // Only contract-provided parameters may expand; match at each dollar without
112
+ // rescanning the remaining template. Braces must contain only the exact name.
113
+ const parameter = /\$(?:[123](?![0-9])|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT)(?![A-Za-z0-9_])|\{(?:[123]|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT))\})/y;
114
+ // Track quotes to reject quoted/escaped placeholders, expansions and comments.
115
+ let quote = '';
116
+ return template.replace(/\\?\{(image|prompt_file|out)\}|\\[\s\S]|\$'|[$"'#]/g, (token, name, offset) => {
117
+ if ((token === '#' || token === '\\#') && quote !== "'")
118
+ reject('comments (# outside single quotes)');
119
+ if (name) {
120
+ if (quote)
121
+ throw new Error(`Vision command placeholders must be outside shell quotes. ${guidance}`);
122
+ if (token.startsWith('\\'))
123
+ throw new Error(`Vision command placeholders must not be escaped. ${guidance}`);
124
+ return `"$${name === 'image' ? 1 : name === 'prompt_file' ? 2 : 3}"`;
125
+ }
126
+ if (token === '#')
127
+ return token;
128
+ // Backslashes are literal inside POSIX single quotes, but escape the next
129
+ // character in double quotes and ANSI-C ($'...') quotes.
130
+ if (token.startsWith('\\')) {
131
+ if (quote === "'" && token[1] === "'")
132
+ quote = '';
133
+ return token;
134
+ }
135
+ if (token.startsWith('$')) {
136
+ if (quote !== "'") {
137
+ parameter.lastIndex = offset;
138
+ if (!parameter.test(template))
139
+ reject('parameter expansions other than the provided vision variables');
140
+ }
141
+ if (token === '$')
142
+ return token;
143
+ }
144
+ if (!quote)
145
+ quote = token;
146
+ else if (quote === token || (quote === "$'" && token === "'") || (quote === "'" && token === "$'"))
147
+ quote = '';
148
+ return token;
149
+ });
150
+ }
73
151
  /**
74
152
  * Generic command-template adapter (provider-neutral). Runs
75
153
  * `AIWG_VISION_COMMAND` (or the `--command` flag) with placeholders:
76
- * {image} absolute image path
77
- * {prompt_file} path to a file containing the prompt
78
- * {out} path the command must write the transcription to
79
- * e.g. claude -p "$(cat {prompt_file})" --image {image} > {out}
154
+ * {image} "$1": absolute image path
155
+ * {prompt_file} "$2": path to a file containing the prompt
156
+ * {out} "$3": path the command must write the transcription to
157
+ * Placeholders must be unquoted and unescaped. AIWG_VISION_IMAGE /
158
+ * AIWG_VISION_PROMPT_FILE / AIWG_VISION_OUT are also exported.
159
+ * Only $1/$2/$3 and the exported vision variables (also braced) may expand.
160
+ * Other $ expansions, command substitution, backticks, eval, nested shells with -c, <<, and #
161
+ * outside single quotes are rejected; use a wrapper script for complex logic.
162
+ * e.g. vision-cli --prompt-file {prompt_file} --image {image} > {out}
80
163
  */
81
164
  function commandAdapter(template) {
165
+ const cmd = visionCommand(template);
82
166
  return {
83
167
  name: 'command',
84
168
  transcribe(imagePath, prompt, ctx) {
@@ -86,12 +170,13 @@ function commandAdapter(template) {
86
170
  const promptFile = path.join(dir, `.vision-prompt-${ctx.page}.txt`);
87
171
  const out = path.join(dir, `.vision-out-${ctx.page}.txt`);
88
172
  fs.writeFileSync(promptFile, prompt);
89
- const cmd = template
90
- .replaceAll('{image}', JSON.stringify(imagePath))
91
- .replaceAll('{prompt_file}', JSON.stringify(promptFile))
92
- .replaceAll('{out}', JSON.stringify(out));
93
173
  try {
94
- const r = spawnSync('sh', ['-c', cmd], { timeout: ctx.timeoutSec * 1000, encoding: 'utf8' });
174
+ // Same caller-controlled-timeout clamp as the codex adapter above.
175
+ const r = spawnSync('sh', ['-c', cmd, 'aiwg-vision', imagePath, promptFile, out], {
176
+ timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000), encoding: 'utf8',
177
+ env: { ...process.env, AIWG_VISION_IMAGE: imagePath, AIWG_VISION_PROMPT_FILE: promptFile, AIWG_VISION_OUT: out },
178
+ killSignal: 'SIGKILL',
179
+ });
95
180
  let text;
96
181
  if (fs.existsSync(out))
97
182
  text = fs.readFileSync(out, 'utf8');
@@ -115,7 +200,7 @@ export function resolveAdapter(opts = {}) {
115
200
  const provider = opts.provider ?? process.env.AIWG_VISION_PROVIDER ?? (command ? 'command' : 'codex');
116
201
  if (provider === 'command') {
117
202
  if (!command)
118
- throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with {image}/{prompt_file}/{out})");
203
+ throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with unquoted, unescaped {image}/{prompt_file}/{out} placeholders)");
119
204
  return commandAdapter(command);
120
205
  }
121
206
  if (provider === 'codex')
@@ -125,7 +210,8 @@ export function resolveAdapter(opts = {}) {
125
210
  // ── PDF → PNG rasterization (poppler pdftoppm) ───────────────────────────────
126
211
  export function pdftoppmAvailable() {
127
212
  try {
128
- return spawnSync('pdftoppm', ['-v'], { encoding: 'utf8' }).status === 0 || spawnSync('pdftoppm', ['-h'], { encoding: 'utf8' }).status !== null;
213
+ return boundedSpawnSync('pdftoppm', ['-v'], 'quick', { encoding: 'utf8' }).status === 0
214
+ || boundedSpawnSync('pdftoppm', ['-h'], 'quick', { encoding: 'utf8' }).status !== null;
129
215
  }
130
216
  catch {
131
217
  return false;
@@ -144,9 +230,15 @@ export function rasterizePdf(pdfPath, outDir, dpi = 200) {
144
230
  fs.mkdirSync(outDir, { recursive: true });
145
231
  // pdftoppm writes <prefix>-<n>.png with its own zero-padding based on page count.
146
232
  const prefix = path.join(outDir, 'page');
147
- const r = spawnSync('pdftoppm', ['-png', '-r', String(dpi), pdfPath, prefix], { encoding: 'utf8' });
148
- if (r.status !== 0)
149
- throw new Error(`pdftoppm failed: ${r.stderr || r.status}`);
233
+ // Rasterizing a multi-page PDF can take a while; long tier (#3062).
234
+ const r = boundedSpawnSync('pdftoppm', ['-png', '-r', String(dpi), pdfPath, prefix], 'long', { encoding: 'utf8' });
235
+ if (r.status !== 0) {
236
+ // #3062 finding 14: r.error.message (when present) carries the
237
+ // enriched timeout explanation (tier/bound/override) that stderr/
238
+ // status alone do not — put it first, captured stderr after.
239
+ const detail = r.error ? r.error.message : `pdftoppm failed: ${r.stderr || r.status}`;
240
+ throw new Error(r.error && r.stderr ? `${detail} (stderr: ${r.stderr})` : detail);
241
+ }
150
242
  // Normalize names to page-NNN.png (3-digit zero-pad).
151
243
  let count = 0;
152
244
  for (const f of fs.readdirSync(outDir)) {
@@ -326,11 +326,13 @@ export function loadCorpus(root) {
326
326
  const venue = classifyFirst(`${venueRaw ?? ''} ${text.slice(0, 3500)}`.toLowerCase(), VENUE_PATTERNS) ||
327
327
  (venueRaw ? String(venueRaw).trim() : null);
328
328
  const { tier, paramsM } = extractParams(text);
329
+ const radar = extractRadar(root, refId);
329
330
  records.push({
330
331
  refId,
331
332
  sourceType: normalizeSourceType({ type: asStr(fm.type), sourceType: asStr(fm.source_type), venue }, srcTypeReg),
332
333
  referencedByProjects: extractReferencedByProjects(text),
333
334
  title: extractTitle(text, fm),
335
+ grade: radar?.gradeCurrent ?? asStr(fm.grade),
334
336
  path: filePath,
335
337
  year: extractYear(text, fm, citation),
336
338
  authors,
@@ -343,7 +345,7 @@ export function loadCorpus(root) {
343
345
  paramsM,
344
346
  incoming,
345
347
  outgoing,
346
- radar: extractRadar(root, refId),
348
+ radar,
347
349
  discovery: extractDiscovery(citation),
348
350
  funders: extractFunders(citation),
349
351
  });
@@ -242,9 +242,9 @@ function renderPipeline(ctx) {
242
242
  // Cadence → window math lives in corpus-config.ts (the one staleness helper, #1498/#1502).
243
243
  /** GRADE display order; unknown grades fall after, 'Ungraded' last. */
244
244
  const GRADE_ORDER = ['A', 'A-', 'B', 'B-', 'C', 'C-', 'D'];
245
- /** Papers grouped by current GRADE (radar `grade-current`); no-radar → Ungraded. (#1492) */
245
+ /** Papers grouped by current GRADE, falling back to reference frontmatter. */
246
246
  function renderByGrade(ctx) {
247
- const groups = groupBy(ctx.records, (r) => [r.radar?.gradeCurrent || 'Ungraded']);
247
+ const groups = groupBy(ctx.records, (r) => [r.grade || 'Ungraded']);
248
248
  const lines = header('Index: Papers by GRADE', ctx, ctx.records.length);
249
249
  const names = [...groups.keys()].sort((a, b) => {
250
250
  if (a === 'Ungraded')
@@ -284,7 +284,7 @@ function renderStaleQueue(ctx) {
284
284
  const lines = header('Index: Radar Stale-Refresh Queue', ctx, ctx.records.length);
285
285
  lines.push(`Overdue = days since last refresh minus the cadence window. ${staleCount} of ${rows.length} radar-tracked papers are overdue.`, '', '| REF | Title | GRADE | Cadence | Last Refreshed | Overdue (days) | Stale |', '|---|---|---|---|---|---:|---|');
286
286
  for (const { r, cadence, last, overdue } of sorted) {
287
- lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.radar.gradeCurrent ?? '—'} | ${cadence} | ${last} | ${overdue} | ${overdue > 0 ? 'yes' : 'no'} |`);
287
+ lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.grade ?? '—'} | ${cadence} | ${last} | ${overdue} | ${overdue > 0 ? 'yes' : 'no'} |`);
288
288
  }
289
289
  return lines.join('\n') + '\n';
290
290
  }
@@ -2,7 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { performance } from 'node:perf_hooks';
5
- import { OPERATIONAL_DISCOVERY_TYPES } from './types.js';
5
+ import { getGraphIndexDir, OPERATIONAL_DISCOVERY_TYPES } from './types.js';
6
+ import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
6
7
  import { loadGraphIndexFile } from './index-reader.js';
7
8
  import { loadFortemiCoreExport, loadFortemiCoreMetadataEntries, scoreStaticRecord } from './fortemi-core-query-adapter.js';
8
9
  import { discoverCapability } from './query-engine.js';
@@ -185,8 +186,9 @@ export function calculateDiscoveryMetrics(records, resultSets) {
185
186
  .some((item) => record.hard_negative_ids.some((id) => matches(item, id)))).length / records.length),
186
187
  };
187
188
  }
188
- function indexBytes(backend) {
189
- const root = path.join(process.env.XDG_DATA_HOME ?? path.join(os.homedir(), '.local', 'share'), 'aiwg', 'index', ...(backend === 'local' ? ['framework'] : ['fortemi-core', 'framework']));
189
+ function indexBytes(cwd, backend) {
190
+ const root = backend === 'local' ? getGraphIndexDir(cwd, 'framework')
191
+ : path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', 'framework');
190
192
  let total = 0;
191
193
  const visit = (target) => {
192
194
  if (!fs.existsSync(target))
@@ -259,7 +261,7 @@ export async function evaluateDiscovery(options) {
259
261
  configuration: { backend: options.backend, strategy: options.strategy, limit, graph: 'framework' },
260
262
  hardware: { platform: os.platform(), arch: os.arch(), cpu_model: cpus[0]?.model ?? 'unknown', logical_cpus: cpus.length, total_memory_bytes: os.totalmem(), node: process.version },
261
263
  metrics,
262
- performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.backend), peak_resident_memory_bytes: peakRss },
264
+ performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.cwd, options.backend), peak_resident_memory_bytes: peakRss },
263
265
  ...(parity ? { parity } : {}),
264
266
  adoption_gate: {
265
267
  baseline: 'local:lexical', no_per_type_hit_at_3_regression: null, aggregate_mrr_improvement: null,
@@ -1,7 +1,11 @@
1
+ import { selectPrebuiltRelations } from './relations.js';
2
+ import { resolveFrameworkIndexRoot } from './framework-index-paths.js';
3
+ import { extractRuleBindings } from './index-builder.js';
1
4
  import fs from "node:fs";
2
5
  import { createHash } from "node:crypto";
3
6
  import { bm25Rank } from "./fulltext.js";
4
- import { getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
7
+ import { consumeFortemiCoreExport, getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
8
+ import { loadGraphIndexFile } from "./index-reader.js";
5
9
  import { operationalStateQueryProjection, } from "./operational-state.js";
6
10
  function tokenize(text) {
7
11
  return text
@@ -167,6 +171,14 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
167
171
  };
168
172
  }
169
173
  }
174
+ // A content-validated shipped corpus does not need the source metadata index.
175
+ // This also avoids preferring a much larger locally materialized text cache.
176
+ if (graph === 'framework' && selectPrebuiltRelations(cwd).reader) {
177
+ const prebuilt = getFortemiCorePrebuiltStatus(graph, resolveFrameworkIndexRoot(cwd));
178
+ if (prebuilt.built && !prebuilt.stale) {
179
+ return { exported: JSON.parse(fs.readFileSync(prebuilt.exportPath, 'utf8')) };
180
+ }
181
+ }
170
182
  let status = getFortemiCoreSyncStatus(cwd, graph);
171
183
  // The packaged framework index is a bootstrap fallback for an absent shared
172
184
  // cache. Once a shared cache exists, its stale state proves that the local
@@ -178,6 +190,20 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
178
190
  status = prebuilt;
179
191
  }
180
192
  }
193
+ // A packaged bootstrap cache can also predate a source checkout's index.
194
+ // Validate it against the same current data as the shared cache before use.
195
+ if (status.source === "prebuilt") {
196
+ const current = loadGraphIndexFile(cwd, "metadata.json", graph);
197
+ if (current &&
198
+ Date.parse(current.builtAt) > Date.parse(status.sourceIndexBuiltAt ?? status.generatedAt ?? "")) {
199
+ status = {
200
+ ...status,
201
+ stale: true,
202
+ sourceIndexNewer: true,
203
+ reason: "source index is newer than the Fortemi Core static cache",
204
+ };
205
+ }
206
+ }
181
207
  if (!status.optedIn) {
182
208
  return {
183
209
  reason: `Fortemi Core static index is not materialized for graph '${graph}'. Run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' first, or pass '--backend local' to use the legacy local index.`,
@@ -185,12 +211,13 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
185
211
  }
186
212
  if (!status.built || status.stale) {
187
213
  return {
214
+ ...(status.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
188
215
  reason: `${status.reason ??
189
216
  `Fortemi Core static index for graph '${graph}' is stale or incomplete`}. Re-run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' or pass '--backend local' to use the legacy local index.`,
190
217
  };
191
218
  }
192
219
  try {
193
- const exported = JSON.parse(fs.readFileSync(status.exportPath, "utf-8"));
220
+ const exported = consumeFortemiCoreExport(status.exportPath, status.exportChecksum);
194
221
  if (exported.schema_version !== "aiwg.fortemi.index.export.v2") {
195
222
  return {
196
223
  reason: `Fortemi Core static index has schema '${exported.schema_version}', expected 'aiwg.fortemi.index.export.v2'. Re-run 'aiwg index sync'.`,
@@ -255,13 +282,19 @@ function entryFromRecord(record) {
255
282
  && typeof indexedScript.runtime === "string"
256
283
  ? indexedScript
257
284
  : undefined,
285
+ ...extractRuleBindings(indexedFrontmatter),
286
+ ...(Array.isArray(indexedFrontmatter.aiwg_governed_paths) ? { governedPaths: indexedFrontmatter.aiwg_governed_paths.filter((value) => typeof value === "string") } : {}),
258
287
  operationalState: record.operational_state,
259
288
  };
260
289
  }
261
290
  export function loadFortemiCoreMetadataEntries(cwd, graph = "project", sourcePaths) {
262
291
  const loaded = loadFortemiCoreExport(cwd, graph, sourcePaths);
263
292
  if (!loaded.exported)
264
- return { entries: [], reason: loaded.reason };
293
+ return {
294
+ entries: [],
295
+ reason: loaded.reason,
296
+ ...(loaded.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
297
+ };
265
298
  return { entries: loaded.exported.items.map(entryFromRecord) };
266
299
  }
267
300
  export async function queryFortemiCoreAiwgDiscovery(cwd, options) {
@@ -1,15 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parse as parseYaml } from "yaml";
6
6
  import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
7
7
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
8
8
  import { loadGraphIndexFile } from "./index-reader.js";
9
+ import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
9
10
  function syncDir(cwd, graph) {
10
11
  loadGlobalGraphConfigs();
12
+ if (graph === 'framework')
13
+ return path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', graph);
11
14
  const config = GRAPH_CONFIGS[graph];
12
- if (graph === "framework" || config?.shared) {
15
+ if (config?.shared) {
13
16
  const xdgData = process.env.XDG_DATA_HOME ?? path.join(process.env.HOME ?? cwd, ".local", "share");
14
17
  return path.join(xdgData, "aiwg", "index", "fortemi-core", graph);
15
18
  }
@@ -36,6 +39,47 @@ function prebuiltDir(graph) {
36
39
  function sha256(text) {
37
40
  return createHash("sha256").update(text).digest("hex");
38
41
  }
42
+ // Status checks and the query reader often inspect the same large export.
43
+ // Keep at most two private snapshots, and never retain an object exposed to a
44
+ // caller: query results have always been fresh, mutable objects.
45
+ const exportSnapshots = new Map();
46
+ function exportSignature(file) {
47
+ const stat = fs.statSync(file, { bigint: true });
48
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
49
+ }
50
+ function readExportSnapshot(file, expectedChecksum) {
51
+ const signature = exportSignature(file);
52
+ const cached = exportSnapshots.get(file);
53
+ // The manifest checksum is part of the snapshot identity. Filesystems may
54
+ // preserve every stat field across a same-size rewrite within one timestamp
55
+ // tick, while the manifest already names the replacement's checksum.
56
+ if (cached?.signature === signature &&
57
+ cached.checksum === expectedChecksum)
58
+ return cached;
59
+ exportSnapshots.delete(file);
60
+ const text = fs.readFileSync(file, "utf-8");
61
+ const exported = JSON.parse(text);
62
+ const snapshot = { signature, exported, checksum: sha256(text) };
63
+ // A concurrent replacement cannot publish a snapshot under the wrong stamp.
64
+ if (exportSignature(file) === signature) {
65
+ exportSnapshots.set(file, snapshot);
66
+ while (exportSnapshots.size > 2)
67
+ exportSnapshots.delete(exportSnapshots.keys().next().value);
68
+ }
69
+ return snapshot;
70
+ }
71
+ /** Transfer a checked snapshot to the query reader without sharing mutable results. */
72
+ export function consumeFortemiCoreExport(file, expectedChecksum) {
73
+ try {
74
+ const snapshot = readExportSnapshot(file, expectedChecksum);
75
+ if (snapshot.checksum !== expectedChecksum)
76
+ throw new Error("export checksum does not match manifest");
77
+ return snapshot.exported;
78
+ }
79
+ finally {
80
+ exportSnapshots.delete(file);
81
+ }
82
+ }
39
83
  function readManifest(manifestPath) {
40
84
  return readManifestResult(manifestPath).manifest;
41
85
  }
@@ -59,11 +103,91 @@ function sourceIndexBuiltAt(cwd, graph) {
59
103
  const index = loadGraphIndexFile(cwd, "metadata.json", graph);
60
104
  return typeof index?.builtAt === "string" ? index.builtAt : null;
61
105
  }
106
+ /**
107
+ * Emit the v2 JSON document without stringifying a collection or a whole record.
108
+ * Strings are escaped in bounded slices too: a single large text field must not
109
+ * reintroduce V8's string limit. Preserve pretty-print bytes for stable checksums.
110
+ */
111
+ function* jsonTokens(value, maxChars, depth = 0, ancestors = new Set()) {
112
+ if (value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function") {
113
+ value = value.toJSON();
114
+ }
115
+ if (typeof value === "string") {
116
+ yield '"';
117
+ const sliceChars = Math.floor(maxChars / 6); // A control character can expand to \uXXXX.
118
+ for (let start = 0; start < value.length;) {
119
+ let end = Math.min(start + sliceChars, value.length);
120
+ // Keep surrogate pairs together, including when UTF-8 encoding each write.
121
+ if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]) && /[\uDC00-\uDFFF]/.test(value[end]))
122
+ end--;
123
+ yield JSON.stringify(value.slice(start, end)).slice(1, -1);
124
+ start = end;
125
+ }
126
+ yield '"';
127
+ }
128
+ else if (value && typeof value === "object") {
129
+ if (ancestors.has(value))
130
+ throw new TypeError("Converting circular structure to JSON");
131
+ ancestors.add(value);
132
+ const array = Array.isArray(value);
133
+ const entries = Array.isArray(value) ? value.map((item, index) => [String(index), item])
134
+ : Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
135
+ yield array ? "[" : "{";
136
+ for (let i = 0; i < entries.length; i++) {
137
+ yield (i === 0 ? "\n" : ",\n") + " ".repeat(depth + 1);
138
+ const [key, item] = entries[i];
139
+ if (!array) {
140
+ yield* jsonTokens(key, maxChars, depth + 1, ancestors);
141
+ yield ": ";
142
+ }
143
+ yield* jsonTokens(item, maxChars, depth + 1, ancestors);
144
+ }
145
+ if (entries.length)
146
+ yield "\n" + " ".repeat(depth);
147
+ yield array ? "]" : "}";
148
+ ancestors.delete(value);
149
+ }
150
+ else {
151
+ yield JSON.stringify(value) ?? "null";
152
+ }
153
+ }
154
+ /** Write and hash the same bounded chunks; publish only complete exports. */
155
+ function writeExport(file, exported, maxChars) {
156
+ const hash = createHash("sha256");
157
+ const fd = fs.openSync(file, "w");
158
+ try {
159
+ const flush = (chunk) => {
160
+ const bytes = Buffer.from(chunk, "utf-8");
161
+ hash.update(bytes);
162
+ for (let offset = 0; offset < bytes.length;)
163
+ offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
164
+ };
165
+ let pending = "";
166
+ for (const token of jsonTokens(exported, maxChars)) {
167
+ if (pending.length + token.length > maxChars) {
168
+ flush(pending);
169
+ pending = "";
170
+ }
171
+ pending += token;
172
+ }
173
+ flush(pending);
174
+ flush("\n");
175
+ return hash.digest("hex");
176
+ }
177
+ finally {
178
+ fs.closeSync(fd);
179
+ }
180
+ }
62
181
  export function syncFortemiCoreIndex(cwd, options = {}) {
63
182
  const graph = options.graph ?? "project";
64
183
  const dir = syncDir(cwd, graph);
65
184
  const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
66
185
  const manifestPath = path.join(dir, "manifest.json");
186
+ const maxChars = options.maxChunkChars ?? 64 * 1024;
187
+ if (!Number.isSafeInteger(maxChars) || maxChars < 64)
188
+ throw new Error("maxChunkChars must be an integer of at least 64");
189
+ fs.mkdirSync(dir, { recursive: true });
190
+ const temporaryPath = path.join(dir, `.export-${randomUUID()}.tmp`);
67
191
  const previous = readManifest(manifestPath);
68
192
  const sourceBuiltAt = sourceIndexBuiltAt(cwd, graph);
69
193
  const buildExport = (generatedAt) => {
@@ -74,12 +198,11 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
74
198
  generatedAt,
75
199
  schemaVersion: "v2",
76
200
  });
77
- const json = JSON.stringify(exported, null, 2) + "\n";
201
+ const checksum = writeExport(temporaryPath, exported, maxChars);
78
202
  const recordsWithConcepts = exported.items.filter((item) => (item.skos_concepts?.length ?? 0) > 0).length;
79
203
  return {
80
204
  generatedAt,
81
- json,
82
- checksum: sha256(json),
205
+ checksum,
83
206
  itemCount: exported.items.length,
84
207
  skosCoverage: {
85
208
  records_with_concepts: recordsWithConcepts,
@@ -91,36 +214,40 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
91
214
  },
92
215
  };
93
216
  };
94
- let built = buildExport(options.generatedAt ?? previous?.generated_at ?? new Date().toISOString());
95
- let status = "created";
96
- if (previous) {
97
- if (previous.export_checksum === built.checksum) {
98
- status = "unchanged";
99
- }
100
- else {
101
- status = "updated";
102
- if (options.generatedAt === undefined) {
103
- built = buildExport(new Date().toISOString());
217
+ try {
218
+ let built = buildExport(options.generatedAt ?? previous?.generated_at ?? new Date().toISOString());
219
+ let status = "created";
220
+ if (previous) {
221
+ if (previous.export_checksum === built.checksum) {
222
+ status = "unchanged";
223
+ }
224
+ else {
225
+ status = "updated";
226
+ if (options.generatedAt === undefined) {
227
+ built = buildExport(new Date().toISOString());
228
+ }
104
229
  }
105
230
  }
231
+ fs.renameSync(temporaryPath, exportPath);
232
+ const manifest = {
233
+ schema_version: "aiwg.fortemi.core-sync.v1",
234
+ backend: "fortemi-core",
235
+ graph,
236
+ generated_at: built.generatedAt,
237
+ export_path: path.relative(cwd, exportPath),
238
+ export_schema_version: "aiwg.fortemi.index.export.v2",
239
+ export_checksum: built.checksum,
240
+ item_count: built.itemCount,
241
+ skos_coverage: built.skosCoverage,
242
+ status,
243
+ source_index_built_at: sourceBuiltAt,
244
+ };
245
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
246
+ return manifest;
247
+ }
248
+ finally {
249
+ fs.rmSync(temporaryPath, { force: true });
106
250
  }
107
- fs.mkdirSync(dir, { recursive: true });
108
- fs.writeFileSync(exportPath, built.json, "utf-8");
109
- const manifest = {
110
- schema_version: "aiwg.fortemi.core-sync.v1",
111
- backend: "fortemi-core",
112
- graph,
113
- generated_at: built.generatedAt,
114
- export_path: path.relative(cwd, exportPath),
115
- export_schema_version: "aiwg.fortemi.index.export.v2",
116
- export_checksum: built.checksum,
117
- item_count: built.itemCount,
118
- skos_coverage: built.skosCoverage,
119
- status,
120
- source_index_built_at: sourceBuiltAt,
121
- };
122
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
123
- return manifest;
124
251
  }
125
252
  export function getFortemiCoreSyncStatus(cwd, graph = "project") {
126
253
  const dir = syncDir(cwd, graph);
@@ -149,9 +276,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
149
276
  let exportChecksumMatches = false;
150
277
  if (exportExists) {
151
278
  try {
152
- const exportText = fs.readFileSync(exportPath, "utf-8");
153
- const exported = JSON.parse(exportText);
154
- exportChecksumMatches = sha256(exportText) === manifest.export_checksum;
279
+ const { exported, checksum } = readExportSnapshot(exportPath, manifest.export_checksum);
280
+ exportChecksumMatches = checksum === manifest.export_checksum;
155
281
  if (exportChecksumMatches) {
156
282
  if (exported.schema_version !== manifest.export_schema_version) {
157
283
  exportReadReason = `export schema '${exported.schema_version}' does not match manifest '${manifest.export_schema_version}'`;
@@ -182,6 +308,7 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
182
308
  exportPath,
183
309
  built: exportExists,
184
310
  stale,
311
+ ...(sourceIsNewer ? { sourceIndexNewer: true } : {}),
185
312
  itemCount: manifest.item_count,
186
313
  exportChecksum: manifest.export_checksum,
187
314
  generatedAt: manifest.generated_at,
@@ -195,8 +322,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
195
322
  : null,
196
323
  };
197
324
  }
198
- export function getFortemiCorePrebuiltStatus(graph = "framework") {
199
- const dir = prebuiltDir(graph) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
325
+ export function getFortemiCorePrebuiltStatus(graph = "framework", packageRoot) {
326
+ const dir = (packageRoot ? path.join(packageRoot, "prebuilt", "fortemi-core", graph) : prebuiltDir(graph)) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
200
327
  const manifestPath = path.join(dir, "manifest.json");
201
328
  const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
202
329
  const { manifest, reason: manifestReadReason } = readManifestResult(manifestPath);
@@ -222,8 +349,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
222
349
  let reason = null;
223
350
  if (exportExists) {
224
351
  try {
225
- const exportText = fs.readFileSync(exportPath, "utf-8");
226
- const exported = JSON.parse(exportText);
352
+ const { exported, checksum } = readExportSnapshot(exportPath, prebuiltManifest.export_checksum);
227
353
  if (prebuiltManifest.schema_version !== "aiwg.fortemi.prebuilt.v1" ||
228
354
  prebuiltManifest.backend !== "fortemi-core" ||
229
355
  prebuiltManifest.graph !== graph ||
@@ -231,7 +357,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
231
357
  prebuiltManifest.export_schema_version !== "aiwg.fortemi.index.export.v2") {
232
358
  reason = "prebuilt manifest is incompatible with the requested graph";
233
359
  }
234
- else if (sha256(exportText) !== prebuiltManifest.export_checksum) {
360
+ else if (checksum !== prebuiltManifest.export_checksum) {
235
361
  reason = "prebuilt export checksum does not match manifest";
236
362
  }
237
363
  else if (exported.schema_version !== prebuiltManifest.export_schema_version) {