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.
- package/README.md +1449 -109
- package/THIRD_PARTY_NOTICES.md +882 -0
- package/bin/mlx-bun.convert.mjs +4 -0
- package/bin/mlx-bun.fuse.mjs +4 -0
- package/bin/mlx-bun.generate.mjs +4 -0
- package/bin/mlx-bun.lora.mjs +4 -0
- package/bin/mlx-bun.mjs +13 -21
- package/bin/mlx-bun.server.mjs +4 -0
- package/bin/mlx-bun.upload.mjs +4 -0
- package/dist/web/app.js +9584 -0
- package/package.json +50 -53
- package/src/cli/ane-install.ts +76 -0
- package/src/cli/args.ts +179 -0
- package/src/cli/engine-entry.ts +68 -0
- package/src/cli/inference.ts +214 -0
- package/src/cli/job-entry.ts +97 -0
- package/src/cli/library-host.ts +117 -0
- package/src/cli/main.ts +57 -0
- package/src/cli/memory-engine.ts +163 -0
- package/src/cli/memory.ts +36 -0
- package/src/cli/mlx-lm-aliases.ts +309 -0
- package/src/cli/model-choice.ts +33 -0
- package/src/cli/model-selection.ts +84 -0
- package/src/cli/module-host.ts +11 -0
- package/src/cli/module-verbs.ts +50 -0
- package/src/cli/numerical-policy.ts +12 -0
- package/src/cli/perplexity.ts +89 -0
- package/src/cli/serve-host.ts +423 -0
- package/src/cli/serve-isolated.ts +217 -0
- package/src/cli/serve-options.ts +106 -0
- package/src/cli/serve-state.ts +205 -0
- package/src/cli/serve.ts +500 -0
- package/src/cli/served-model-host.ts +102 -0
- package/src/cli/server-entry.ts +148 -0
- package/src/cli/serving-unit.ts +177 -0
- package/src/{tui.ts → cli/terminal.ts} +13 -8
- package/src/cli/worker-adapters.ts +34 -0
- package/src/cli/worker-entry.ts +272 -0
- package/src/cli/worker-unit.ts +179 -0
- package/src/engine/adapter-operation.ts +26 -0
- package/src/engine/cache-services.ts +114 -0
- package/src/engine/completion.ts +39 -0
- package/src/engine/generation-gateway.ts +358 -0
- package/src/engine/index.ts +59 -0
- package/src/engine/kv-budget.ts +120 -0
- package/src/engine/media-preparation.ts +232 -0
- package/src/engine/model-binding.ts +24 -0
- package/src/engine/model-host.ts +513 -0
- package/src/engine/model-serving.ts +89 -0
- package/src/{serve → engine}/preparation.ts +7 -5
- package/src/{serve → engine}/session-completion-engine.ts +6 -6
- package/src/engine/telemetry.ts +109 -0
- package/src/hub/downloads.ts +116 -0
- package/src/jobs/db.ts +16 -7
- package/src/jobs/events.ts +29 -0
- package/src/jobs/executable.ts +5 -0
- package/src/jobs/host.ts +51 -0
- package/src/jobs/in-process.ts +55 -0
- package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
- package/src/jobs/runner.ts +150 -231
- package/src/jobs/service.ts +75 -0
- package/src/jobs/sse.ts +59 -23
- package/src/jobs/worker-memory.ts +46 -0
- package/src/jobs/worker-process.ts +163 -0
- package/src/jobs/worker-supervisor.ts +258 -0
- package/src/modules.ts +58 -0
- package/src/publishing/catalog-hub.ts +18 -0
- package/src/publishing/credentials.ts +31 -0
- package/src/publishing/upload.ts +37 -0
- package/src/residency/leased-adapters.ts +21 -0
- package/src/residency/local-records.ts +16 -0
- package/src/residency/model-residency.ts +405 -0
- package/src/residency/record-index.ts +28 -0
- package/src/residency/resident-estimate.ts +50 -0
- package/src/{anthropic.ts → server/anthropic.ts} +3 -81
- package/src/server/cache-routes.ts +36 -0
- package/src/{serve → server}/chat-request.ts +10 -10
- package/src/{serve → server}/chat-stage.ts +14 -14
- package/src/{client.ts → server/client.ts} +28 -2
- package/src/{serve → server}/completion-executor.ts +33 -41
- package/src/server/decision-body.ts +55 -0
- package/src/server/decision-routes.ts +134 -0
- package/src/server/decision-wire.ts +228 -0
- package/src/{serve → server}/discovery-routes.ts +25 -82
- package/src/{serve → server}/http.ts +25 -13
- package/src/{serve → server}/inference-request.ts +5 -5
- package/src/server/job-routes.ts +20 -0
- package/src/server/media-prompt.ts +63 -0
- package/src/server/memory-completion-client.ts +46 -0
- package/src/server/model-routes.ts +192 -0
- package/src/{serve → server}/openai-wire.ts +3 -2
- package/src/{serve → server}/pipeline.ts +1 -1
- package/src/server/prompt-builder.ts +13 -0
- package/src/server/prompt-contracts.ts +17 -0
- package/src/server/proxy-routes.ts +187 -0
- package/src/server/publishing-routes.ts +41 -0
- package/src/{serve → server}/request-plan.ts +6 -8
- package/src/{serve → server}/request-prep.ts +47 -50
- package/src/{serve → server}/responses-client.ts +30 -13
- package/src/{responses.ts → server/responses.ts} +12 -93
- package/src/server/routes.ts +205 -0
- package/src/server/start.ts +72 -0
- package/src/server/status-routes.ts +108 -0
- package/src/{serve → server}/text-completion-stage.ts +25 -4
- package/src/server/text-prompt.ts +12 -0
- package/src/{serve → server}/token-streams.ts +36 -38
- package/src/server/worker-routes.ts +311 -0
- package/src/storage/paths.ts +61 -0
- package/src/web/assets.ts +37 -0
- package/src/web/{src → browser}/api.ts +5 -23
- package/src/web/browser/chat-host.ts +17 -0
- package/src/web/browser/installed-panels.ts +6 -0
- package/src/web/browser/main.ts +55 -0
- package/src/web/browser/markdown.ts +279 -0
- package/src/web/browser/memory-panel.ts +9 -0
- package/src/web/browser/palette.ts +64 -0
- package/src/web/browser/protocol.ts +13 -0
- package/src/web/browser/shell.ts +467 -0
- package/src/web/{src → browser}/status.ts +32 -36
- package/src/web/build.ts +51 -0
- package/src/web/public/app.html +1054 -0
- package/src/web/{sw.js → public/sw.js} +4 -3
- package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
- package/src/web/{vendor → public/vendor}/hljs.js +2 -2
- package/THIRD_PARTY_LICENSES.md +0 -51
- package/docs/README.md +0 -107
- package/docs/archive/hlg-sampling.md +0 -350
- package/docs/archive/investigations/audio-input-plan.md +0 -356
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
- package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
- package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
- package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
- package/docs/archive/investigations/curve-bisector-routing.md +0 -123
- package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
- package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
- package/docs/archive/investigations/dspark-handoff.md +0 -224
- package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
- package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
- package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
- package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
- package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
- package/docs/archive/investigations/lab-build-journal.md +0 -157
- package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
- package/docs/archive/investigations/openjarvis-fit.md +0 -198
- package/docs/archive/investigations/openwiki-evaluation.md +0 -249
- package/docs/archive/investigations/optimization_plan.md +0 -346
- package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
- package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
- package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
- package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
- package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
- package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
- package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
- package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
- package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
- package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
- package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
- package/docs/archive/planning/chat-ui-vision.md +0 -50
- package/docs/archive/planning/chunk-task-roadmap.md +0 -77
- package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
- package/docs/archive/planning/journal.md +0 -316
- package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
- package/docs/archive/planning/pi-integration-review.md +0 -178
- package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
- package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
- package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
- package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
- package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
- package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
- package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
- package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
- package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
- package/docs/archive/planning/web-ui-pass-plan.md +0 -213
- package/docs/archive/planning/website-readme-pass-plan.md +0 -224
- package/docs/design/batching.md +0 -2054
- package/docs/design/decode-speed-program.md +0 -2404
- package/docs/design/dreaming-nightly-pipeline.md +0 -684
- package/docs/design/generic-model-support.md +0 -821
- package/docs/design/kv-cache.md +0 -993
- package/docs/design/orpo-training.md +0 -680
- package/docs/design/speculative-decoding.md +0 -2129
- package/docs/design/turboquant.md +0 -2225
- package/docs/design/unified-engine-frontier-plan.md +0 -1987
- package/docs/design/web-chat-redesign.md +0 -695
- package/docs/planning/PRODUCT_ROADMAP.md +0 -593
- package/docs/planning/ResearchTopics.md +0 -119
- package/docs/planning/public-readiness-handoff.md +0 -244
- package/docs/planning/public-readiness.md +0 -149
- package/docs/planning/video-series.md +0 -421
- package/docs/reference/benchmarks.md +0 -3587
- package/docs/reference/cli.md +0 -646
- package/docs/reference/distribution.md +0 -380
- package/docs/reference/environment.md +0 -494
- package/docs/reference/glossary.md +0 -39
- package/docs/reference/library-api.md +0 -688
- package/docs/reference/memory.md +0 -260
- package/docs/reference/models.md +0 -346
- package/docs/reference/server-api.md +0 -1945
- package/docs/reference/server-config.md +0 -973
- package/docs/reference/training.md +0 -462
- package/docs/reference/troubleshooting.md +0 -158
- package/src/admission-plan.ts +0 -18
- package/src/audio/audiotoolbox.ts +0 -112
- package/src/audio/conformer.ts +0 -655
- package/src/audio/decode.ts +0 -133
- package/src/audio/features.ts +0 -202
- package/src/audio/mic-capture.ts +0 -98
- package/src/audio/silero-vad.ts +0 -379
- package/src/audio/transcode.ts +0 -67
- package/src/audio/whisper-decode.ts +0 -774
- package/src/audio/whisper-fast.ts +0 -732
- package/src/audio/whisper-format.ts +0 -73
- package/src/audio/whisper-mel.ts +0 -185
- package/src/audio/whisper-timing.ts +0 -274
- package/src/audio/whisper-tokenizer.ts +0 -239
- package/src/audio/whisper-transcribe.ts +0 -410
- package/src/backends/mlx/assistant-target.ts +0 -46
- package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
- package/src/backends/mlx/autoregressive.ts +0 -129
- package/src/backends/mlx/batch-group.ts +0 -1874
- package/src/backends/mlx/cache-attachment.ts +0 -67
- package/src/backends/mlx/cache-layout.ts +0 -31
- package/src/backends/mlx/checkpoint-state.ts +0 -53
- package/src/backends/mlx/continuation-persistence.ts +0 -107
- package/src/backends/mlx/continuation-request.ts +0 -43
- package/src/backends/mlx/continuation.ts +0 -77
- package/src/backends/mlx/diffusion.ts +0 -39
- package/src/backends/mlx/draft-target.ts +0 -32
- package/src/backends/mlx/encoder-cache.ts +0 -25
- package/src/backends/mlx/evaluation.ts +0 -105
- package/src/backends/mlx/fill-append.ts +0 -58
- package/src/backends/mlx/fill-group.ts +0 -497
- package/src/backends/mlx/full-kv-row-append.ts +0 -32
- package/src/backends/mlx/full-kv-row-donor.ts +0 -54
- package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
- package/src/backends/mlx/gateway-binding.ts +0 -174
- package/src/backends/mlx/graph.ts +0 -38
- package/src/backends/mlx/kv-codec.ts +0 -9
- package/src/backends/mlx/kv-maintenance.ts +0 -161
- package/src/backends/mlx/kv-tensor-rows.ts +0 -212
- package/src/backends/mlx/memory-client.ts +0 -189
- package/src/backends/mlx/methods.ts +0 -90
- package/src/backends/mlx/mixed-iteration.ts +0 -62
- package/src/backends/mlx/model-prompt.ts +0 -178
- package/src/backends/mlx/model-serving.ts +0 -80
- package/src/backends/mlx/paged-cache-codec.ts +0 -27
- package/src/backends/mlx/prefill-cohort.ts +0 -100
- package/src/backends/mlx/prefill-policy.ts +0 -26
- package/src/backends/mlx/prefill-rows.ts +0 -244
- package/src/backends/mlx/prefill.ts +0 -33
- package/src/backends/mlx/projected-context-rows.ts +0 -72
- package/src/backends/mlx/prompt-input.ts +0 -29
- package/src/backends/mlx/qwen-prompt-input.ts +0 -40
- package/src/backends/mlx/request-policy.ts +0 -20
- package/src/backends/mlx/request-state-policy.ts +0 -54
- package/src/backends/mlx/rollback.ts +0 -92
- package/src/backends/mlx/row-sampling.ts +0 -49
- package/src/backends/mlx/serial-executor.ts +0 -251
- package/src/backends/mlx/speculative-group.ts +0 -438
- package/src/backends/mlx/speculative-round.ts +0 -161
- package/src/backends/mlx/speculative.ts +0 -131
- package/src/backends/mlx/state-rows.ts +0 -104
- package/src/backends/mlx/state-views.ts +0 -46
- package/src/backends/mlx/target-layout-capability.ts +0 -35
- package/src/backends/mlx/text-engine.ts +0 -73
- package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
- package/src/chat-template.ts +0 -382
- package/src/cli.ts +0 -3490
- package/src/config.ts +0 -399
- package/src/contracts/completion.ts +0 -16
- package/src/contracts/execution.ts +0 -70
- package/src/contracts/generation.ts +0 -103
- package/src/contracts/host.ts +0 -7
- package/src/contracts/object-cache.ts +0 -8
- package/src/contracts/pi-provider.ts +0 -30
- package/src/contracts/pi.ts +0 -261
- package/src/contracts/prefix-cache.ts +0 -33
- package/src/contracts/resources.ts +0 -25
- package/src/contracts/sampling.ts +0 -23
- package/src/contracts/scheduling.ts +0 -54
- package/src/contracts/tsconfig.json +0 -14
- package/src/dataset/generators.ts +0 -1087
- package/src/dataset/index.ts +0 -12
- package/src/dataset/job.ts +0 -81
- package/src/dataset/llm.ts +0 -71
- package/src/dataset/registry.ts +0 -528
- package/src/diffusion/diffusion-generate.ts +0 -507
- package/src/download.ts +0 -505
- package/src/embed.ts +0 -77
- package/src/engine/admission.ts +0 -59
- package/src/engine/cancellation.ts +0 -38
- package/src/engine/engine.ts +0 -56
- package/src/engine/execution-coordinator.ts +0 -70
- package/src/engine/execution-plan.ts +0 -64
- package/src/engine/execution-tasks.ts +0 -44
- package/src/engine/namespaced-cache.ts +0 -13
- package/src/engine/resources.ts +0 -53
- package/src/engine/scheduler.ts +0 -92
- package/src/engine/session.ts +0 -247
- package/src/eval/README.md +0 -130
- package/src/eval/ifeval.ts +0 -81
- package/src/eval/kl-prompts.ts +0 -29
- package/src/eval/kl.ts +0 -590
- package/src/eval/perplexity.ts +0 -164
- package/src/eval/quality-db.ts +0 -92
- package/src/eval/reference-logits.ts +0 -207
- package/src/eval/runner.ts +0 -124
- package/src/eval/score.ts +0 -49
- package/src/eval/tasks/bfcl.ts +0 -535
- package/src/eval/tasks/gsm8k.ts +0 -98
- package/src/eval/tasks/hashhop.ts +0 -139
- package/src/eval/tasks/humaneval.ts +0 -275
- package/src/eval/tasks/ifeval.ts +0 -525
- package/src/eval/tasks/mmlu.ts +0 -157
- package/src/eval/ultrafeedback.ts +0 -60
- package/src/evaldb.ts +0 -199
- package/src/expert-io.ts +0 -393
- package/src/expert-offload-build.ts +0 -77
- package/src/expert-offload.ts +0 -76
- package/src/expert-residency.ts +0 -1134
- package/src/expert-usage.ts +0 -415
- package/src/fill/echo-index.ts +0 -188
- package/src/fill/fill-session.ts +0 -463
- package/src/fill/proposal.ts +0 -68
- package/src/fill/schema-rows.ts +0 -506
- package/src/fill/tool-boundary.ts +0 -103
- package/src/fit.ts +0 -249
- package/src/generate.ts +0 -1352
- package/src/grammar.ts +0 -604
- package/src/harness-pi.ts +0 -184
- package/src/hf-push.ts +0 -628
- package/src/hub-rest.ts +0 -242
- package/src/index.ts +0 -63
- package/src/inference/denoising.ts +0 -17
- package/src/inference/draft-acceptance.ts +0 -41
- package/src/inference/draft-output.ts +0 -49
- package/src/inference/graph.ts +0 -33
- package/src/inference/prefill.ts +0 -45
- package/src/inference/rollback.ts +0 -7
- package/src/jobs/index.ts +0 -31
- package/src/jobs/job-entry.ts +0 -107
- package/src/jobs/task-client.ts +0 -19
- package/src/jobs/types.ts +0 -2
- package/src/kv-scheme.ts +0 -279
- package/src/kv-store.ts +0 -1135
- package/src/lab/README.md +0 -22
- package/src/lab/curve/curve-designer.html +0 -303
- package/src/lab/curve/curve-sampler.ts +0 -122
- package/src/lab/expert-trace/expert-trace.ts +0 -69
- package/src/lab/paged-kv/paged-attention.ts +0 -117
- package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
- package/src/lab/paged-kv/paged-kv.ts +0 -336
- package/src/library.ts +0 -40
- package/src/lora.ts +0 -408
- package/src/md-modules.d.ts +0 -54
- package/src/media-fetch.ts +0 -442
- package/src/memory/article.ts +0 -558
- package/src/memory/chunk-validation.ts +0 -150
- package/src/memory/chunk.ts +0 -318
- package/src/memory/cluster.ts +0 -351
- package/src/memory/coverage.ts +0 -134
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +0 -253
- package/src/memory/entity.ts +0 -203
- package/src/memory/gate.ts +0 -187
- package/src/memory/history.ts +0 -152
- package/src/memory/ingest.ts +0 -335
- package/src/memory/model.ts +0 -183
- package/src/memory/normalize.ts +0 -671
- package/src/memory/parse.ts +0 -22
- package/src/memory/pipeline.ts +0 -275
- package/src/memory/prompts.ts +0 -65
- package/src/memory/query.ts +0 -334
- package/src/memory/reconcile.ts +0 -734
- package/src/memory/redirect.ts +0 -108
- package/src/memory/reindex.ts +0 -325
- package/src/memory/resolve.ts +0 -307
- package/src/memory/rest.ts +0 -245
- package/src/memory/route.ts +0 -272
- package/src/memory/schedule.ts +0 -165
- package/src/memory/stages.ts +0 -777
- package/src/memory/synthesize.ts +0 -1638
- package/src/memory/tools.ts +0 -749
- package/src/memory/vault.ts +0 -1194
- package/src/memory/wikify.ts +0 -1064
- package/src/mlx/array.ts +0 -425
- package/src/mlx/autograd.ts +0 -304
- package/src/mlx/checkpoint.ts +0 -149
- package/src/mlx/compile.ts +0 -168
- package/src/mlx/custom-vjp.ts +0 -188
- package/src/mlx/ffi.ts +0 -531
- package/src/mlx/materialize.ts +0 -23
- package/src/mlx/metal-kernel.ts +0 -159
- package/src/mlx/normalized-argmax.ts +0 -116
- package/src/mlx/ops.ts +0 -1108
- package/src/mlx/quantized-rows.ts +0 -36
- package/src/mlx/token-bitmask.ts +0 -52
- package/src/mlx/turboquant-kv-decode.ts +0 -82
- package/src/mlx/turboquant-kv-inverse.ts +0 -122
- package/src/mlx/turboquant-ops.ts +0 -861
- package/src/mlx/turboquant-tables.ts +0 -237
- package/src/mmap.ts +0 -90
- package/src/model/artifact-identity.ts +0 -73
- package/src/model/batched-kv.ts +0 -235
- package/src/model/batched-mask.ts +0 -279
- package/src/model/batched-quant.ts +0 -212
- package/src/model/batched-quantized-kv.ts +0 -144
- package/src/model/batched-rotating-quant.ts +0 -353
- package/src/model/batched-rotating-state.ts +0 -149
- package/src/model/batched-rotating.ts +0 -338
- package/src/model/batched-row-storage.ts +0 -179
- package/src/model/batched-ssm.ts +0 -52
- package/src/model/batched-turboquant-kv.ts +0 -94
- package/src/model/compiled-decode.ts +0 -712
- package/src/model/decoded-kv-donor.ts +0 -12
- package/src/model/delayed-quantized-kv.ts +0 -72
- package/src/model/delayed-rotating-quantized-kv.ts +0 -93
- package/src/model/delayed-turboquant-kv.ts +0 -57
- package/src/model/diffusion-gemma.ts +0 -1026
- package/src/model/factory.ts +0 -215
- package/src/model/fingerprint.ts +0 -45
- package/src/model/flash-attention.ts +0 -694
- package/src/model/full-prefill-padding.ts +0 -56
- package/src/model/full-prefill-row.ts +0 -105
- package/src/model/gemma4-base.ts +0 -2370
- package/src/model/gemma4.ts +0 -1395
- package/src/model/generated/gemma4-12b.ts +0 -429
- package/src/model/generated/gemma4-26b.ts +0 -448
- package/src/model/generated/gemma4-e4b.ts +0 -717
- package/src/model/generated/index.ts +0 -18
- package/src/model/glm52-atlas.ts +0 -511
- package/src/model/glm52-cache.ts +0 -792
- package/src/model/glm52-config.ts +0 -262
- package/src/model/glm52-container.ts +0 -305
- package/src/model/glm52-coupling.ts +0 -388
- package/src/model/glm52-dsa.ts +0 -606
- package/src/model/glm52-expert-layout.ts +0 -253
- package/src/model/glm52-layout.ts +0 -152
- package/src/model/glm52-memory.ts +0 -500
- package/src/model/glm52-mla.ts +0 -792
- package/src/model/glm52-moe.ts +0 -223
- package/src/model/glm52-pilot.ts +0 -537
- package/src/model/glm52-reference.ts +0 -445
- package/src/model/glm52-residency.ts +0 -557
- package/src/model/glm52-resident-weights.ts +0 -135
- package/src/model/glm52-streamed-experts.ts +0 -453
- package/src/model/glm52-streamed-metal.ts +0 -610
- package/src/model/glm52-weights.ts +0 -503
- package/src/model/glm52.ts +0 -1132
- package/src/model/implementation.ts +0 -69
- package/src/model/kv-attention-view.ts +0 -70
- package/src/model/minicpm5.ts +0 -376
- package/src/model/profile.ts +0 -468
- package/src/model/quantized-append-attention.ts +0 -31
- package/src/model/qwen-append.ts +0 -12
- package/src/model/qwen-conv.ts +0 -52
- package/src/model/qwen3-delta-state.ts +0 -74
- package/src/model/qwen3-delta.ts +0 -451
- package/src/model/qwen3-moe.ts +0 -404
- package/src/model/qwen3-mrope.ts +0 -250
- package/src/model/qwen3.ts +0 -277
- package/src/model/qwen3_5-checkpoint.ts +0 -164
- package/src/model/qwen3_5.ts +0 -881
- package/src/model/rotating-kv-donor.ts +0 -35
- package/src/model/rotating-kv-layout.ts +0 -262
- package/src/model/rotating-row-transaction.ts +0 -35
- package/src/model/speculative-rotating-kv.ts +0 -101
- package/src/model/ssm-prefill-padding.ts +0 -39
- package/src/model/support.ts +0 -110
- package/src/model/token-groups.ts +0 -66
- package/src/model/trellis-balanced-scatter.ts +0 -51
- package/src/model/trellis-linear.ts +0 -672
- package/src/model/trellis-shared-m.ts +0 -151
- package/src/model/trellis-shared-scatter.ts +0 -144
- package/src/model/trellis-splitk-prefill.ts +0 -158
- package/src/model/trellis-tiled-prefill.ts +0 -136
- package/src/model/trellis-vector-expand.ts +0 -62
- package/src/model/trellis-wide-prefill.ts +0 -94
- package/src/model/turboquant-codec.ts +0 -89
- package/src/model/universal/archs.ts +0 -332
- package/src/model/universal/dense.ts +0 -612
- package/src/model/universal/modules.ts +0 -170
- package/src/model/universal/rope.ts +0 -381
- package/src/model/whisper.ts +0 -435
- package/src/native/expert_io.c +0 -523
- package/src/native/frame_extract.swift +0 -140
- package/src/native/mic_capture.swift +0 -92
- package/src/native-pack.ts +0 -163
- package/src/pi-provider.ts +0 -53
- package/src/pi-session.ts +0 -81
- package/src/pi-terminal.ts +0 -274
- package/src/pi-web.ts +0 -1940
- package/src/preflight.ts +0 -158
- package/src/prompt-cache.ts +0 -682
- package/src/quantize/allocator.ts +0 -627
- package/src/quantize/atomic-output.ts +0 -38
- package/src/quantize/calibration.ts +0 -181
- package/src/quantize/config-writer.ts +0 -145
- package/src/quantize/index.ts +0 -84
- package/src/quantize/job.ts +0 -162
- package/src/quantize/quantizer.ts +0 -666
- package/src/quantize/rotate.ts +0 -603
- package/src/quantize/safetensors-writer.ts +0 -249
- package/src/quantize/sensitivity.ts +0 -216
- package/src/quantize/trellis.ts +0 -586
- package/src/quantize/weight-transform.ts +0 -292
- package/src/registry.ts +0 -602
- package/src/runtime-config.ts +0 -77
- package/src/safetensors.ts +0 -165
- package/src/sampler.ts +0 -1012
- package/src/serve/admin-routes.ts +0 -184
- package/src/serve/assets/curve-terrain.html +0 -105
- package/src/serve/assets/training-inference-map.html +0 -575
- package/src/serve/audio-routes.ts +0 -323
- package/src/serve/aux-routes.ts +0 -132
- package/src/serve/batch-scheduler.ts +0 -8
- package/src/serve/checkpoint-identity.ts +0 -64
- package/src/serve/generation-gateway.ts +0 -537
- package/src/serve/isolate.ts +0 -609
- package/src/serve/kv-budget.ts +0 -14
- package/src/serve/lab-routes.ts +0 -341
- package/src/serve/lane-registry.ts +0 -45
- package/src/serve/model-admin-routes.ts +0 -155
- package/src/serve/model-binding.ts +0 -49
- package/src/serve/model-host.ts +0 -511
- package/src/serve/parent-application.ts +0 -56
- package/src/serve/prompt-response-trace.ts +0 -161
- package/src/serve/response-lease.ts +0 -24
- package/src/serve/session-search.ts +0 -326
- package/src/serve/static-routes.ts +0 -101
- package/src/serve/transcription-server.ts +0 -64
- package/src/serve/transcription-service.ts +0 -502
- package/src/server.ts +0 -1282
- package/src/spec/assistant-rows.ts +0 -93
- package/src/spec/assistant-source.ts +0 -126
- package/src/spec/cache-identity.ts +0 -22
- package/src/spec/deepspec-rows.ts +0 -29
- package/src/spec/deepspec-source.ts +0 -145
- package/src/spec/dflash-source.ts +0 -149
- package/src/spec/drafter.ts +0 -304
- package/src/spec/dspark/ab-stats.ts +0 -136
- package/src/spec/dspark/calibration.ts +0 -112
- package/src/spec/dspark/data-dflash.ts +0 -156
- package/src/spec/dspark/data.ts +0 -201
- package/src/spec/dspark/deepspec-module.ts +0 -776
- package/src/spec/dspark/generate-dflash.ts +0 -245
- package/src/spec/dspark/generate.ts +0 -290
- package/src/spec/dspark/loader.ts +0 -23
- package/src/spec/dspark/loss.ts +0 -213
- package/src/spec/dspark/module-dflash.ts +0 -560
- package/src/spec/dspark/module.ts +0 -540
- package/src/spec/dspark/quantize-drafter.ts +0 -91
- package/src/spec/dspark/sample.ts +0 -112
- package/src/spec/generate.ts +0 -276
- package/src/spec/glm52-mtp-graph.ts +0 -29
- package/src/spec/glm52-mtp-rows.ts +0 -132
- package/src/spec/glm52-mtp-source.ts +0 -318
- package/src/spec/glm52-mtp-state.ts +0 -33
- package/src/spec/ngram-source.ts +0 -226
- package/src/spec/projected-draft-rows.ts +0 -73
- package/src/spec/qwen-mtp-module.ts +0 -171
- package/src/spec/qwen-mtp-rows.ts +0 -230
- package/src/spec/qwen-mtp-source.ts +0 -386
- package/src/spec/qwen-mtp-state.ts +0 -30
- package/src/spec/serve-loop.ts +0 -587
- package/src/spec/source.ts +0 -228
- package/src/spec/two-model-rows.ts +0 -62
- package/src/spec/two-model.ts +0 -113
- package/src/ssd-cache.ts +0 -431
- package/src/ssd-durability.ts +0 -209
- package/src/storage/host-buffer.ts +0 -24
- package/src/storage/kv-writer.ts +0 -83
- package/src/storage/kv-writer.worker.js +0 -264
- package/src/storage/retention-policy.ts +0 -37
- package/src/tiered-prompt-cache.ts +0 -45
- package/src/tokenizer.ts +0 -335
- package/src/tool-approvals.ts +0 -131
- package/src/tool-call.ts +0 -695
- package/src/train/dataset.ts +0 -484
- package/src/train/diffusion-lora.ts +0 -216
- package/src/train/export.ts +0 -32
- package/src/train/flash-cce.ts +0 -1234
- package/src/train/forward.ts +0 -266
- package/src/train/fuse.ts +0 -243
- package/src/train/index.ts +0 -46
- package/src/train/job.ts +0 -205
- package/src/train/lora-params.ts +0 -254
- package/src/train/loss.ts +0 -1591
- package/src/train/merge.ts +0 -195
- package/src/train/optimizer.ts +0 -183
- package/src/train/prefix-shared.ts +0 -490
- package/src/train/rank.ts +0 -137
- package/src/train/segmented.ts +0 -1828
- package/src/train/steel-qmm-header.ts +0 -1461
- package/src/train/trainer.ts +0 -1221
- package/src/train/watch.ts +0 -542
- package/src/vision/diffusion-vision.ts +0 -620
- package/src/vision/embedder.ts +0 -176
- package/src/vision/preprocess.ts +0 -247
- package/src/vision/prompt.ts +0 -409
- package/src/vision/qwen3vl-preprocess.ts +0 -409
- package/src/vision/qwen3vl-prompt.ts +0 -233
- package/src/vision/qwen3vl-tower.ts +0 -566
- package/src/vision/siglip.ts +0 -607
- package/src/vision/video-frames.ts +0 -148
- package/src/web/app.html +0 -2155
- package/src/web/app.js +0 -5863
- package/src/web/skills/memory/SKILL.md +0 -120
- package/src/web/skills/web-research/SKILL.md +0 -46
- package/src/web/skills.ts +0 -97
- package/src/web/src/adapters-panel.ts +0 -288
- package/src/web/src/assistant.ts +0 -451
- package/src/web/src/chat.ts +0 -1060
- package/src/web/src/composer.ts +0 -962
- package/src/web/src/dataset.ts +0 -132
- package/src/web/src/finetune.ts +0 -267
- package/src/web/src/hub.ts +0 -428
- package/src/web/src/main.ts +0 -98
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +0 -572
- package/src/web/src/model-picker.ts +0 -159
- package/src/web/src/palette.ts +0 -346
- package/src/web/src/protocol.ts +0 -26
- package/src/web/src/quantize.ts +0 -167
- package/src/web/src/rag.ts +0 -274
- package/src/web/src/sessions.ts +0 -469
- package/src/web/src/shell.ts +0 -804
- package/src/web/src/ui-catalog.ts +0 -78
- package/src/web/src/voice.ts +0 -141
- package/src/web/vendor/README.md +0 -61
- package/src/web-assets.ts +0 -47
- package/src/web-tools.ts +0 -478
- package/src/weights.ts +0 -255
- /package/src/{serve → server}/completion-sink.ts +0 -0
- /package/src/{serve → server}/generated-token-history.ts +0 -0
- /package/src/web/{icon.svg → public/icon.svg} +0 -0
- /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
- /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
package/src/memory/wikify.ts
DELETED
|
@@ -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
|
-
}
|