mlx-bun 0.5.0 → 1.0.0

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 (640) hide show
  1. package/README.md +1449 -109
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +7 -5
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +10 -10
  78. package/src/{serve → server}/chat-stage.ts +14 -14
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +33 -41
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +25 -82
  85. package/src/{serve → server}/http.ts +25 -13
  86. package/src/{serve → server}/inference-request.ts +5 -5
  87. package/src/server/job-routes.ts +20 -0
  88. package/src/server/media-prompt.ts +63 -0
  89. package/src/server/memory-completion-client.ts +46 -0
  90. package/src/server/model-routes.ts +192 -0
  91. package/src/{serve → server}/openai-wire.ts +3 -2
  92. package/src/{serve → server}/pipeline.ts +1 -1
  93. package/src/server/prompt-builder.ts +13 -0
  94. package/src/server/prompt-contracts.ts +17 -0
  95. package/src/server/proxy-routes.ts +187 -0
  96. package/src/server/publishing-routes.ts +41 -0
  97. package/src/{serve → server}/request-plan.ts +6 -8
  98. package/src/{serve → server}/request-prep.ts +47 -50
  99. package/src/{serve → server}/responses-client.ts +30 -13
  100. package/src/{responses.ts → server/responses.ts} +12 -93
  101. package/src/server/routes.ts +205 -0
  102. package/src/server/start.ts +72 -0
  103. package/src/server/status-routes.ts +108 -0
  104. package/src/{serve → server}/text-completion-stage.ts +25 -4
  105. package/src/server/text-prompt.ts +12 -0
  106. package/src/{serve → server}/token-streams.ts +36 -38
  107. package/src/server/worker-routes.ts +311 -0
  108. package/src/storage/paths.ts +61 -0
  109. package/src/web/assets.ts +37 -0
  110. package/src/web/{src → browser}/api.ts +5 -23
  111. package/src/web/browser/chat-host.ts +17 -0
  112. package/src/web/browser/installed-panels.ts +6 -0
  113. package/src/web/browser/main.ts +55 -0
  114. package/src/web/browser/markdown.ts +279 -0
  115. package/src/web/browser/memory-panel.ts +9 -0
  116. package/src/web/browser/palette.ts +64 -0
  117. package/src/web/browser/protocol.ts +13 -0
  118. package/src/web/browser/shell.ts +467 -0
  119. package/src/web/{src → browser}/status.ts +32 -36
  120. package/src/web/build.ts +51 -0
  121. package/src/web/public/app.html +1054 -0
  122. package/src/web/{sw.js → public/sw.js} +4 -3
  123. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  124. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  125. package/THIRD_PARTY_LICENSES.md +0 -51
  126. package/docs/README.md +0 -107
  127. package/docs/archive/hlg-sampling.md +0 -350
  128. package/docs/archive/investigations/audio-input-plan.md +0 -356
  129. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
  174. package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
  175. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  176. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  177. package/docs/design/batching.md +0 -2054
  178. package/docs/design/decode-speed-program.md +0 -2404
  179. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  180. package/docs/design/generic-model-support.md +0 -821
  181. package/docs/design/kv-cache.md +0 -993
  182. package/docs/design/orpo-training.md +0 -680
  183. package/docs/design/speculative-decoding.md +0 -2129
  184. package/docs/design/turboquant.md +0 -2225
  185. package/docs/design/unified-engine-frontier-plan.md +0 -1987
  186. package/docs/design/web-chat-redesign.md +0 -695
  187. package/docs/planning/PRODUCT_ROADMAP.md +0 -593
  188. package/docs/planning/ResearchTopics.md +0 -119
  189. package/docs/planning/public-readiness-handoff.md +0 -244
  190. package/docs/planning/public-readiness.md +0 -149
  191. package/docs/planning/video-series.md +0 -421
  192. package/docs/reference/benchmarks.md +0 -3587
  193. package/docs/reference/cli.md +0 -646
  194. package/docs/reference/distribution.md +0 -380
  195. package/docs/reference/environment.md +0 -494
  196. package/docs/reference/glossary.md +0 -39
  197. package/docs/reference/library-api.md +0 -688
  198. package/docs/reference/memory.md +0 -260
  199. package/docs/reference/models.md +0 -346
  200. package/docs/reference/server-api.md +0 -1945
  201. package/docs/reference/server-config.md +0 -973
  202. package/docs/reference/training.md +0 -462
  203. package/docs/reference/troubleshooting.md +0 -158
  204. package/src/admission-plan.ts +0 -18
  205. package/src/audio/audiotoolbox.ts +0 -112
  206. package/src/audio/conformer.ts +0 -655
  207. package/src/audio/decode.ts +0 -133
  208. package/src/audio/features.ts +0 -202
  209. package/src/audio/mic-capture.ts +0 -98
  210. package/src/audio/silero-vad.ts +0 -379
  211. package/src/audio/transcode.ts +0 -67
  212. package/src/audio/whisper-decode.ts +0 -774
  213. package/src/audio/whisper-fast.ts +0 -732
  214. package/src/audio/whisper-format.ts +0 -73
  215. package/src/audio/whisper-mel.ts +0 -185
  216. package/src/audio/whisper-timing.ts +0 -274
  217. package/src/audio/whisper-tokenizer.ts +0 -239
  218. package/src/audio/whisper-transcribe.ts +0 -410
  219. package/src/backends/mlx/assistant-target.ts +0 -46
  220. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  221. package/src/backends/mlx/autoregressive.ts +0 -129
  222. package/src/backends/mlx/batch-group.ts +0 -1874
  223. package/src/backends/mlx/cache-attachment.ts +0 -67
  224. package/src/backends/mlx/cache-layout.ts +0 -31
  225. package/src/backends/mlx/checkpoint-state.ts +0 -53
  226. package/src/backends/mlx/continuation-persistence.ts +0 -107
  227. package/src/backends/mlx/continuation-request.ts +0 -43
  228. package/src/backends/mlx/continuation.ts +0 -77
  229. package/src/backends/mlx/diffusion.ts +0 -39
  230. package/src/backends/mlx/draft-target.ts +0 -32
  231. package/src/backends/mlx/encoder-cache.ts +0 -25
  232. package/src/backends/mlx/evaluation.ts +0 -105
  233. package/src/backends/mlx/fill-append.ts +0 -58
  234. package/src/backends/mlx/fill-group.ts +0 -497
  235. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  236. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  237. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  238. package/src/backends/mlx/gateway-binding.ts +0 -174
  239. package/src/backends/mlx/graph.ts +0 -38
  240. package/src/backends/mlx/kv-codec.ts +0 -9
  241. package/src/backends/mlx/kv-maintenance.ts +0 -161
  242. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  243. package/src/backends/mlx/memory-client.ts +0 -189
  244. package/src/backends/mlx/methods.ts +0 -90
  245. package/src/backends/mlx/mixed-iteration.ts +0 -62
  246. package/src/backends/mlx/model-prompt.ts +0 -178
  247. package/src/backends/mlx/model-serving.ts +0 -80
  248. package/src/backends/mlx/paged-cache-codec.ts +0 -27
  249. package/src/backends/mlx/prefill-cohort.ts +0 -100
  250. package/src/backends/mlx/prefill-policy.ts +0 -26
  251. package/src/backends/mlx/prefill-rows.ts +0 -244
  252. package/src/backends/mlx/prefill.ts +0 -33
  253. package/src/backends/mlx/projected-context-rows.ts +0 -72
  254. package/src/backends/mlx/prompt-input.ts +0 -29
  255. package/src/backends/mlx/qwen-prompt-input.ts +0 -40
  256. package/src/backends/mlx/request-policy.ts +0 -20
  257. package/src/backends/mlx/request-state-policy.ts +0 -54
  258. package/src/backends/mlx/rollback.ts +0 -92
  259. package/src/backends/mlx/row-sampling.ts +0 -49
  260. package/src/backends/mlx/serial-executor.ts +0 -251
  261. package/src/backends/mlx/speculative-group.ts +0 -438
  262. package/src/backends/mlx/speculative-round.ts +0 -161
  263. package/src/backends/mlx/speculative.ts +0 -131
  264. package/src/backends/mlx/state-rows.ts +0 -104
  265. package/src/backends/mlx/state-views.ts +0 -46
  266. package/src/backends/mlx/target-layout-capability.ts +0 -35
  267. package/src/backends/mlx/text-engine.ts +0 -73
  268. package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
  269. package/src/chat-template.ts +0 -382
  270. package/src/cli.ts +0 -3490
  271. package/src/config.ts +0 -399
  272. package/src/contracts/completion.ts +0 -16
  273. package/src/contracts/execution.ts +0 -70
  274. package/src/contracts/generation.ts +0 -103
  275. package/src/contracts/host.ts +0 -7
  276. package/src/contracts/object-cache.ts +0 -8
  277. package/src/contracts/pi-provider.ts +0 -30
  278. package/src/contracts/pi.ts +0 -261
  279. package/src/contracts/prefix-cache.ts +0 -33
  280. package/src/contracts/resources.ts +0 -25
  281. package/src/contracts/sampling.ts +0 -23
  282. package/src/contracts/scheduling.ts +0 -54
  283. package/src/contracts/tsconfig.json +0 -14
  284. package/src/dataset/generators.ts +0 -1087
  285. package/src/dataset/index.ts +0 -12
  286. package/src/dataset/job.ts +0 -81
  287. package/src/dataset/llm.ts +0 -71
  288. package/src/dataset/registry.ts +0 -528
  289. package/src/diffusion/diffusion-generate.ts +0 -507
  290. package/src/download.ts +0 -505
  291. package/src/embed.ts +0 -77
  292. package/src/engine/admission.ts +0 -59
  293. package/src/engine/cancellation.ts +0 -38
  294. package/src/engine/engine.ts +0 -56
  295. package/src/engine/execution-coordinator.ts +0 -70
  296. package/src/engine/execution-plan.ts +0 -64
  297. package/src/engine/execution-tasks.ts +0 -44
  298. package/src/engine/namespaced-cache.ts +0 -13
  299. package/src/engine/resources.ts +0 -53
  300. package/src/engine/scheduler.ts +0 -92
  301. package/src/engine/session.ts +0 -247
  302. package/src/eval/README.md +0 -130
  303. package/src/eval/ifeval.ts +0 -81
  304. package/src/eval/kl-prompts.ts +0 -29
  305. package/src/eval/kl.ts +0 -590
  306. package/src/eval/perplexity.ts +0 -164
  307. package/src/eval/quality-db.ts +0 -92
  308. package/src/eval/reference-logits.ts +0 -207
  309. package/src/eval/runner.ts +0 -124
  310. package/src/eval/score.ts +0 -49
  311. package/src/eval/tasks/bfcl.ts +0 -535
  312. package/src/eval/tasks/gsm8k.ts +0 -98
  313. package/src/eval/tasks/hashhop.ts +0 -139
  314. package/src/eval/tasks/humaneval.ts +0 -275
  315. package/src/eval/tasks/ifeval.ts +0 -525
  316. package/src/eval/tasks/mmlu.ts +0 -157
  317. package/src/eval/ultrafeedback.ts +0 -60
  318. package/src/evaldb.ts +0 -199
  319. package/src/expert-io.ts +0 -393
  320. package/src/expert-offload-build.ts +0 -77
  321. package/src/expert-offload.ts +0 -76
  322. package/src/expert-residency.ts +0 -1134
  323. package/src/expert-usage.ts +0 -415
  324. package/src/fill/echo-index.ts +0 -188
  325. package/src/fill/fill-session.ts +0 -463
  326. package/src/fill/proposal.ts +0 -68
  327. package/src/fill/schema-rows.ts +0 -506
  328. package/src/fill/tool-boundary.ts +0 -103
  329. package/src/fit.ts +0 -249
  330. package/src/generate.ts +0 -1352
  331. package/src/grammar.ts +0 -604
  332. package/src/harness-pi.ts +0 -184
  333. package/src/hf-push.ts +0 -628
  334. package/src/hub-rest.ts +0 -242
  335. package/src/index.ts +0 -63
  336. package/src/inference/denoising.ts +0 -17
  337. package/src/inference/draft-acceptance.ts +0 -41
  338. package/src/inference/draft-output.ts +0 -49
  339. package/src/inference/graph.ts +0 -33
  340. package/src/inference/prefill.ts +0 -45
  341. package/src/inference/rollback.ts +0 -7
  342. package/src/jobs/index.ts +0 -31
  343. package/src/jobs/job-entry.ts +0 -107
  344. package/src/jobs/task-client.ts +0 -19
  345. package/src/jobs/types.ts +0 -2
  346. package/src/kv-scheme.ts +0 -279
  347. package/src/kv-store.ts +0 -1135
  348. package/src/lab/README.md +0 -22
  349. package/src/lab/curve/curve-designer.html +0 -303
  350. package/src/lab/curve/curve-sampler.ts +0 -122
  351. package/src/lab/expert-trace/expert-trace.ts +0 -69
  352. package/src/lab/paged-kv/paged-attention.ts +0 -117
  353. package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
  354. package/src/lab/paged-kv/paged-kv.ts +0 -336
  355. package/src/library.ts +0 -40
  356. package/src/lora.ts +0 -408
  357. package/src/md-modules.d.ts +0 -54
  358. package/src/media-fetch.ts +0 -442
  359. package/src/memory/article.ts +0 -558
  360. package/src/memory/chunk-validation.ts +0 -150
  361. package/src/memory/chunk.ts +0 -318
  362. package/src/memory/cluster.ts +0 -351
  363. package/src/memory/coverage.ts +0 -134
  364. package/src/memory/crosslink.ts +0 -0
  365. package/src/memory/db.ts +0 -253
  366. package/src/memory/entity.ts +0 -203
  367. package/src/memory/gate.ts +0 -187
  368. package/src/memory/history.ts +0 -152
  369. package/src/memory/ingest.ts +0 -335
  370. package/src/memory/model.ts +0 -183
  371. package/src/memory/normalize.ts +0 -671
  372. package/src/memory/parse.ts +0 -22
  373. package/src/memory/pipeline.ts +0 -275
  374. package/src/memory/prompts.ts +0 -65
  375. package/src/memory/query.ts +0 -334
  376. package/src/memory/reconcile.ts +0 -734
  377. package/src/memory/redirect.ts +0 -108
  378. package/src/memory/reindex.ts +0 -325
  379. package/src/memory/resolve.ts +0 -307
  380. package/src/memory/rest.ts +0 -245
  381. package/src/memory/route.ts +0 -272
  382. package/src/memory/schedule.ts +0 -165
  383. package/src/memory/stages.ts +0 -777
  384. package/src/memory/synthesize.ts +0 -1638
  385. package/src/memory/tools.ts +0 -749
  386. package/src/memory/vault.ts +0 -1194
  387. package/src/memory/wikify.ts +0 -1064
  388. package/src/mlx/array.ts +0 -425
  389. package/src/mlx/autograd.ts +0 -304
  390. package/src/mlx/checkpoint.ts +0 -149
  391. package/src/mlx/compile.ts +0 -168
  392. package/src/mlx/custom-vjp.ts +0 -188
  393. package/src/mlx/ffi.ts +0 -531
  394. package/src/mlx/materialize.ts +0 -23
  395. package/src/mlx/metal-kernel.ts +0 -159
  396. package/src/mlx/normalized-argmax.ts +0 -116
  397. package/src/mlx/ops.ts +0 -1108
  398. package/src/mlx/quantized-rows.ts +0 -36
  399. package/src/mlx/token-bitmask.ts +0 -52
  400. package/src/mlx/turboquant-kv-decode.ts +0 -82
  401. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  402. package/src/mlx/turboquant-ops.ts +0 -861
  403. package/src/mlx/turboquant-tables.ts +0 -237
  404. package/src/mmap.ts +0 -90
  405. package/src/model/artifact-identity.ts +0 -73
  406. package/src/model/batched-kv.ts +0 -235
  407. package/src/model/batched-mask.ts +0 -279
  408. package/src/model/batched-quant.ts +0 -212
  409. package/src/model/batched-quantized-kv.ts +0 -144
  410. package/src/model/batched-rotating-quant.ts +0 -353
  411. package/src/model/batched-rotating-state.ts +0 -149
  412. package/src/model/batched-rotating.ts +0 -338
  413. package/src/model/batched-row-storage.ts +0 -179
  414. package/src/model/batched-ssm.ts +0 -52
  415. package/src/model/batched-turboquant-kv.ts +0 -94
  416. package/src/model/compiled-decode.ts +0 -712
  417. package/src/model/decoded-kv-donor.ts +0 -12
  418. package/src/model/delayed-quantized-kv.ts +0 -72
  419. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  420. package/src/model/delayed-turboquant-kv.ts +0 -57
  421. package/src/model/diffusion-gemma.ts +0 -1026
  422. package/src/model/factory.ts +0 -215
  423. package/src/model/fingerprint.ts +0 -45
  424. package/src/model/flash-attention.ts +0 -694
  425. package/src/model/full-prefill-padding.ts +0 -56
  426. package/src/model/full-prefill-row.ts +0 -105
  427. package/src/model/gemma4-base.ts +0 -2370
  428. package/src/model/gemma4.ts +0 -1395
  429. package/src/model/generated/gemma4-12b.ts +0 -429
  430. package/src/model/generated/gemma4-26b.ts +0 -448
  431. package/src/model/generated/gemma4-e4b.ts +0 -717
  432. package/src/model/generated/index.ts +0 -18
  433. package/src/model/glm52-atlas.ts +0 -511
  434. package/src/model/glm52-cache.ts +0 -792
  435. package/src/model/glm52-config.ts +0 -262
  436. package/src/model/glm52-container.ts +0 -305
  437. package/src/model/glm52-coupling.ts +0 -388
  438. package/src/model/glm52-dsa.ts +0 -606
  439. package/src/model/glm52-expert-layout.ts +0 -253
  440. package/src/model/glm52-layout.ts +0 -152
  441. package/src/model/glm52-memory.ts +0 -500
  442. package/src/model/glm52-mla.ts +0 -792
  443. package/src/model/glm52-moe.ts +0 -223
  444. package/src/model/glm52-pilot.ts +0 -537
  445. package/src/model/glm52-reference.ts +0 -445
  446. package/src/model/glm52-residency.ts +0 -557
  447. package/src/model/glm52-resident-weights.ts +0 -135
  448. package/src/model/glm52-streamed-experts.ts +0 -453
  449. package/src/model/glm52-streamed-metal.ts +0 -610
  450. package/src/model/glm52-weights.ts +0 -503
  451. package/src/model/glm52.ts +0 -1132
  452. package/src/model/implementation.ts +0 -69
  453. package/src/model/kv-attention-view.ts +0 -70
  454. package/src/model/minicpm5.ts +0 -376
  455. package/src/model/profile.ts +0 -468
  456. package/src/model/quantized-append-attention.ts +0 -31
  457. package/src/model/qwen-append.ts +0 -12
  458. package/src/model/qwen-conv.ts +0 -52
  459. package/src/model/qwen3-delta-state.ts +0 -74
  460. package/src/model/qwen3-delta.ts +0 -451
  461. package/src/model/qwen3-moe.ts +0 -404
  462. package/src/model/qwen3-mrope.ts +0 -250
  463. package/src/model/qwen3.ts +0 -277
  464. package/src/model/qwen3_5-checkpoint.ts +0 -164
  465. package/src/model/qwen3_5.ts +0 -881
  466. package/src/model/rotating-kv-donor.ts +0 -35
  467. package/src/model/rotating-kv-layout.ts +0 -262
  468. package/src/model/rotating-row-transaction.ts +0 -35
  469. package/src/model/speculative-rotating-kv.ts +0 -101
  470. package/src/model/ssm-prefill-padding.ts +0 -39
  471. package/src/model/support.ts +0 -110
  472. package/src/model/token-groups.ts +0 -66
  473. package/src/model/trellis-balanced-scatter.ts +0 -51
  474. package/src/model/trellis-linear.ts +0 -672
  475. package/src/model/trellis-shared-m.ts +0 -151
  476. package/src/model/trellis-shared-scatter.ts +0 -144
  477. package/src/model/trellis-splitk-prefill.ts +0 -158
  478. package/src/model/trellis-tiled-prefill.ts +0 -136
  479. package/src/model/trellis-vector-expand.ts +0 -62
  480. package/src/model/trellis-wide-prefill.ts +0 -94
  481. package/src/model/turboquant-codec.ts +0 -89
  482. package/src/model/universal/archs.ts +0 -332
  483. package/src/model/universal/dense.ts +0 -612
  484. package/src/model/universal/modules.ts +0 -170
  485. package/src/model/universal/rope.ts +0 -381
  486. package/src/model/whisper.ts +0 -435
  487. package/src/native/expert_io.c +0 -523
  488. package/src/native/frame_extract.swift +0 -140
  489. package/src/native/mic_capture.swift +0 -92
  490. package/src/native-pack.ts +0 -163
  491. package/src/pi-provider.ts +0 -53
  492. package/src/pi-session.ts +0 -81
  493. package/src/pi-terminal.ts +0 -274
  494. package/src/pi-web.ts +0 -1940
  495. package/src/preflight.ts +0 -158
  496. package/src/prompt-cache.ts +0 -682
  497. package/src/quantize/allocator.ts +0 -627
  498. package/src/quantize/atomic-output.ts +0 -38
  499. package/src/quantize/calibration.ts +0 -181
  500. package/src/quantize/config-writer.ts +0 -145
  501. package/src/quantize/index.ts +0 -84
  502. package/src/quantize/job.ts +0 -162
  503. package/src/quantize/quantizer.ts +0 -666
  504. package/src/quantize/rotate.ts +0 -603
  505. package/src/quantize/safetensors-writer.ts +0 -249
  506. package/src/quantize/sensitivity.ts +0 -216
  507. package/src/quantize/trellis.ts +0 -586
  508. package/src/quantize/weight-transform.ts +0 -292
  509. package/src/registry.ts +0 -602
  510. package/src/runtime-config.ts +0 -77
  511. package/src/safetensors.ts +0 -165
  512. package/src/sampler.ts +0 -1012
  513. package/src/serve/admin-routes.ts +0 -184
  514. package/src/serve/assets/curve-terrain.html +0 -105
  515. package/src/serve/assets/training-inference-map.html +0 -575
  516. package/src/serve/audio-routes.ts +0 -323
  517. package/src/serve/aux-routes.ts +0 -132
  518. package/src/serve/batch-scheduler.ts +0 -8
  519. package/src/serve/checkpoint-identity.ts +0 -64
  520. package/src/serve/generation-gateway.ts +0 -537
  521. package/src/serve/isolate.ts +0 -609
  522. package/src/serve/kv-budget.ts +0 -14
  523. package/src/serve/lab-routes.ts +0 -341
  524. package/src/serve/lane-registry.ts +0 -45
  525. package/src/serve/model-admin-routes.ts +0 -155
  526. package/src/serve/model-binding.ts +0 -49
  527. package/src/serve/model-host.ts +0 -511
  528. package/src/serve/parent-application.ts +0 -56
  529. package/src/serve/prompt-response-trace.ts +0 -161
  530. package/src/serve/response-lease.ts +0 -24
  531. package/src/serve/session-search.ts +0 -326
  532. package/src/serve/static-routes.ts +0 -101
  533. package/src/serve/transcription-server.ts +0 -64
  534. package/src/serve/transcription-service.ts +0 -502
  535. package/src/server.ts +0 -1282
  536. package/src/spec/assistant-rows.ts +0 -93
  537. package/src/spec/assistant-source.ts +0 -126
  538. package/src/spec/cache-identity.ts +0 -22
  539. package/src/spec/deepspec-rows.ts +0 -29
  540. package/src/spec/deepspec-source.ts +0 -145
  541. package/src/spec/dflash-source.ts +0 -149
  542. package/src/spec/drafter.ts +0 -304
  543. package/src/spec/dspark/ab-stats.ts +0 -136
  544. package/src/spec/dspark/calibration.ts +0 -112
  545. package/src/spec/dspark/data-dflash.ts +0 -156
  546. package/src/spec/dspark/data.ts +0 -201
  547. package/src/spec/dspark/deepspec-module.ts +0 -776
  548. package/src/spec/dspark/generate-dflash.ts +0 -245
  549. package/src/spec/dspark/generate.ts +0 -290
  550. package/src/spec/dspark/loader.ts +0 -23
  551. package/src/spec/dspark/loss.ts +0 -213
  552. package/src/spec/dspark/module-dflash.ts +0 -560
  553. package/src/spec/dspark/module.ts +0 -540
  554. package/src/spec/dspark/quantize-drafter.ts +0 -91
  555. package/src/spec/dspark/sample.ts +0 -112
  556. package/src/spec/generate.ts +0 -276
  557. package/src/spec/glm52-mtp-graph.ts +0 -29
  558. package/src/spec/glm52-mtp-rows.ts +0 -132
  559. package/src/spec/glm52-mtp-source.ts +0 -318
  560. package/src/spec/glm52-mtp-state.ts +0 -33
  561. package/src/spec/ngram-source.ts +0 -226
  562. package/src/spec/projected-draft-rows.ts +0 -73
  563. package/src/spec/qwen-mtp-module.ts +0 -171
  564. package/src/spec/qwen-mtp-rows.ts +0 -230
  565. package/src/spec/qwen-mtp-source.ts +0 -386
  566. package/src/spec/qwen-mtp-state.ts +0 -30
  567. package/src/spec/serve-loop.ts +0 -587
  568. package/src/spec/source.ts +0 -228
  569. package/src/spec/two-model-rows.ts +0 -62
  570. package/src/spec/two-model.ts +0 -113
  571. package/src/ssd-cache.ts +0 -431
  572. package/src/ssd-durability.ts +0 -209
  573. package/src/storage/host-buffer.ts +0 -24
  574. package/src/storage/kv-writer.ts +0 -83
  575. package/src/storage/kv-writer.worker.js +0 -264
  576. package/src/storage/retention-policy.ts +0 -37
  577. package/src/tiered-prompt-cache.ts +0 -45
  578. package/src/tokenizer.ts +0 -335
  579. package/src/tool-approvals.ts +0 -131
  580. package/src/tool-call.ts +0 -695
  581. package/src/train/dataset.ts +0 -484
  582. package/src/train/diffusion-lora.ts +0 -216
  583. package/src/train/export.ts +0 -32
  584. package/src/train/flash-cce.ts +0 -1234
  585. package/src/train/forward.ts +0 -266
  586. package/src/train/fuse.ts +0 -243
  587. package/src/train/index.ts +0 -46
  588. package/src/train/job.ts +0 -205
  589. package/src/train/lora-params.ts +0 -254
  590. package/src/train/loss.ts +0 -1591
  591. package/src/train/merge.ts +0 -195
  592. package/src/train/optimizer.ts +0 -183
  593. package/src/train/prefix-shared.ts +0 -490
  594. package/src/train/rank.ts +0 -137
  595. package/src/train/segmented.ts +0 -1828
  596. package/src/train/steel-qmm-header.ts +0 -1461
  597. package/src/train/trainer.ts +0 -1221
  598. package/src/train/watch.ts +0 -542
  599. package/src/vision/diffusion-vision.ts +0 -620
  600. package/src/vision/embedder.ts +0 -176
  601. package/src/vision/preprocess.ts +0 -247
  602. package/src/vision/prompt.ts +0 -409
  603. package/src/vision/qwen3vl-preprocess.ts +0 -409
  604. package/src/vision/qwen3vl-prompt.ts +0 -233
  605. package/src/vision/qwen3vl-tower.ts +0 -566
  606. package/src/vision/siglip.ts +0 -607
  607. package/src/vision/video-frames.ts +0 -148
  608. package/src/web/app.html +0 -2155
  609. package/src/web/app.js +0 -5863
  610. package/src/web/skills/memory/SKILL.md +0 -120
  611. package/src/web/skills/web-research/SKILL.md +0 -46
  612. package/src/web/skills.ts +0 -97
  613. package/src/web/src/adapters-panel.ts +0 -288
  614. package/src/web/src/assistant.ts +0 -451
  615. package/src/web/src/chat.ts +0 -1060
  616. package/src/web/src/composer.ts +0 -962
  617. package/src/web/src/dataset.ts +0 -132
  618. package/src/web/src/finetune.ts +0 -267
  619. package/src/web/src/hub.ts +0 -428
  620. package/src/web/src/main.ts +0 -98
  621. package/src/web/src/markdown.ts +0 -0
  622. package/src/web/src/memory-panel.ts +0 -572
  623. package/src/web/src/model-picker.ts +0 -159
  624. package/src/web/src/palette.ts +0 -346
  625. package/src/web/src/protocol.ts +0 -26
  626. package/src/web/src/quantize.ts +0 -167
  627. package/src/web/src/rag.ts +0 -274
  628. package/src/web/src/sessions.ts +0 -469
  629. package/src/web/src/shell.ts +0 -804
  630. package/src/web/src/ui-catalog.ts +0 -78
  631. package/src/web/src/voice.ts +0 -141
  632. package/src/web/vendor/README.md +0 -61
  633. package/src/web-assets.ts +0 -47
  634. package/src/web-tools.ts +0 -478
  635. package/src/weights.ts +0 -255
  636. /package/src/{serve → server}/completion-sink.ts +0 -0
  637. /package/src/{serve → server}/generated-token-history.ts +0 -0
  638. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  639. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  640. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,1064 +0,0 @@
