mlx-bun 0.4.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 (601) hide show
  1. package/README.md +1453 -88
  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 +10 -8
  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 +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  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 +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  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/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /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
- }