1
- // mlx-bun memory — the LLM WIKIFY structural pass (P8-T1 / P8-T2).
2
- //
3
- // The periodic editorial sweep, distinct from the deterministic NORMALIZE pass
4
- // (`normalize.ts`). Where NORMALIZE enforces invariants with zero judgment,
5
- // WIKIFY asks the local model for JUDGMENT — and the deterministic gate disposes
6
- // of weak judgment (LoRAs propose, gates dispose). It runs PERIODICALLY (the
7
- // nightly sweep), NOT per chunk, after synthesis has folded the day's notes in.
8
- //
9
- // Two nodes, both behind the conservative gate (`gateEdit`) + NORMALIZE:
10
- // P8-T1 · per-section IMPROVE — port of lucien `scripts/wikify.ts`
11
- // buildEditorialPrompt, applied PER SECTION via callLocal("editor"):
12
- // "tighten this section, keep every citation and every specific
13
- // detail, do not generalize away particulars." A weak edit is a NO-OP
14
- // — the section keeps its original bytes.
15
- // P8-T2 · infobox EXTRACT/REFRESH — pull the kind's structured key-value facts
16
- // out of the article (entity-valued fields → [[wikilinks]], declared
17
- // aliases: feed the entity index on reindex). Creates infoboxes where
18
- // missing and refreshes them where present, never losing a grounded
19
- // fact; the single-info-block invariant is asserted.
20
- //
21
- // Every prompt here is SCHEMATIC — no copyable concrete values — because a small
22
- // base model parrots example values verbatim (the policy-example parroting bug).
23
- // Meta/reasoning leakage is rejected by the shared `isLeakyDraft` heuristic.
24
-
25
- import {
26
- articleStructure,
27
- infoboxAliases,
28
- isFenceLine,
29
- fenceLabel,
30
- parseInfobox,
31
- validateArticleStructure,
32
- type EntityKind,
33
- } from "./article";
34
- import { checkFootnoteIntegrity, gateEdit, type GateVerdict } from "./gate";
35
- import { callLocal } from "./model";
36
- import { articleThinness, hasStubMarker, normalizeArticle } from "./normalize";
37
- import {
38
- assembleArticle,
39
- buildInfoboxFields,
40
- countFencedInfoBlocks,
41
- entityStem,
42
- isLeakyDraft,
43
- replaceSection,
44
- sanitizeSection,
45
- type SynthesisCall,
46
- } from "./synthesize";
47
- import { articlesDir, commitVault, extractSection, listArticles, slugifyHeading, vaultRoot } from "./vault";
48
- import { mkdir, readFile, writeFile } from "node:fs/promises";
49
- import { join } from "node:path";
50
-
51
- // ===========================================================================
52
- // P8-T1 — per-section IMPROVE (the editorial tighten node)
53
- // ===========================================================================
54
-
55
- /** The per-section editorial prompt — a section-scoped port of lucien
56
- * `scripts/wikify.ts` EDITORIAL_PROMPT. Purely SCHEMATIC: it names no concrete
57
- * value the model could copy. The hard invariant (keep every [^N] and every
58
- * particular) is what the gate then enforces deterministically. */
59
- const SECTION_EDITORIAL_PROMPT = `You are an expert Wikipedia editor tightening ONE section of a personal-wiki article. You are NOT given new source material — your only job is to restructure and condense the section below into its leanest, clearest form.
60
-
61
- TIGHTEN:
62
- - Remove redundancy: wherever the same point, fact, or example is stated more than once, merge it into ONE clean sentence and delete the echoes. State each thing exactly once, in the right place.
63
- - Improve flow: order the sentences so the section reads cleanly; fix run-ons and filler.
64
-
65
- HARD INVARIANT — preserve every particular (non-negotiable):
66
- - Keep EVERY citation marker [^N] exactly — never drop, renumber, or invent one.
67
- - Keep EVERY specific detail: names, model numbers, measurements, prices, dates, and stated opinions. Do NOT generalize a particular into a vague summary; do NOT drop a claim.
68
- - Do NOT add any claim the section did not already make.
69
-
70
- OUTPUT:
71
- Output ONLY the tightened section prose. Do NOT repeat the section heading, do NOT write any other section, do NOT write a "## References" section or any [^N]: definition line. No preamble, no explanation, no code fences.
72
-
73
- SECTION TO TIGHTEN:
74
- {{SECTION}}
75
- `;
76
-
77
- /** Build the per-section editorial prompt for one section's body prose. */
78
- export function buildSectionEditorialPrompt(sectionBody: string): string {
79
- return SECTION_EDITORIAL_PROMPT.replace("{{SECTION}}", sectionBody);
80
- }
81
-
82
- /** Distinct `[^N]` marker numbers in a section body (definition lines excluded —
83
- * a section body never carries defs; those live in `## References`). */
84
- function bodyMarkers(text: string): Set<number> {
85
- const out = new Set<number>();
86
- for (const line of text.split("\n")) {
87
- if (/^\s*\[\^\d+\]:/.test(line)) continue;
88
- for (const m of line.matchAll(/\[\^(\d+)\]/g)) out.add(parseInt(m[1]!, 10));
89
- }
90
- return out;
91
- }
92
-
93
- /** The body-section titles of an article (in document order), excluding the
94
- * lead, See also, Notes, and References — only the sections an editorial tighten
95
- * should touch. */
96
- export function improvableSectionTitles(content: string): string[] {
97
- return articleStructure(content)
98
- .filter((it) => it.kind === "section" && it.title)
99
- .map((it) => it.title!);
100
- }
101
-
102
- export interface SectionImproveResult {
103
- /** The article with every accepted section swapped in (raw — caller NORMALIZEs). */
104
- content: string;
105
- /** Sections whose tightened body passed the gate and was swapped in. */
106
- improved: string[];
107
- /** Sections whose tightened body was a weak edit (leak / dropped a citation /
108
- * below the word floor / gate veto) — the original section was kept. */
109
- rejected: string[];
110
- }
111
-
112
- /**
113
- * P8-T1 driver. Tighten each body section in turn via `call("editor", …)`,
114
- * accepting the edit ONLY when it survives the gate, otherwise keeping the
115
- * section's original bytes (a weak edit is a NO-OP). The gate is applied at the
116
- * whole-article level after swapping the candidate section in, so the footnote
117
- * bijection (a dropped [^N] orphans its `## References` def) and citation
118
- * preservation are checked the same way the write pipeline checks every edit;
119
- * a per-section word floor + leak check pre-reject the obvious failures. Pure
120
- * with respect to disk — the caller persists.
121
- */
122
- export async function improveSections(
123
- content: string,
124
- opts: { call: SynthesisCall; floor?: number; maxTokens?: number },
125
- ): Promise<SectionImproveResult> {
126
- const floor = opts.floor ?? 0.7;
127
- const maxTokens = opts.maxTokens ?? 512;
128
- let working = content;
129
- const improved: string[] = [];
130
- const rejected: string[] = [];
131
-
132
- for (const title of improvableSectionTitles(working)) {
133
- const anchor = slugifyHeading(title);
134
- const block = extractSection(working, anchor);
135
- if (block == null) continue;
136
- const lines = block.split("\n");
137
- const headingLine = lines[0] ?? `## ${title}`;
138
- const beforeBody = lines.slice(1).join("\n").trim();
139
- if (!beforeBody) continue; // empty section — nothing to tighten
140
-
141
- const raw = await opts.call(buildSectionEditorialPrompt(beforeBody), { maxTokens });
142
- const candidate = sanitizeSection(raw, title);
143
-
144
- // Pre-reject the obvious failures before the swap: a meta/reasoning leak, or
145
- // a dropped citation marker (also caught by the whole-article footnote
146
- // bijection below, but rejected here with a crisp per-section reason). A
147
- // per-section word floor is deliberately NOT applied — legitimately removing
148
- // a duplicated sentence can halve a short section's words; the bulk-deletion
149
- // guard is the whole-article gate's floor, which sees all the structural text.
150
- if (!candidate.trim() || isLeakyDraft(candidate)) {
151
- rejected.push(title);
152
- continue;
153
- }
154
- const beforeMarkers = bodyMarkers(beforeBody);
155
- const afterMarkers = bodyMarkers(candidate);
156
- const lostCitation = [...beforeMarkers].some((n) => !afterMarkers.has(n));
157
- if (lostCitation) {
158
- rejected.push(title);
159
- continue;
160
- }
161
-
162
- // Whole-article gate: swap the candidate in and verify the edit preserved
163
- // the article (footnote bijection, citations, structure). Weak → keep original.
164
- const trial = replaceSection(working, anchor, `${headingLine}\n\n${candidate}\n`);
165
- if (trial == null) continue;
166
- const verdict: GateVerdict = gateEdit(working, trial, { floor });
167
- if (verdict.ok) {
168
- working = trial;
169
- improved.push(title);
170
- } else {
171
- rejected.push(title);
172
- }
173
- }
174
-
175
- return { content: working, improved, rejected };
176
- }
177
-
178
- // ===========================================================================
179
- // P8-T2 — infobox EXTRACT / REFRESH
180
- // ===========================================================================
181
-
182
- /** The infobox extraction prompt — SCHEMATIC (no copyable spec values). The
183
- * model proposes `key: value` facts; `buildInfoboxFields` then applies the
184
- * grounded-field rule (physical-spec keys only on a `kind:thing`), drops any
185
- * model-supplied kind/aliases (we own those), and orders the block. */
186
- function buildInfoboxRefreshPrompt(title: string, body: string, otherArticles: string): string {
187
- return (
188
- `Use ONLY content from the article below; never copy any name or value from these instructions.\n` +
189
- `You are extracting the INFOBOX facts for the personal-wiki article about "${title}". ` +
190
- `Read the article below and emit its structured key-value facts. Output ONLY \`key: value\` lines, one per line, no fence, no prose. Rules:\n` +
191
- `- snake_case keys.\n` +
192
- `- A value that names another entity is a [[wikilink]] to one of the stems in OTHER ARTICLES (a related entity, component, or standard).\n` +
193
- `- Emit a world-fact (a spec-sheet/citable property) only if the article states it; emit a relationship-fact (owned / acquired / used_for / chosen_over / opinion) the article establishes.\n` +
194
- `- Include \`type:\` (the emergent label) when the article makes it clear.\n` +
195
- `- Do NOT emit \`kind:\` or \`aliases:\` — those are added for you.\n` +
196
- `- Never guess a spec the article does not state.\n\n` +
197
- `OTHER ARTICLES (link targets):\n${otherArticles}\n\n` +
198
- `ARTICLE:\n${body}\n`
199
- );
200
- }
201
-
202
- /** The article text the extractor reads — capped so the prefill stays bounded. */
203
- function infoboxSourceText(content: string, cap = 4000): string {
204
- return content.slice(0, cap);
205
- }
206
-
207
- /** A stem list block for the extractor so its [[wikilinks]] resolve. */
208
- function otherArticlesBlock(stems: Iterable<string>, selfStem: string, cap = 200): string {
209
- const list = [...stems].filter((s) => s !== selfStem).sort((a, b) => a.localeCompare(b)).slice(0, cap);
210
- return list.length ? list.map((s) => `- ${s}`).join("\n") : "(none yet)";
211
- }
212
-
213
- /**
214
- * Merge the model's freshly-extracted facts with the article's existing infobox
215
- * into the final ordered field list. EXISTING facts are placed first so
216
- * `buildInfoboxFields`' first-occurrence-wins keeps a grounded [[wikilink]] (a
217
- * `<entity_key>: [[Linked Entity]]` already in the vault) sticky — a refresh adds
218
- * new keys and the declared aliases, it never degrades a linked fact to a bare scalar or drops
219
- * one. With no existing infobox this is a pure CREATE (only the model's facts).
220
- */
221
- export function refreshInfoboxFields(
222
- content: string,
223
- kind: EntityKind,
224
- modelRaw: string,
225
- declaredAliases: string[] = [],
226
- ): string[] {
227
- const box = parseInfobox(content);
228
- const existingLines: string[] = [];
229
- if (box) {
230
- for (const f of box.fields) {
231
- if (f.key === "kind" || f.key === "aliases") continue;
232
- existingLines.push(`${f.key}: ${f.value}`);
233
- }
234
- }
235
- const existingAliases = box ? infoboxAliases(box) : [];
236
- const aliases = [...existingAliases, ...declaredAliases];
237
- const combinedRaw = `${existingLines.join("\n")}\n${modelRaw}`;
238
- return buildInfoboxFields(combinedRaw, kind, aliases);
239
- }
240
-
241
- /** The line range `[start, end]` (inclusive) of the first fenced ```info block,
242
- * or null when there is none. */
243
- function findInfoBlockRange(lines: string[]): { start: number; end: number } | null {
244
- for (let i = 0; i < lines.length; i++) {
245
- if (isFenceLine(lines[i]!) && fenceLabel(lines[i]!) === "info") {
246
- for (let j = i + 1; j < lines.length; j++) {
247
- if (isFenceLine(lines[j]!)) return { start: i, end: j };
248
- }
249
- return null; // unterminated — not a usable block
250
- }
251
- }
252
- return null;
253
- }
254
-
255
- const HEADING_RE = /^(#{1,6})\s+/;
256
- const SERIES_RE = /^\*Part of a series on \[\[/;
257
-
258
- /** The index after which a freshly-created infobox should be inserted: after the
259
- * H1, or after a series banner when one follows the H1 (the grammar order is
260
- * H1 → series → infobox → lead). Returns -1 when there is no H1 (prepend). */
261
- function infoboxInsertAfter(lines: string[]): number {
262
- let inFence = false;
263
- let h1 = -1;
264
- let banner = -1;
265
- for (let i = 0; i < lines.length; i++) {
266
- if (isFenceLine(lines[i]!)) {
267
- inFence = !inFence;
268
- continue;
269
- }
270
- if (inFence) continue;
271
- const t = lines[i]!.trim();
272
- if (h1 < 0 && HEADING_RE.test(lines[i]!) && lines[i]!.startsWith("# ")) h1 = i;
273
- else if (h1 >= 0 && banner < 0 && SERIES_RE.test(t)) banner = i;
274
- }
275
- return Math.max(h1, banner);
276
- }
277
-
278
- /**
279
- * Write `fields` into the article as the ONE fenced ```info block: replace an
280
- * existing block in place, else insert a fresh block after the H1 / series
281
- * banner. Returns the new content. Pure.
282
- */
283
- export function setInfobox(content: string, fields: string[]): string {
284
- const blockLines = ["```info", ...fields, "```"];
285
- const lines = content.split(/\r?\n/);
286
- const range = findInfoBlockRange(lines);
287
- if (range) {
288
- const next = [...lines.slice(0, range.start), ...blockLines, ...lines.slice(range.end + 1)];
289
- return next.join("\n");
290
- }
291
- const after = infoboxInsertAfter(lines);
292
- if (after < 0) return [...blockLines, "", ...lines].join("\n");
293
- const next = [...lines.slice(0, after + 1), "", ...blockLines, ...lines.slice(after + 1)];
294
- return next.join("\n");
295
- }
296
-
297
- export interface InfoboxRefreshResult {
298
- /** The article with the refreshed single ```info block (raw — caller NORMALIZEs). */
299
- content: string;
300
- /** True when the on-disk content changed. */
301
- changed: boolean;
302
- /** The final ordered infobox field lines. */
303
- fields: string[];
304
- /** Set when the refresh was rejected (e.g. it would yield >1 info block). */
305
- reason: string | null;
306
- }
307
-
308
- /**
309
- * P8-T2 driver. Extract/refresh the entity's infobox: ask `call("editor", …)`
310
- * for the structured facts, merge them with the existing box (grounded facts
311
- * sticky), and write back the single ```info block. The single-info-block
312
- * invariant is asserted via `countFencedInfoBlocks` — a result with more than
313
- * one block is rejected (NO-OP) rather than committed. Pure with respect to disk.
314
- */
315
- export async function refreshInfobox(
316
- content: string,
317
- opts: {
318
- title: string;
319
- stem: string;
320
- kind: EntityKind;
321
- call: SynthesisCall;
322
- stems?: Set<string>;
323
- aliases?: string[];
324
- maxTokens?: number;
325
- },
326
- ): Promise<InfoboxRefreshResult> {
327
- const stems = new Set(opts.stems ?? []);
328
- stems.add(opts.stem);
329
- const otherArticles = otherArticlesBlock(stems, opts.stem);
330
- const raw = await opts.call(
331
- buildInfoboxRefreshPrompt(opts.title, infoboxSourceText(content), otherArticles),
332
- { maxTokens: opts.maxTokens ?? 192 },
333
- );
334
- const fields = refreshInfoboxFields(content, opts.kind, raw, opts.aliases ?? []);
335
- const next = setInfobox(content, fields);
336
-
337
- if (countFencedInfoBlocks(next) !== 1) {
338
- return { content, changed: false, fields, reason: `refresh would yield ${countFencedInfoBlocks(next)} info blocks` };
339
- }
340
- return { content: next, changed: next !== content, fields, reason: null };
341
- }
342
-
343
- // ===========================================================================
344
- // Effectful entry — wikify one vault article (sections + infobox)
345
- // ===========================================================================
346
-
347
- export interface WikifyArticleOptions {
348
- /** Article filename stem. */
349
- stem: string;
350
- /** Entity kind for the grounded-field rule; defaults to the infobox `kind:` or "thing". */
351
- kind?: EntityKind;
352
- /** Declared aliases to fold into the infobox `aliases:` line. */
353
- aliases?: string[];
354
- /** Vault root (honors MLX_BUN_WIKI); defaults to `vaultRoot()`. */
355
- root?: string;
356
- /** Model-call override (tests inject a fake). Defaults to `callLocal("editor", …)`. */
357
- call?: SynthesisCall;
358
- /** Run the per-section tighten node (P8-T1). Default true. */
359
- improveSections?: boolean;
360
- /** Run the infobox extract/refresh node (P8-T2). Default true. */
361
- refreshInfobox?: boolean;
362
- /** Skip the git commit (tests). */
363
- commit?: boolean;
364
- floor?: number;
365
- }
366
-
367
- export interface WikifyArticleResult {
368
- stem: string;
369
- status: "edited" | "unchanged" | "rejected";
370
- /** Sections tightened and accepted by the gate. */
371
- sectionsImproved: number;
372
- /** Sections whose tightened body was a weak edit, original kept. */
373
- weakEditRejected: number;
374
- /** True when the infobox was created/refreshed. */
375
- infoboxRefreshed: boolean;
376
- /** Veto reason when the final article failed the gate; null otherwise. */
377
- reason: string | null;
378
- normalizeNotes: string[];
379
- }
380
-
381
- /**
382
- * Wikify ONE vault article end to end: per-section tighten (P8-T1) → infobox
383
- * extract/refresh (P8-T2) → NORMALIZE → final gate → write + commit. The final
384
- * `gateEdit(original, normalized)` is the global safety net: if the cumulative
385
- * edit somehow degraded the article (citation/structure/word-floor loss), the
386
- * whole pass is a NO-OP and the vault stays byte-for-byte unchanged.
387
- */
388
- export async function wikifyArticle(opts: WikifyArticleOptions): Promise<WikifyArticleResult> {
389
- const root = opts.root ?? vaultRoot();
390
- const call: SynthesisCall = opts.call ?? ((p, o) => callLocal("editor", { user: p }, o));
391
- const path = join(articlesDir(root), `${opts.stem}.md`);
392
- const base: WikifyArticleResult = {
393
- stem: opts.stem,
394
- status: "unchanged",
395
- sectionsImproved: 0,
396
- weakEditRejected: 0,
397
- infoboxRefreshed: false,
398
- reason: null,
399
- normalizeNotes: [],
400
- };
401
-
402
- let original: string;
403
- try {
404
- original = await readFile(path, "utf8");
405
- } catch {
406
- return { ...base, status: "rejected", reason: `article not found: ${opts.stem}` };
407
- }
408
-
409
- const stems = new Set(await listArticles(root));
410
- const box = parseInfobox(original);
411
- const kind: EntityKind = opts.kind ?? (box ? box.entityKind : "thing");
412
- const title = opts.stem.replace(/_/g, " ");
413
-
414
- let working = original;
415
- let sectionsImproved = 0;
416
- let weakEditRejected = 0;
417
- let infoboxRefreshed = false;
418
-
419
- if (opts.improveSections !== false) {
420
- const r = await improveSections(working, { call, floor: opts.floor });
421
- working = r.content;
422
- sectionsImproved = r.improved.length;
423
- weakEditRejected = r.rejected.length;
424
- }
425
-
426
- if (opts.refreshInfobox !== false) {
427
- const r = await refreshInfobox(working, { title, stem: opts.stem, kind, call, stems, aliases: opts.aliases });
428
- if (r.changed && r.reason == null) {
429
- working = r.content;
430
- infoboxRefreshed = true;
431
- }
432
- }
433
-
434
- const norm = normalizeArticle(working, { stem: opts.stem, stems });
435
- const result: WikifyArticleResult = {
436
- ...base,
437
- sectionsImproved,
438
- weakEditRejected,
439
- infoboxRefreshed,
440
- normalizeNotes: norm.notes,
441
- };
442
-
443
- if (norm.content === original) return { ...result, status: "unchanged" };
444
-
445
- // Final global safety net — the same conservative gate the write path uses.
446
- // A {{stub}} article legitimately falls below the prose floor, so the gate is
447
- // advisory there; otherwise a veto means NO-OP.
448
- const verdict = gateEdit(original, norm.content, { floor: opts.floor });
449
- if (!verdict.ok && !hasStubMarker(norm.content)) {
450
- return { ...result, status: "rejected", reason: verdict.reason };
451
- }
452
-
453
- await mkdir(articlesDir(root), { recursive: true });
454
- await writeFile(path, norm.content);
455
- if (opts.commit !== false) {
456
- await commitVault(root, `memory: wikify ${opts.stem} (+${sectionsImproved} sections${infoboxRefreshed ? ", infobox" : ""})`);
457
- }
458
- return { ...result, status: "edited" };
459
- }
460
-
461
- export interface WikifyVaultSummary {
462
- /** Stems whose on-disk content changed. */
463
- edited: string[];
464
- /** Per-stem result for every article processed. */
465
- results: WikifyArticleResult[];
466
- }
467
-
468
- /**
469
- * Wikify every article in the vault (the periodic sweep). Best-effort and
470
- * non-fatal per article; the caller drives scheduling. Each article commits its
471
- * own change (or is a NO-OP).
472
- */
473
- export async function wikifyVault(
474
- opts: { root?: string; call?: SynthesisCall; commit?: boolean; floor?: number } = {},
475
- ): Promise<WikifyVaultSummary> {
476
- const root = opts.root ?? vaultRoot();
477
- const stems = await listArticles(root);
478
- const edited: string[] = [];
479
- const results: WikifyArticleResult[] = [];
480
- for (const stem of stems) {
481
- const r = await wikifyArticle({ stem, root, call: opts.call, commit: opts.commit, floor: opts.floor });
482
- results.push(r);
483
- if (r.status === "edited") edited.push(stem);
484
- }
485
- return { edited, results };
486
- }
487
-
488
- // ===========================================================================
489
- // P8-T3 — summary-style SPLIT + series banner maintenance
490
- //
491
- // FORWARD maintenance, not a legacy retrofit: our vault is rebuilt entity-
492
- // granular from chunks, so a section only outgrows its article LATER as nightly
493
- // patches accrete. When one section comes to dominate the article (large in
494
- // absolute prose AND bigger than every other body section combined), it is split
495
- // into its own child entity article — the child takes the full cited prose + a
496
- // minimal infobox, the parent keeps a 2–3 sentence summary and a
497
- // `*Main article: [[Child]]*` pointer (the {{Main}} edge `reindex` records as
498
- // via=main). EVERY [^N] in the moved section travels to the child; the parent's
499
- // now-orphan defs are dropped by NORMALIZE, so no citation is lost.
500
- //
501
- // The split deliberately SKIPS the word-floor gate (it is a sanctioned bulk move,
502
- // not an accidental deletion); its gate is structural — both halves must stay
503
- // `validateArticleStructure`-clean and footnote-bijective, else the split is a
504
- // NO-OP. Series-banner maintenance keeps a single `*Part of a series on [[X]].*`
505
- // line directly under the H1.
506
- // ===========================================================================
507
-
508
- function wordCount(s: string): number {
509
- const t = s.trim();
510
- return t ? t.split(/\s+/).length : 0;
511
- }
512
-
513
- /** The H1 title of an article (the first `# Title` line), or "". */
514
- function articleTitle(content: string): string {
515
- for (const line of content.split(/\r?\n/)) {
516
- const m = /^#\s+(.+)$/.exec(line);
517
- if (m) return m[1]!.trim();
518
- }
519
- return "";
520
- }
521
-
522
- /** Map each `[^N]:` definition number to its full definition line. */
523
- function referenceDefMap(content: string): Map<number, string> {
524
- const out = new Map<number, string>();
525
- for (const line of content.split("\n")) {
526
- const d = /^\[\^(\d+)\]:.*$/.exec(line);
527
- if (d) out.set(parseInt(d[1]!, 10), line);
528
- }
529
- return out;
530
- }
531
-
532
- /** The first `n` sentences of `text`, whitespace-collapsed (the deterministic
533
- * summary fallback when no model is supplied / its draft is unusable). */
534
- function firstSentences(text: string, n: number): string {
535
- const flat = text.replace(/\s+/g, " ").trim();
536
- if (!flat) return "";
537
- const parts = flat.match(/[^.!?]+[.!?]+|\S[^.!?]*$/g) ?? [flat];
538
- return parts.slice(0, Math.max(1, n)).join(" ").trim();
539
- }
540
-
541
- export interface BodySectionSize {
542
- title: string;
543
- anchor: string;
544
- /** Word count of the section's body prose (heading excluded). */
545
- words: number;
546
- }
547
-
548
- /** Per body-section prose word counts (excludes lead, See also, Notes,
549
- * References — only the `section` skeleton items). */
550
- export function bodySectionSizes(content: string): BodySectionSize[] {
551
- const lines = content.split(/\r?\n/);
552
- const out: BodySectionSize[] = [];
553
- for (const it of articleStructure(content)) {
554
- if (it.kind !== "section" || !it.title) continue;
555
- const body = lines.slice(it.startLine, it.endLine).join("\n");
556
- out.push({ title: it.title, anchor: slugifyHeading(it.title), words: wordCount(body) });
557
- }
558
- return out;
559
- }
560
-
561
- export interface SplitCandidate {
562
- title: string;
563
- anchor: string;
564
- /** The candidate section's body word count. */
565
- words: number;
566
- /** Combined body word count of every OTHER body section. */
567
- otherWords: number;
568
- }
569
-
570
- /**
571
- * Detect a section that has outgrown the article: a body section that is large
572
- * in absolute prose (≥ `minWords`, a screen-worth) AND dominates — at least
573
- * `dominance`× the words of every other body section combined. Requires ≥2 body
574
- * sections so the parent survives the split. A cohesive article whose sections
575
- * are balanced has no dominator → null (no false split). The largest qualifying
576
- * section wins. Pure.
577
- */
578
- export function detectSplitCandidate(
579
- content: string,
580
- opts: { minWords?: number; dominance?: number } = {},
581
- ): SplitCandidate | null {
582
- const minWords = opts.minWords ?? 120;
583
- const dominance = opts.dominance ?? 1.0;
584
- const sizes = bodySectionSizes(content);
585
- if (sizes.length < 2) return null;
586
- const total = sizes.reduce((s, x) => s + x.words, 0);
587
- let best: SplitCandidate | null = null;
588
- for (const s of sizes) {
589
- const other = total - s.words;
590
- if (s.words >= minWords && s.words >= dominance * other) {
591
- if (!best || s.words > best.words) {
592
- best = { title: s.title, anchor: s.anchor, words: s.words, otherWords: other };
593
- }
594
- }
595
- }
596
- return best;
597
- }
598
-
599
- /** The SCHEMATIC split-summary prompt (no copyable concrete values): the model
600
- * writes the 2–3 sentence pointer that stays in the parent. Citations/headings
601
- * /the Main pointer are added deterministically, so it asks for none. */
602
- function buildSplitSummaryPrompt(parentTitle: string, childTitle: string, body: string): string {
603
- return (
604
- `A sub-topic has been split out of the personal-wiki article "${parentTitle}" into its own article. ` +
605
- `Write a 2 to 3 sentence SUMMARY of "${childTitle}" to leave behind in the parent, so a reader knows what it is and follows the link for detail. ` +
606
- `Do NOT include citation markers, headings, a "Main article" pointer, or code fences — those are added for you. Output ONLY the summary prose.\n\n` +
607
- `SECTION BEING SPLIT OUT:\n${body}\n`
608
- );
609
- }
610
-
611
- export interface SplitOptions {
612
- /** Model override for the parent summary (tests inject a fake); falls back to
613
- * the first sentences of the moved body when absent/leaky. */
614
- call?: SynthesisCall;
615
- /** Known neighbor stems (for the child's wikilink canonicalization). */
616
- stems?: Set<string>;
617
- /** Parent filename stem (H1 enforcement); defaults to the H1's entity stem. */
618
- parentStem?: string;
619
- /** Aliases to seed the child infobox `aliases:`. */
620
- aliases?: string[];
621
- summarySentences?: number;
622
- maxTokens?: number;
623
- }
624
-
625
- export interface SplitResult {
626
- childStem: string;
627
- childTitle: string;
628
- /** The new child article (post-NORMALIZE). */
629
- child: string;
630
- /** The parent with the section replaced by a summary + Main pointer (post-NORMALIZE). */
631
- parent: string;
632
- /** The `[^N]` numbers carried to the child (parent-side renumber may differ). */
633
- movedMarkers: number[];
634
- /** The 2–3 sentence summary left in the parent. */
635
- summary: string;
636
- }
637
-
638
- /**
639
- * Split one section out of `content` into a child entity article. The child
640
- * takes the full cited section prose + a minimal `kind: thing` infobox; the
641
- * parent keeps a summary + `*Main article: [[Child]]*`. Every `[^N]:` definition
642
- * referenced by the moved section is copied to the child; the parent's resulting
643
- * orphan defs are dropped by NORMALIZE. The word-floor gate is intentionally NOT
644
- * applied (a split is a sanctioned move) — both halves are validated structurally
645
- * + footnote-bijectively instead, and the split is a NO-OP (null) if either
646
- * fails. Pure with respect to disk.
647
- */
648
- export async function splitOutSection(
649
- content: string,
650
- anchor: string,
651
- opts: SplitOptions = {},
652
- ): Promise<SplitResult | null> {
653
- const block = extractSection(content, anchor);
654
- if (block == null) return null;
655
- const lines = block.split("\n");
656
- const headingLine = lines[0] ?? "";
657
- const childTitle = headingLine.replace(/^#{1,6}\s+/, "").trim();
658
- if (!childTitle) return null;
659
- const body = lines.slice(1).join("\n").trim();
660
- if (!body) return null;
661
- const childStem = entityStem(childTitle);
662
-
663
- const movedMarkers = [...bodyMarkers(body)].sort((a, b) => a - b);
664
- const refDefs = referenceDefMap(content);
665
- const childDefs = movedMarkers.map((n) => refDefs.get(n)).filter((x): x is string => x != null);
666
-
667
- // --- child: minimal infobox + the full moved prose (the lead) + moved defs ---
668
- const stems = new Set(opts.stems ?? []);
669
- stems.add(childStem);
670
- const infoboxFields = buildInfoboxFields("", "thing", opts.aliases ?? []);
671
- const childRaw = assembleArticle({
672
- stem: childStem,
673
- infoboxFields,
674
- lead: body,
675
- sections: [],
676
- seeAlso: [],
677
- referenceDefs: childDefs,
678
- });
679
- const childNorm = normalizeArticle(childRaw, { stem: childStem, stems });
680
- if (!checkFootnoteIntegrity(childNorm.content).ok || !validateArticleStructure(childNorm.content).ok) {
681
- return null;
682
- }
683
-
684
- // --- parent: summary + Main pointer in place of the section body ---
685
- const parentTitle = articleTitle(content);
686
- let summary = "";
687
- if (opts.call) {
688
- const raw = sanitizeSection(
689
- await opts.call(buildSplitSummaryPrompt(parentTitle, childTitle, body), { maxTokens: opts.maxTokens ?? 160 }),
690
- childTitle,
691
- );
692
- if (raw.trim() && !isLeakyDraft(raw)) summary = raw;
693
- }
694
- if (!summary.trim()) summary = firstSentences(body, opts.summarySentences ?? 2);
695
- // The pointer summary carries NO citations — every marker travels to the child,
696
- // so none dangles in the parent.
697
- summary = summary.replace(/\s*\[\^\d+\]/g, "").replace(/\s+/g, " ").trim();
698
- if (!summary) summary = `See the main article for details on ${childTitle}.`;
699
-
700
- const newBlock = `${headingLine}\n\n${summary}\n\n*Main article: [[${childStem}]]*\n`;
701
- const swapped = replaceSection(content, anchor, newBlock);
702
- if (swapped == null) return null;
703
-
704
- const parentStem = opts.parentStem ?? entityStem(parentTitle || "entity");
705
- const parentStems = new Set(opts.stems ?? []);
706
- parentStems.add(childStem);
707
- parentStems.add(parentStem);
708
- const parentNorm = normalizeArticle(swapped, { stem: parentStem, stems: parentStems });
709
- if (!checkFootnoteIntegrity(parentNorm.content).ok || !validateArticleStructure(parentNorm.content).ok) {
710
- return null;
711
- }
712
-
713
- return {
714
- childStem,
715
- childTitle,
716
- child: childNorm.content,
717
- parent: parentNorm.content,
718
- movedMarkers,
719
- summary,
720
- };
721
- }
722
-
723
- const H1_LINE_RE = /^#\s+/;
724
-
725
- /**
726
- * Maintain the series banner: ensure a single `*Part of a series on [[series]].*`
727
- * line sits directly under the H1 (the grammar slot before the infobox). Inserts
728
- * one when absent, rewrites a divergent one, no-ops on a match. A null/empty
729
- * `series` is a no-op — this maintains, it does not remove. Pure.
730
- */
731
- export function setSeriesBanner(content: string, series: string | null | undefined): { content: string; changed: boolean } {
732
- const name = (series ?? "").trim();
733
- if (!name) return { content, changed: false };
734
- const banner = `*Part of a series on [[${name}]].*`;
735
-
736
- const lines = content.split(/\r?\n/);
737
- let inFence = false;
738
- let h1 = -1;
739
- let bannerIdx = -1;
740
- for (let i = 0; i < lines.length; i++) {
741
- if (isFenceLine(lines[i]!)) {
742
- inFence = !inFence;
743
- continue;
744
- }
745
- if (inFence) continue;
746
- if (h1 < 0 && H1_LINE_RE.test(lines[i]!)) h1 = i;
747
- else if (bannerIdx < 0 && SERIES_RE.test(lines[i]!.trim())) bannerIdx = i;
748
- }
749
-
750
- if (bannerIdx >= 0) {
751
- if (lines[bannerIdx]!.trim() === banner) return { content, changed: false };
752
- lines[bannerIdx] = banner;
753
- return { content: lines.join("\n"), changed: true };
754
- }
755
- if (h1 < 0) return { content: `${banner}\n\n${content}`, changed: true };
756
- lines.splice(h1 + 1, 0, "", banner);
757
- return { content: lines.join("\n"), changed: true };
758
- }
759
-
760
- // ---- effectful SPLIT entry (write child + parent, commit) ------------------
761
-
762
- export interface ApplySplitResult {
763
- status: "split" | "no-candidate" | "rejected";
764
- stem: string;
765
- childStem: string | null;
766
- anchor: string | null;
767
- reason: string | null;
768
- }
769
-
770
- /**
771
- * Detect and apply a summary-style split on one vault article: write the new
772
- * child article + the trimmed parent and commit. A NO-OP when no section
773
- * dominates ("no-candidate") or the structural split gate vetoes ("rejected").
774
- * Never clobbers an existing child article (that would be a MERGE, not a split).
775
- */
776
- export async function applySplit(opts: {
777
- stem: string;
778
- root?: string;
779
- call?: SynthesisCall;
780
- commit?: boolean;
781
- minWords?: number;
782
- dominance?: number;
783
- }): Promise<ApplySplitResult> {
784
- const root = opts.root ?? vaultRoot();
785
- const base: ApplySplitResult = { status: "no-candidate", stem: opts.stem, childStem: null, anchor: null, reason: null };
786
- const path = join(articlesDir(root), `${opts.stem}.md`);
787
-
788
- let content: string;
789
- try {
790
- content = await readFile(path, "utf8");
791
- } catch {
792
- return { ...base, status: "rejected", reason: `article not found: ${opts.stem}` };
793
- }
794
-
795
- const cand = detectSplitCandidate(content, { minWords: opts.minWords, dominance: opts.dominance });
796
- if (!cand) return base;
797
-
798
- const stems = new Set(await listArticles(root));
799
- const result = await splitOutSection(content, cand.anchor, { call: opts.call, stems, parentStem: opts.stem });
800
- if (!result) return { ...base, status: "rejected", anchor: cand.anchor, reason: "split failed the structural gate (NO-OP)" };
801
-
802
- const childPath = join(articlesDir(root), `${result.childStem}.md`);
803
- try {
804
- await readFile(childPath, "utf8");
805
- return { ...base, status: "rejected", anchor: cand.anchor, childStem: result.childStem, reason: "child article already exists (would be a MERGE, not a split)" };
806
- } catch {
807
- // child does not exist — good, proceed.
808
- }
809
-
810
- await mkdir(articlesDir(root), { recursive: true });
811
- await writeFile(childPath, result.child);
812
- await writeFile(path, result.parent);
813
- if (opts.commit !== false) {
814
- await commitVault(root, `memory: split ${result.childStem} out of ${opts.stem} (${result.movedMarkers.length} citations moved)`);
815
- }
816
- return { status: "split", stem: opts.stem, childStem: result.childStem, anchor: cand.anchor, reason: null };
817
- }
818
-
819
- // ===========================================================================
820
- // P8-T3 (cont.) — MERGE signal: two near-duplicate stubs
821
- //
822
- // MERGE is a CROSS-article action: it is NEVER performed silently. This only
823
- // EMITS a signal (which P8-T4 surfaces on a Talk page); a human / a later
824
- // reconcile step decides. Two stubs describing the same thing under different
825
- // names have near-identical token content — a high Jaccard overlap flags them.
826
- // ===========================================================================
827
-
828
- const MERGE_STOP = new Set([
829
- "the", "a", "an", "and", "or", "of", "to", "for", "in", "on", "with", "is", "it", "as",
830
- "at", "by", "be", "this", "that", "are", "was", "user", "uses", "used", "use",
831
- ]);
832
-
833
- /** Content tokens (title + body words), markers/links/fences stripped, stopworded
834
- * — the bag a stub-vs-stub Jaccard compares. */
835
- function articleTokens(content: string): Set<string> {
836
- const flat = content
837
- .replace(/```[\s\S]*?```/g, " ") // fenced blocks (infobox/code) out
838
- .replace(/\[\^\d+\][:]?/g, " ") // footnote markers/defs
839
- .replace(/\[\[([^\]]+)\]\]/g, "$1") // wikilink → its text
840
- .replace(/[#*_`>|-]/g, " ");
841
- const out = new Set<string>();
842
- for (const t of flat.toLowerCase().split(/[^a-z0-9]+/)) {
843
- if (t.length > 1 && !MERGE_STOP.has(t)) out.add(t);
844
- }
845
- return out;
846
- }
847
-
848
- function jaccard(a: Set<string>, b: Set<string>): number {
849
- if (a.size === 0 || b.size === 0) return 0;
850
- let inter = 0;
851
- for (const t of a) if (b.has(t)) inter++;
852
- return inter / (a.size + b.size - inter);
853
- }
854
-
855
- export interface MergeSignal {
856
- a: string;
857
- b: string;
858
- jaccard: number;
859
- reason: string;
860
- }
861
-
862
- /**
863
- * Flag pairs of articles that look like the SAME thing under different names —
864
- * a MERGE signal, never an action. By default only stubs are considered (the
865
- * thin near-duplicates summary-style merges); their content-token Jaccard ≥
866
- * `threshold` emits a signal. Deterministic: pairs are sorted, scanned in stem
867
- * order. Pure.
868
- */
869
- export function detectMergeCandidates(
870
- articles: { stem: string; content: string }[],
871
- opts: { threshold?: number; stubsOnly?: boolean } = {},
872
- ): MergeSignal[] {
873
- const threshold = opts.threshold ?? 0.5;
874
- const stubsOnly = opts.stubsOnly ?? true;
875
- const pool = articles
876
- .filter((x) => !stubsOnly || articleThinness(x.content).stub)
877
- .map((x) => ({ stem: x.stem, toks: articleTokens(x.content) }))
878
- .sort((p, q) => p.stem.localeCompare(q.stem));
879
-
880
- const out: MergeSignal[] = [];
881
- for (let i = 0; i < pool.length; i++) {
882
- for (let j = i + 1; j < pool.length; j++) {
883
- const score = jaccard(pool[i]!.toks, pool[j]!.toks);
884
- if (score >= threshold) {
885
- out.push({
886
- a: pool[i]!.stem,
887
- b: pool[j]!.stem,
888
- jaccard: score,
889
- reason: `near-duplicate stubs (token Jaccard ${score.toFixed(2)}) — likely the same thing under two names`,
890
- });
891
- }
892
- }
893
- }
894
- return out.sort((x, y) => y.jaccard - x.jaccard || x.a.localeCompare(y.a));
895
- }
896
-
897
- // ===========================================================================
898
- // P8-T4 — Talk-page restructure SIGNALS (never silent cross-article surgery)
899
- //
900
- // The structural pass NEVER restructures across articles on its own — split and
901
- // merge are CROSS-article actions, so it writes SUGGESTIONS to `Talk/<stem>.md`
902
- // (Lucien `<<<TALK>>>` style) for a human / a later reconcile step. The embedding
903
- // SILHOUETTE is the ONE sanctioned OFFLINE triage instrument here: section-cohesion
904
- // spots an article whose sections drifted into distinct topics (a split candidate
905
- // the size heuristic alone might miss). It runs ONLY in this offline triage — the
906
- // `embed` function is injected, so this module never reaches `src/embed.ts` and
907
- // the read/route hot path stays at zero embedding calls (the P0-T6 tripwire).
908
- // ===========================================================================
909
-
910
- /** Inject the embedding backend (so `wikify.ts` never imports `src/embed.ts` and
911
- * the read path can never accidentally embed). The eval wires the real
912
- * Qwen3-Embedding through here; tests inject a deterministic fake. */
913
- export type EmbedFn = (texts: string[]) => Float32Array[];
914
-
915
- function cosine(a: Float32Array, b: Float32Array): number {
916
- let dot = 0;
917
- let na = 0;
918
- let nb = 0;
919
- const n = Math.min(a.length, b.length);
920
- for (let i = 0; i < n; i++) {
921
- dot += a[i]! * b[i]!;
922
- na += a[i]! * a[i]!;
923
- nb += b[i]! * b[i]!;
924
- }
925
- if (na === 0 || nb === 0) return 0;
926
- return dot / Math.sqrt(na * nb);
927
- }
928
-
929
- export interface CohesionResult {
930
- sections: number;
931
- /** Mean pairwise cosine across body-section embeddings (1 when <2 sections).
932
- * LOW = the sections cover distinct topics → a split candidate. */
933
- meanPairwise: number;
934
- /** The least-similar section pair's cosine (1 when <2 sections). */
935
- minPair: number;
936
- }
937
-
938
- /**
939
- * The OFFLINE cohesion silhouette: embed each body section and measure how alike
940
- * they are. A low mean pairwise cosine means the article's sections have drifted
941
- * into distinct topics — a structural split signal the word-size heuristic can
942
- * miss. This is the SANCTIONED offline use of embeddings; `embed` is injected and
943
- * MUST NOT be the read/route path. Pure aside from the injected `embed`.
944
- */
945
- export function sectionCohesion(content: string, embed: EmbedFn): CohesionResult {
946
- const lines = content.split(/\r?\n/);
947
- const bodies: string[] = [];
948
- for (const it of articleStructure(content)) {
949
- if (it.kind !== "section" || !it.title) continue;
950
- const body = lines.slice(it.startLine, it.endLine).join("\n").trim();
951
- if (body) bodies.push(body);
952
- }
953
- if (bodies.length < 2) return { sections: bodies.length, meanPairwise: 1, minPair: 1 };
954
-
955
- const vecs = embed(bodies);
956
- let sum = 0;
957
- let count = 0;
958
- let min = 1;
959
- for (let i = 0; i < vecs.length; i++) {
960
- for (let j = i + 1; j < vecs.length; j++) {
961
- const c = cosine(vecs[i]!, vecs[j]!);
962
- sum += c;
963
- count++;
964
- if (c < min) min = c;
965
- }
966
- }
967
- return { sections: bodies.length, meanPairwise: count ? sum / count : 1, minPair: min };
968
- }
969
-
970
- export interface TalkSuggestion {
971
- kind: "split" | "merge" | "contradiction";
972
- text: string;
973
- }
974
-
975
- /**
976
- * Compose the structural-triage suggestions for one article from the detections:
977
- * a size-dominant split candidate, a low-cohesion silhouette split signal, and
978
- * any merge pairs involving this article. Pure; SCHEMATIC text (no copyable
979
- * concrete values from policy).
980
- */
981
- export function buildStructureTalk(input: {
982
- stem: string;
983
- split?: SplitCandidate | null;
984
- cohesion?: CohesionResult | null;
985
- merges?: MergeSignal[];
986
- cohesionFloor?: number;
987
- }): TalkSuggestion[] {
988
- const out: TalkSuggestion[] = [];
989
- const floor = input.cohesionFloor ?? 0.4;
990
-
991
- if (input.split) {
992
- const childStem = entityStem(input.split.title);
993
- out.push({
994
- kind: "split",
995
- text:
996
- `SPLIT: the "${input.split.title}" section (${input.split.words} words) dominates this article ` +
997
- `(vs ${input.split.otherWords} words across all other sections). Consider splitting it to ` +
998
- `[[${childStem}]] with a {{Main}} pointer; every [^N] in it travels to the child.`,
999
- });
1000
- }
1001
- if (input.cohesion && input.cohesion.sections >= 2 && input.cohesion.meanPairwise < floor) {
1002
- out.push({
1003
- kind: "split",
1004
- text:
1005
- `SPLIT (cohesion): this article's ${input.cohesion.sections} sections read as distinct topics ` +
1006
- `(mean section cohesion ${input.cohesion.meanPairwise.toFixed(2)} < ${floor.toFixed(2)}). ` +
1007
- `Consider splitting the divergent section(s) into their own article(s).`,
1008
- });
1009
- }
1010
- for (const m of input.merges ?? []) {
1011
- if (m.a !== input.stem && m.b !== input.stem) continue;
1012
- const other = m.a === input.stem ? m.b : m.a;
1013
- out.push({
1014
- kind: "merge",
1015
- text: `MERGE: [[${input.stem}]] and [[${other}]] look like the same thing under two names (${m.reason}). Consider merging to the canonical title; the loser becomes an alias + redirect.`,
1016
- });
1017
- }
1018
- return out;
1019
- }
1020
-
1021
- /** The Talk directory for a vault (sibling of articles/). */
1022
- export function talkDir(root = vaultRoot()): string {
1023
- return join(root, "Talk");
1024
- }
1025
-
1026
- /**
1027
- * Write the structural-triage suggestions to `Talk/<stem>.md` in the Lucien
1028
- * `<<<TALK>>>` style. Writes ONLY under `Talk/` — it NEVER edits an article, so
1029
- * a cross-article split/merge is surfaced, never silently performed. Returns the
1030
- * written path, or null when there is nothing to suggest. The block is rewritten
1031
- * each run (idempotent — one managed block per article).
1032
- */
1033
- export async function writeTalkPage(opts: {
1034
- stem: string;
1035
- suggestions: TalkSuggestion[];
1036
- title?: string;
1037
- root?: string;
1038
- now?: number;
1039
- }): Promise<string | null> {
1040
- if (opts.suggestions.length === 0) return null;
1041
- const root = opts.root ?? vaultRoot();
1042
- const dir = talkDir(root);
1043
- await mkdir(dir, { recursive: true });
1044
- const path = join(dir, `${opts.stem}.md`);
1045
- const date = new Date(opts.now ?? Date.now()).toISOString().slice(0, 10);
1046
- const title = opts.title ?? opts.stem.replace(/_/g, " ");
1047
-
1048
- const body = [
1049
- `# Talk: ${title}`,
1050
- "",
1051
- "> Structural-triage suggestions from the nightly wikify sweep. These are",
1052
- "> SUGGESTIONS ONLY — mlx-bun never restructures across articles automatically.",
1053
- "",
1054
- "<<<TALK>>>",
1055
- `## ${date} — structural triage`,
1056
- "",
1057
- ...opts.suggestions.map((s) => `- **${s.kind.toUpperCase()}** — ${s.text}`),
1058
- "<<<END TALK>>>",
1059
- "",
1060
- ].join("\n");
1061
-
1062
- await writeFile(path, body);
1063
- return path;
1064
- }