wiki-graph-core 0.3.1
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/LICENSE +201 -0
- package/data/editor/clue_reviewer.jinja +410 -0
- package/data/editor/clue_reviewer_generator.jinja +428 -0
- package/data/editor/revision_feedback.jinja +5 -0
- package/data/editor/text_compressor.jinja +127 -0
- package/data/guaranteed/response_intent_classifier.jinja +8 -0
- package/data/help/commands/archive/next.jinja +29 -0
- package/data/help/commands/gc.jinja +38 -0
- package/data/help/commands/legacy/migrate.jinja +20 -0
- package/data/help/commands/legacy.jinja +19 -0
- package/data/help/commands/maintenance/chapter/add.jinja +41 -0
- package/data/help/commands/maintenance/chapter/list.jinja +33 -0
- package/data/help/commands/maintenance/chapter/move.jinja +36 -0
- package/data/help/commands/maintenance/chapter/remove.jinja +26 -0
- package/data/help/commands/maintenance/chapter/reset.jinja +29 -0
- package/data/help/commands/maintenance/chapter/set-source.jinja +34 -0
- package/data/help/commands/maintenance/chapter/set-summary.jinja +31 -0
- package/data/help/commands/maintenance/chapter/set-title.jinja +29 -0
- package/data/help/commands/maintenance/chapter/tree.jinja +58 -0
- package/data/help/commands/maintenance/chapter.jinja +59 -0
- package/data/help/commands/maintenance/cover.jinja +28 -0
- package/data/help/commands/maintenance/meta.jinja +55 -0
- package/data/help/commands/predicate.jinja +460 -0
- package/data/help/commands/root.jinja +83 -0
- package/data/help/commands/transform.jinja +34 -0
- package/data/help/commands/uri.jinja +274 -0
- package/data/help/topics/config.jinja +88 -0
- package/data/help/topics/format.jinja +72 -0
- package/data/help/topics/readiness.jinja +99 -0
- package/data/help/topics/recipe.jinja +151 -0
- package/data/help/topics/runtime.jinja +62 -0
- package/data/help/topics/uri.jinja +178 -0
- package/data/output/epub/cover.xhtml.jinja +13 -0
- package/data/output/epub/nav.xhtml.jinja +12 -0
- package/data/output/epub/package.opf.xml.jinja +39 -0
- package/data/output/epub/section.xhtml.jinja +12 -0
- package/data/topologization/book_coherence_extraction.jinja +223 -0
- package/data/topologization/chunk_extraction.jinja +233 -0
- package/data/topologization/evidence_choice.jinja +38 -0
- package/data/topologization/translate_chunks.jinja +138 -0
- package/data/topologization/user_focused_extraction.jinja +378 -0
- package/dist/archive/query/archive-view.d.ts +26 -0
- package/dist/archive/query/archive-view.js +4540 -0
- package/dist/archive/query/archive-view.js.map +1 -0
- package/dist/archive/query/continuation-cursor.d.ts +61 -0
- package/dist/archive/query/continuation-cursor.js +363 -0
- package/dist/archive/query/continuation-cursor.js.map +1 -0
- package/dist/archive/query/index.d.ts +29 -0
- package/dist/archive/query/index.js +63 -0
- package/dist/archive/query/index.js.map +1 -0
- package/dist/archive/query/lexical-search.d.ts +22 -0
- package/dist/archive/query/lexical-search.js +137 -0
- package/dist/archive/query/lexical-search.js.map +1 -0
- package/dist/archive/query/search-cache.d.ts +192 -0
- package/dist/archive/query/search-cache.js +1248 -0
- package/dist/archive/query/search-cache.js.map +1 -0
- package/dist/archive/search-index/index.d.ts +26 -0
- package/dist/archive/search-index/index.js +29 -0
- package/dist/archive/search-index/index.js.map +1 -0
- package/dist/archive/search-index/search-index.d.ts +26 -0
- package/dist/archive/search-index/search-index.js +555 -0
- package/dist/archive/search-index/search-index.js.map +1 -0
- package/dist/archive/search-index/search-tokenizer.d.ts +16 -0
- package/dist/archive/search-index/search-tokenizer.js +225 -0
- package/dist/archive/search-index/search-tokenizer.js.map +1 -0
- package/dist/common/cli-command.d.ts +4 -0
- package/dist/common/cli-command.js +7 -0
- package/dist/common/cli-command.js.map +1 -0
- package/dist/common/data-dir.d.ts +3 -0
- package/dist/common/data-dir.js +48 -0
- package/dist/common/data-dir.js.map +1 -0
- package/dist/common/language.d.ts +55 -0
- package/dist/common/language.js +208 -0
- package/dist/common/language.js.map +1 -0
- package/dist/common/llm-scope.d.ts +19 -0
- package/dist/common/llm-scope.js +31 -0
- package/dist/common/llm-scope.js.map +1 -0
- package/dist/common/logging.d.ts +22 -0
- package/dist/common/logging.js +141 -0
- package/dist/common/logging.js.map +1 -0
- package/dist/common/template.d.ts +8 -0
- package/dist/common/template.js +54 -0
- package/dist/common/template.js.map +1 -0
- package/dist/common/tinyld-language.d.ts +6 -0
- package/dist/common/tinyld-language.js +34 -0
- package/dist/common/tinyld-language.js.map +1 -0
- package/dist/common/wiki-graph-dir.d.ts +10 -0
- package/dist/common/wiki-graph-dir.js +41 -0
- package/dist/common/wiki-graph-dir.js.map +1 -0
- package/dist/common/wiki-graph-temp.d.ts +6 -0
- package/dist/common/wiki-graph-temp.js +21 -0
- package/dist/common/wiki-graph-temp.js.map +1 -0
- package/dist/common/wiki-graph-uri.d.ts +28 -0
- package/dist/common/wiki-graph-uri.js +171 -0
- package/dist/common/wiki-graph-uri.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +13 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/task-context.d.ts +41 -0
- package/dist/context/task-context.js +111 -0
- package/dist/context/task-context.js.map +1 -0
- package/dist/document/database.d.ts +30 -0
- package/dist/document/database.js +273 -0
- package/dist/document/database.js.map +1 -0
- package/dist/document/document.d.ts +153 -0
- package/dist/document/document.js +615 -0
- package/dist/document/document.js.map +1 -0
- package/dist/document/fragments.d.ts +63 -0
- package/dist/document/fragments.js +371 -0
- package/dist/document/fragments.js.map +1 -0
- package/dist/document/index.d.ts +13 -0
- package/dist/document/index.js +62 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/schema.d.ts +8 -0
- package/dist/document/schema.js +472 -0
- package/dist/document/schema.js.map +1 -0
- package/dist/document/shared-state-database.d.ts +9 -0
- package/dist/document/shared-state-database.js +193 -0
- package/dist/document/shared-state-database.js.map +1 -0
- package/dist/document/stores.d.ts +203 -0
- package/dist/document/stores.js +1678 -0
- package/dist/document/stores.js.map +1 -0
- package/dist/document/text-streams.d.ts +86 -0
- package/dist/document/text-streams.js +515 -0
- package/dist/document/text-streams.js.map +1 -0
- package/dist/document/types.d.ts +148 -0
- package/dist/document/types.js +69 -0
- package/dist/document/types.js.map +1 -0
- package/dist/editor/clue.d.ts +27 -0
- package/dist/editor/clue.js +158 -0
- package/dist/editor/clue.js.map +1 -0
- package/dist/editor/compressor.d.ts +18 -0
- package/dist/editor/compressor.js +68 -0
- package/dist/editor/compressor.js.map +1 -0
- package/dist/editor/editor.d.ts +39 -0
- package/dist/editor/editor.js +253 -0
- package/dist/editor/editor.js.map +1 -0
- package/dist/editor/feedback.d.ts +16 -0
- package/dist/editor/feedback.js +104 -0
- package/dist/editor/feedback.js.map +1 -0
- package/dist/editor/index.d.ts +17 -0
- package/dist/editor/index.js +5 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/language-review.d.ts +14 -0
- package/dist/editor/language-review.js +35 -0
- package/dist/editor/language-review.js.map +1 -0
- package/dist/editor/log.d.ts +37 -0
- package/dist/editor/log.js +202 -0
- package/dist/editor/log.js.map +1 -0
- package/dist/editor/markup.d.ts +20 -0
- package/dist/editor/markup.js +294 -0
- package/dist/editor/markup.js.map +1 -0
- package/dist/editor/prompt-templates.d.ts +6 -0
- package/dist/editor/prompt-templates.js +11 -0
- package/dist/editor/prompt-templates.js.map +1 -0
- package/dist/editor/response.d.ts +5 -0
- package/dist/editor/response.js +31 -0
- package/dist/editor/response.js.map +1 -0
- package/dist/editor/review.d.ts +34 -0
- package/dist/editor/review.js +189 -0
- package/dist/editor/review.js.map +1 -0
- package/dist/editor/types.d.ts +31 -0
- package/dist/editor/types.js +21 -0
- package/dist/editor/types.js.map +1 -0
- package/dist/evidence-selection/anchor-resolver.d.ts +43 -0
- package/dist/evidence-selection/anchor-resolver.js +783 -0
- package/dist/evidence-selection/anchor-resolver.js.map +1 -0
- package/dist/evidence-selection/anchor-types.d.ts +27 -0
- package/dist/evidence-selection/anchor-types.js +1 -0
- package/dist/evidence-selection/anchor-types.js.map +1 -0
- package/dist/evidence-selection/index.d.ts +8 -0
- package/dist/evidence-selection/index.js +32 -0
- package/dist/evidence-selection/index.js.map +1 -0
- package/dist/evidence-selection/prompt.d.ts +18 -0
- package/dist/evidence-selection/prompt.js +54 -0
- package/dist/evidence-selection/prompt.js.map +1 -0
- package/dist/evidence-selection/quote-score.d.ts +29 -0
- package/dist/evidence-selection/quote-score.js +331 -0
- package/dist/evidence-selection/quote-score.js.map +1 -0
- package/dist/evidence-selection/selection-resolver.d.ts +19 -0
- package/dist/evidence-selection/selection-resolver.js +303 -0
- package/dist/evidence-selection/selection-resolver.js.map +1 -0
- package/dist/evidence-selection/text.d.ts +4 -0
- package/dist/evidence-selection/text.js +66 -0
- package/dist/evidence-selection/text.js.map +1 -0
- package/dist/evidence-selection/types.d.ts +36 -0
- package/dist/evidence-selection/types.js +1 -0
- package/dist/evidence-selection/types.js.map +1 -0
- package/dist/facade/app.d.ts +75 -0
- package/dist/facade/app.js +141 -0
- package/dist/facade/app.js.map +1 -0
- package/dist/facade/build-queue.d.ts +181 -0
- package/dist/facade/build-queue.js +1442 -0
- package/dist/facade/build-queue.js.map +1 -0
- package/dist/facade/chapter-build.d.ts +60 -0
- package/dist/facade/chapter-build.js +1020 -0
- package/dist/facade/chapter-build.js.map +1 -0
- package/dist/facade/chapter.d.ts +168 -0
- package/dist/facade/chapter.js +944 -0
- package/dist/facade/chapter.js.map +1 -0
- package/dist/facade/digest.d.ts +55 -0
- package/dist/facade/digest.js +168 -0
- package/dist/facade/digest.js.map +1 -0
- package/dist/facade/graph.d.ts +60 -0
- package/dist/facade/graph.js +203 -0
- package/dist/facade/graph.js.map +1 -0
- package/dist/facade/import.d.ts +49 -0
- package/dist/facade/import.js +262 -0
- package/dist/facade/import.js.map +1 -0
- package/dist/facade/index.d.ts +39 -0
- package/dist/facade/index.js +171 -0
- package/dist/facade/index.js.map +1 -0
- package/dist/facade/knowledge-graph-build.d.ts +75 -0
- package/dist/facade/knowledge-graph-build.js +897 -0
- package/dist/facade/knowledge-graph-build.js.map +1 -0
- package/dist/facade/llm-sampling.d.ts +11 -0
- package/dist/facade/llm-sampling.js +58 -0
- package/dist/facade/llm-sampling.js.map +1 -0
- package/dist/facade/types.d.ts +8 -0
- package/dist/facade/types.js +1 -0
- package/dist/facade/types.js.map +1 -0
- package/dist/facade/wiki-graph-archive.d.ts +41 -0
- package/dist/facade/wiki-graph-archive.js +130 -0
- package/dist/facade/wiki-graph-archive.js.map +1 -0
- package/dist/gc/files.d.ts +15 -0
- package/dist/gc/files.js +141 -0
- package/dist/gc/files.js.map +1 -0
- package/dist/gc/index.d.ts +2 -0
- package/dist/gc/index.js +5 -0
- package/dist/gc/index.js.map +1 -0
- package/dist/gc/lock.d.ts +3 -0
- package/dist/gc/lock.js +137 -0
- package/dist/gc/lock.js.map +1 -0
- package/dist/gc/runner.d.ts +9 -0
- package/dist/gc/runner.js +129 -0
- package/dist/gc/runner.js.map +1 -0
- package/dist/gc/tmp-gc.d.ts +5 -0
- package/dist/gc/tmp-gc.js +57 -0
- package/dist/gc/tmp-gc.js.map +1 -0
- package/dist/gc/types.d.ts +24 -0
- package/dist/gc/types.js +1 -0
- package/dist/gc/types.js.map +1 -0
- package/dist/gc.cjs +10041 -0
- package/dist/gc.cjs.map +1 -0
- package/dist/gc.d.cts +10 -0
- package/dist/gc.d.ts +2 -0
- package/dist/gc.js +5 -0
- package/dist/gc.js.map +1 -0
- package/dist/guaranteed/classifier.d.ts +10 -0
- package/dist/guaranteed/classifier.js +88 -0
- package/dist/guaranteed/classifier.js.map +1 -0
- package/dist/guaranteed/errors.d.ts +38 -0
- package/dist/guaranteed/errors.js +73 -0
- package/dist/guaranteed/errors.js.map +1 -0
- package/dist/guaranteed/index.d.ts +7 -0
- package/dist/guaranteed/index.js +21 -0
- package/dist/guaranteed/index.js.map +1 -0
- package/dist/guaranteed/request.d.ts +8 -0
- package/dist/guaranteed/request.js +206 -0
- package/dist/guaranteed/request.js.map +1 -0
- package/dist/guaranteed/response.d.ts +12 -0
- package/dist/guaranteed/response.js +98 -0
- package/dist/guaranteed/response.js.map +1 -0
- package/dist/guaranteed/types.d.ts +20 -0
- package/dist/guaranteed/types.js +1 -0
- package/dist/guaranteed/types.js.map +1 -0
- package/dist/index-B8NNnQ8P.d.ts +424 -0
- package/dist/index.cjs +34774 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2462 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy-sdpub/upgrade.d.ts +7 -0
- package/dist/legacy-sdpub/upgrade.js +795 -0
- package/dist/legacy-sdpub/upgrade.js.map +1 -0
- package/dist/llm/cache.d.ts +13 -0
- package/dist/llm/cache.js +35 -0
- package/dist/llm/cache.js.map +1 -0
- package/dist/llm/client.d.ts +26 -0
- package/dist/llm/client.js +572 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/context.d.ts +22 -0
- package/dist/llm/context.js +78 -0
- package/dist/llm/context.js.map +1 -0
- package/dist/llm/errors.d.ts +7 -0
- package/dist/llm/errors.js +12 -0
- package/dist/llm/errors.js.map +1 -0
- package/dist/llm/index.d.ts +7 -0
- package/dist/llm/index.js +17 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/request-log.d.ts +9 -0
- package/dist/llm/request-log.js +35 -0
- package/dist/llm/request-log.js.map +1 -0
- package/dist/llm/sampling.d.ts +11 -0
- package/dist/llm/sampling.js +67 -0
- package/dist/llm/sampling.js.map +1 -0
- package/dist/llm/types.d.ts +50 -0
- package/dist/llm/types.js +1 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/output/epub/archive.d.ts +9 -0
- package/dist/output/epub/archive.js +81 -0
- package/dist/output/epub/archive.js.map +1 -0
- package/dist/output/epub/book.d.ts +15 -0
- package/dist/output/epub/book.js +76 -0
- package/dist/output/epub/book.js.map +1 -0
- package/dist/output/epub/content.d.ts +9 -0
- package/dist/output/epub/content.js +35 -0
- package/dist/output/epub/content.js.map +1 -0
- package/dist/output/epub/index.d.ts +20 -0
- package/dist/output/epub/index.js +14 -0
- package/dist/output/epub/index.js.map +1 -0
- package/dist/output/epub/model.d.ts +24 -0
- package/dist/output/epub/model.js +1 -0
- package/dist/output/epub/model.js.map +1 -0
- package/dist/output/epub/navigation.d.ts +10 -0
- package/dist/output/epub/navigation.js +39 -0
- package/dist/output/epub/navigation.js.map +1 -0
- package/dist/output/epub/package.d.ts +17 -0
- package/dist/output/epub/package.js +29 -0
- package/dist/output/epub/package.js.map +1 -0
- package/dist/output/epub/shared.d.ts +4 -0
- package/dist/output/epub/shared.js +12 -0
- package/dist/output/epub/shared.js.map +1 -0
- package/dist/output/epub/templates.d.ts +35 -0
- package/dist/output/epub/templates.js +26 -0
- package/dist/output/epub/templates.js.map +1 -0
- package/dist/output/index.d.ts +14 -0
- package/dist/output/index.js +11 -0
- package/dist/output/index.js.map +1 -0
- package/dist/output/plain-text.d.ts +18 -0
- package/dist/output/plain-text.js +56 -0
- package/dist/output/plain-text.js.map +1 -0
- package/dist/progress/index.d.ts +3 -0
- package/dist/progress/index.js +14 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/progress/reporter.d.ts +10 -0
- package/dist/progress/reporter.js +79 -0
- package/dist/progress/reporter.js.map +1 -0
- package/dist/progress/tracker.d.ts +37 -0
- package/dist/progress/tracker.js +119 -0
- package/dist/progress/tracker.js.map +1 -0
- package/dist/progress/types.d.ts +28 -0
- package/dist/progress/types.js +1 -0
- package/dist/progress/types.js.map +1 -0
- package/dist/reader/attention/attention.d.ts +39 -0
- package/dist/reader/attention/attention.js +123 -0
- package/dist/reader/attention/attention.js.map +1 -0
- package/dist/reader/attention/index.d.ts +9 -0
- package/dist/reader/attention/index.js +5 -0
- package/dist/reader/attention/index.js.map +1 -0
- package/dist/reader/attention/wave-reflection.d.ts +21 -0
- package/dist/reader/attention/wave-reflection.js +153 -0
- package/dist/reader/attention/wave-reflection.js.map +1 -0
- package/dist/reader/attention/working-memory.d.ts +28 -0
- package/dist/reader/attention/working-memory.js +62 -0
- package/dist/reader/attention/working-memory.js.map +1 -0
- package/dist/reader/chunk-batch/extract.d.ts +13 -0
- package/dist/reader/chunk-batch/extract.js +14 -0
- package/dist/reader/chunk-batch/extract.js.map +1 -0
- package/dist/reader/chunk-batch/extractor.d.ts +17 -0
- package/dist/reader/chunk-batch/extractor.js +337 -0
- package/dist/reader/chunk-batch/extractor.js.map +1 -0
- package/dist/reader/chunk-batch/fragment-projection.d.ts +34 -0
- package/dist/reader/chunk-batch/fragment-projection.js +94 -0
- package/dist/reader/chunk-batch/fragment-projection.js.map +1 -0
- package/dist/reader/chunk-batch/index.d.ts +12 -0
- package/dist/reader/chunk-batch/index.js +13 -0
- package/dist/reader/chunk-batch/index.js.map +1 -0
- package/dist/reader/chunk-batch/language.d.ts +9 -0
- package/dist/reader/chunk-batch/language.js +23 -0
- package/dist/reader/chunk-batch/language.js.map +1 -0
- package/dist/reader/chunk-batch/parser.d.ts +94 -0
- package/dist/reader/chunk-batch/parser.js +640 -0
- package/dist/reader/chunk-batch/parser.js.map +1 -0
- package/dist/reader/chunk-batch/prompt-templates.d.ts +6 -0
- package/dist/reader/chunk-batch/prompt-templates.js +11 -0
- package/dist/reader/chunk-batch/prompt-templates.js.map +1 -0
- package/dist/reader/chunk-batch/types.d.ts +85 -0
- package/dist/reader/chunk-batch/types.js +1 -0
- package/dist/reader/chunk-batch/types.js.map +1 -0
- package/dist/reader/index.d.ts +13 -0
- package/dist/reader/index.js +7 -0
- package/dist/reader/index.js.map +1 -0
- package/dist/reader/reader.d.ts +37 -0
- package/dist/reader/reader.js +82 -0
- package/dist/reader/reader.js.map +1 -0
- package/dist/reader/segment/index.d.ts +2 -0
- package/dist/reader/segment/index.js +5 -0
- package/dist/reader/segment/index.js.map +1 -0
- package/dist/reader/segment/intl-segmenter.d.ts +5 -0
- package/dist/reader/segment/intl-segmenter.js +102 -0
- package/dist/reader/segment/intl-segmenter.js.map +1 -0
- package/dist/reader/segment/segment.d.ts +5 -0
- package/dist/reader/segment/segment.js +9 -0
- package/dist/reader/segment/segment.js.map +1 -0
- package/dist/reader/segment/types.d.ts +14 -0
- package/dist/reader/segment/types.js +1 -0
- package/dist/reader/segment/types.js.map +1 -0
- package/dist/reader/types.d.ts +29 -0
- package/dist/reader/types.js +1 -0
- package/dist/reader/types.js.map +1 -0
- package/dist/serial.d.ts +89 -0
- package/dist/serial.js +476 -0
- package/dist/serial.js.map +1 -0
- package/dist/source/adapter.d.ts +15 -0
- package/dist/source/adapter.js +1 -0
- package/dist/source/adapter.js.map +1 -0
- package/dist/source/epub/archive.d.ts +27 -0
- package/dist/source/epub/archive.js +189 -0
- package/dist/source/epub/archive.js.map +1 -0
- package/dist/source/epub/content.d.ts +22 -0
- package/dist/source/epub/content.js +183 -0
- package/dist/source/epub/content.js.map +1 -0
- package/dist/source/epub/document.d.ts +55 -0
- package/dist/source/epub/document.js +179 -0
- package/dist/source/epub/document.js.map +1 -0
- package/dist/source/epub/index.d.ts +10 -0
- package/dist/source/epub/index.js +6 -0
- package/dist/source/epub/index.js.map +1 -0
- package/dist/source/epub/navigation.d.ts +16 -0
- package/dist/source/epub/navigation.js +215 -0
- package/dist/source/epub/navigation.js.map +1 -0
- package/dist/source/epub/package.d.ts +33 -0
- package/dist/source/epub/package.js +237 -0
- package/dist/source/epub/package.js.map +1 -0
- package/dist/source/epub/xml.d.ts +16 -0
- package/dist/source/epub/xml.js +111 -0
- package/dist/source/epub/xml.js.map +1 -0
- package/dist/source/index.d.ts +12 -0
- package/dist/source/index.js +32 -0
- package/dist/source/index.js.map +1 -0
- package/dist/source/meta.d.ts +30 -0
- package/dist/source/meta.js +22 -0
- package/dist/source/meta.js.map +1 -0
- package/dist/source/plain-text.d.ts +16 -0
- package/dist/source/plain-text.js +86 -0
- package/dist/source/plain-text.js.map +1 -0
- package/dist/source/toc.d.ts +16 -0
- package/dist/source/toc.js +17 -0
- package/dist/source/toc.js.map +1 -0
- package/dist/source/types.d.ts +16 -0
- package/dist/source/types.js +1 -0
- package/dist/source/types.js.map +1 -0
- package/dist/topology/grouping.d.ts +15 -0
- package/dist/topology/grouping.js +18 -0
- package/dist/topology/grouping.js.map +1 -0
- package/dist/topology/index.d.ts +21 -0
- package/dist/topology/index.js +5 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/topology/resource-segmentation.d.ts +13 -0
- package/dist/topology/resource-segmentation.js +111 -0
- package/dist/topology/resource-segmentation.js.map +1 -0
- package/dist/topology/segment-incision.d.ts +18 -0
- package/dist/topology/segment-incision.js +197 -0
- package/dist/topology/segment-incision.js.map +1 -0
- package/dist/topology/snake-detector.d.ts +13 -0
- package/dist/topology/snake-detector.js +520 -0
- package/dist/topology/snake-detector.js.map +1 -0
- package/dist/topology/snake-graph-builder.d.ts +14 -0
- package/dist/topology/snake-graph-builder.js +76 -0
- package/dist/topology/snake-graph-builder.js.map +1 -0
- package/dist/topology/topology.d.ts +29 -0
- package/dist/topology/topology.js +348 -0
- package/dist/topology/topology.js.map +1 -0
- package/dist/topology/weights.d.ts +10 -0
- package/dist/topology/weights.js +84 -0
- package/dist/topology/weights.js.map +1 -0
- package/dist/types-FVIqiLC9.d.cts +24 -0
- package/dist/utils/async-semaphore.d.ts +7 -0
- package/dist/utils/async-semaphore.js +40 -0
- package/dist/utils/async-semaphore.js.map +1 -0
- package/dist/utils/enum.d.ts +5 -0
- package/dist/utils/enum.js +18 -0
- package/dist/utils/enum.js.map +1 -0
- package/dist/utils/hash.d.ts +3 -0
- package/dist/utils/hash.js +132 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/node-error.d.ts +4 -0
- package/dist/utils/node-error.js +59 -0
- package/dist/utils/node-error.js.map +1 -0
- package/dist/wikg/archive-uri.d.ts +1 -0
- package/dist/wikg/archive-uri.js +37 -0
- package/dist/wikg/archive-uri.js.map +1 -0
- package/dist/wikg/archive.d.ts +28 -0
- package/dist/wikg/archive.js +496 -0
- package/dist/wikg/archive.js.map +1 -0
- package/dist/wikg/index.d.ts +31 -0
- package/dist/wikg/index.js +60 -0
- package/dist/wikg/index.js.map +1 -0
- package/dist/wikg/wikg-coordinator.d.ts +45 -0
- package/dist/wikg/wikg-coordinator.js +1687 -0
- package/dist/wikg/wikg-coordinator.js.map +1 -0
- package/dist/wikg/wiki-graph-archive-file.d.ts +42 -0
- package/dist/wikg/wiki-graph-archive-file.js +78 -0
- package/dist/wikg/wiki-graph-archive-file.js.map +1 -0
- package/dist/wikilink/index.d.ts +10 -0
- package/dist/wikilink/index.js +7 -0
- package/dist/wikilink/index.js.map +1 -0
- package/dist/wikilink/relation-discovery.d.ts +31 -0
- package/dist/wikilink/relation-discovery.js +477 -0
- package/dist/wikilink/relation-discovery.js.map +1 -0
- package/dist/wikilink/types.d.ts +23 -0
- package/dist/wikilink/types.js +1 -0
- package/dist/wikilink/types.js.map +1 -0
- package/dist/wikilink/window.d.ts +7 -0
- package/dist/wikilink/window.js +53 -0
- package/dist/wikilink/window.js.map +1 -0
- package/dist/wikimatch/enrichment.d.ts +10 -0
- package/dist/wikimatch/enrichment.js +55 -0
- package/dist/wikimatch/enrichment.js.map +1 -0
- package/dist/wikimatch/index.d.ts +16 -0
- package/dist/wikimatch/index.js +66 -0
- package/dist/wikimatch/index.js.map +1 -0
- package/dist/wikimatch/option-narrowing.d.ts +17 -0
- package/dist/wikimatch/option-narrowing.js +208 -0
- package/dist/wikimatch/option-narrowing.js.map +1 -0
- package/dist/wikimatch/options.d.ts +14 -0
- package/dist/wikimatch/options.js +155 -0
- package/dist/wikimatch/options.js.map +1 -0
- package/dist/wikimatch/policy-judge.d.ts +17 -0
- package/dist/wikimatch/policy-judge.js +430 -0
- package/dist/wikimatch/policy-judge.js.map +1 -0
- package/dist/wikimatch/range-suppression.d.ts +9 -0
- package/dist/wikimatch/range-suppression.js +27 -0
- package/dist/wikimatch/range-suppression.js.map +1 -0
- package/dist/wikimatch/surface-screening.d.ts +16 -0
- package/dist/wikimatch/surface-screening.js +168 -0
- package/dist/wikimatch/surface-screening.js.map +1 -0
- package/dist/wikimatch/surface-window.d.ts +6 -0
- package/dist/wikimatch/surface-window.js +44 -0
- package/dist/wikimatch/surface-window.js.map +1 -0
- package/dist/wikimatch/types.d.ts +151 -0
- package/dist/wikimatch/types.js +1 -0
- package/dist/wikimatch/types.js.map +1 -0
- package/dist/wikimatch/wikispine.d.ts +43 -0
- package/dist/wikimatch/wikispine.js +399 -0
- package/dist/wikimatch/wikispine.js.map +1 -0
- package/dist/wikimatch/window.d.ts +6 -0
- package/dist/wikimatch/window.js +83 -0
- package/dist/wikimatch/window.js.map +1 -0
- package/dist/wikimatch/words.d.ts +16 -0
- package/dist/wikimatch/words.js +66 -0
- package/dist/wikimatch/words.js.map +1 -0
- package/dist/wikipage/cache.d.ts +18 -0
- package/dist/wikipage/cache.js +404 -0
- package/dist/wikipage/cache.js.map +1 -0
- package/dist/wikipage/fetch-log.d.ts +17 -0
- package/dist/wikipage/fetch-log.js +143 -0
- package/dist/wikipage/fetch-log.js.map +1 -0
- package/dist/wikipage/index.d.ts +14 -0
- package/dist/wikipage/index.js +17 -0
- package/dist/wikipage/index.js.map +1 -0
- package/dist/wikipage/normalizer.d.ts +14 -0
- package/dist/wikipage/normalizer.js +147 -0
- package/dist/wikipage/normalizer.js.map +1 -0
- package/dist/wikipage/rate-limiter.d.ts +13 -0
- package/dist/wikipage/rate-limiter.js +84 -0
- package/dist/wikipage/rate-limiter.js.map +1 -0
- package/dist/wikipage/resolver.d.ts +26 -0
- package/dist/wikipage/resolver.js +325 -0
- package/dist/wikipage/resolver.js.map +1 -0
- package/dist/wikipage/types.d.ts +96 -0
- package/dist/wikipage/types.js +1 -0
- package/dist/wikipage/types.js.map +1 -0
- package/dist/wikipage/wikimedia-client.d.ts +48 -0
- package/dist/wikipage/wikimedia-client.js +397 -0
- package/dist/wikipage/wikimedia-client.js.map +1 -0
- package/dist/worker-BgQ0Xl8T.d.cts +181 -0
- package/dist/worker.cjs +5259 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.cts +2 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +5 -0
- package/dist/worker.js.map +1 -0
- package/package.json +78 -0
package/dist/gc.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/common/wiki-graph-dir.ts","../src/common/wiki-graph-temp.ts","../src/document/database.ts","../src/utils/node-error.ts","../src/document/shared-state-database.ts","../src/utils/enum.ts","../src/document/types.ts","../src/document/text-streams.ts","../src/document/fragments.ts","../src/source/meta.ts","../src/source/toc.ts","../src/document/schema.ts","../src/utils/hash.ts","../src/document/stores.ts","../src/document/document.ts","../src/document/index.ts","../src/wikipage/cache.ts","../src/guaranteed/errors.ts","../src/guaranteed/classifier.ts","../src/guaranteed/response.ts","../src/guaranteed/request.ts","../src/guaranteed/index.ts","../src/wikipage/normalizer.ts","../src/wikipage/rate-limiter.ts","../src/common/logging.ts","../src/wikipage/fetch-log.ts","../src/wikipage/wikimedia-client.ts","../src/wikipage/resolver.ts","../src/wikipage/index.ts","../src/facade/graph.ts","../src/common/language.ts","../src/utils/async-semaphore.ts","../src/common/llm-scope.ts","../src/reader/attention/wave-reflection.ts","../src/reader/attention/working-memory.ts","../src/reader/attention/attention.ts","../src/evidence-selection/text.ts","../src/evidence-selection/anchor-resolver.ts","../src/evidence-selection/prompt.ts","../src/evidence-selection/quote-score.ts","../src/evidence-selection/selection-resolver.ts","../src/evidence-selection/index.ts","../src/reader/chunk-batch/parser.ts","../src/reader/chunk-batch/fragment-projection.ts","../src/common/tinyld-language.ts","../src/reader/chunk-batch/language.ts","../src/reader/chunk-batch/prompt-templates.ts","../src/reader/chunk-batch/extractor.ts","../src/reader/chunk-batch/extract.ts","../src/reader/segment/intl-segmenter.ts","../src/reader/segment/segment.ts","../src/reader/reader.ts","../src/reader/index.ts","../src/editor/clue.ts","../src/editor/prompt-templates.ts","../src/editor/compressor.ts","../src/editor/types.ts","../src/editor/feedback.ts","../src/editor/language-review.ts","../src/editor/log.ts","../src/editor/markup.ts","../src/editor/response.ts","../src/editor/review.ts","../src/editor/editor.ts","../src/editor/index.ts","../src/topology/segment-incision.ts","../src/topology/resource-segmentation.ts","../src/topology/grouping.ts","../src/topology/weights.ts","../src/topology/snake-graph-builder.ts","../src/topology/snake-detector.ts","../src/topology/topology.ts","../src/topology/index.ts","../src/serial.ts","../src/source/epub/archive.ts","../src/source/epub/content.ts","../src/source/epub/xml.ts","../src/source/epub/package.ts","../src/source/epub/navigation.ts","../src/source/epub/document.ts","../src/source/epub/index.ts","../src/source/plain-text.ts","../src/source/index.ts","../src/facade/chapter.ts","../src/archive/query/lexical-search.ts","../src/archive/query/search-cache.ts","../src/archive/search-index/search-tokenizer.ts","../src/archive/search-index/search-index.ts","../src/common/wiki-graph-uri.ts","../src/archive/query/archive-view.ts","../src/archive/query/continuation-cursor.ts","../src/archive/query/index.ts","../src/common/data-dir.ts","../src/common/template.ts","../src/llm/cache.ts","../src/llm/context.ts","../src/llm/errors.ts","../src/llm/request-log.ts","../src/llm/sampling.ts","../src/llm/client.ts","../src/llm/index.ts","../src/progress/reporter.ts","../src/progress/tracker.ts","../src/progress/index.ts","../src/facade/import.ts","../src/output/plain-text.ts","../src/output/epub/shared.ts","../src/output/epub/templates.ts","../src/output/epub/archive.ts","../src/output/epub/content.ts","../src/output/epub/navigation.ts","../src/output/epub/package.ts","../src/output/epub/book.ts","../src/output/epub/index.ts","../src/output/index.ts","../src/wikg/archive.ts","../src/wikg/archive-uri.ts","../src/archive/search-index/index.ts","../src/gc/files.ts","../src/wikg/wikg-coordinator.ts","../src/wikg/wiki-graph-archive-file.ts","../src/wikg/index.ts","../src/facade/wiki-graph-archive.ts","../src/facade/digest.ts","../src/facade/llm-sampling.ts","../src/facade/app.ts","../src/facade/build-queue.ts","../src/facade/chapter-build.ts","../src/wikimatch/options.ts","../src/wikimatch/policy-judge.ts","../src/wikimatch/option-narrowing.ts","../src/wikimatch/enrichment.ts","../src/wikimatch/surface-screening.ts","../src/wikimatch/range-suppression.ts","../src/wikimatch/surface-window.ts","../src/wikimatch/wikispine.ts","../src/wikimatch/words.ts","../src/wikimatch/window.ts","../src/wikimatch/index.ts","../src/reader/chunk-batch/index.ts","../src/wikilink/relation-discovery.ts","../src/wikilink/window.ts","../src/wikilink/index.ts","../src/facade/knowledge-graph-build.ts","../src/facade/index.ts","../src/gc/lock.ts","../src/gc/tmp-gc.ts","../src/gc/runner.ts","../src/gc/index.ts","../src/gc.ts"],"sourcesContent":["import { homedir } from \"os\";\nimport { join, resolve } from \"path\";\n\nexport function resolveWikiGraphHomeDirectoryPath(): string {\n const localDataRootPath = process.env.WIKIGRAPH_STATE_DIR;\n\n if (localDataRootPath !== undefined && localDataRootPath.trim() !== \"\") {\n return resolve(localDataRootPath);\n }\n\n return join(homedir(), \".wikigraph\");\n}\n\nexport function resolveWikiGraphCoreDatabasePath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"core.sqlite\");\n}\n\nexport function resolveWikiGraphCacheDirectoryPath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"cache\");\n}\n\nexport function resolveWikiGraphCacheDatabasePath(): string {\n return join(resolveWikiGraphCacheDirectoryPath(), \"cache.sqlite\");\n}\n\nexport function resolveWikiGraphJobsDirectoryPath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"jobs\");\n}\n\nexport function resolveWikiGraphStagingDirectoryPath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"staging\");\n}\n\nexport function resolveWikiGraphTempRootDirectoryPath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"tmp\");\n}\n\nexport function resolveWikiGraphLogsDirectoryPath(): string {\n return join(resolveWikiGraphHomeDirectoryPath(), \"logs\");\n}\n","import { mkdir, mkdtemp } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphTempRootDirectoryPath } from \"./wiki-graph-dir.js\";\n\nexport type WikiGraphTempCategory =\n | \"archive-open\"\n | \"archive-write\"\n | \"cli-output\"\n | \"sdpub-upgrade\"\n | \"stdin-create\"\n | \"url-create\";\n\nexport function resolveWikiGraphStateRootPath(): string {\n return resolveWikiGraphTempRootDirectoryPath();\n}\n\nexport function resolveWikiGraphTempDirectoryPath(\n category?: WikiGraphTempCategory,\n): string {\n const rootPath = resolveWikiGraphStateRootPath();\n\n return category === undefined ? rootPath : join(rootPath, category);\n}\n\nexport async function createWikiGraphTempDirectory(\n category: WikiGraphTempCategory,\n): Promise<string> {\n const rootPath = resolveWikiGraphTempDirectoryPath(category);\n\n await mkdir(rootPath, { recursive: true });\n return await mkdtemp(join(rootPath, `${category}-`));\n}\n","import { AsyncLocalStorage } from \"async_hooks\";\nimport { stat } from \"fs/promises\";\nimport { resolve } from \"path\";\n\nimport type * as Sqlite3Namespace from \"sqlite3\";\n\ntype Sqlite3Module = typeof Sqlite3Namespace;\ntype SqliteDatabase = Sqlite3Namespace.Database;\nexport type SqlBindValue = Buffer | Uint8Array | number | string | null;\ntype SqlBindParams = readonly SqlBindValue[];\ntype SqlRowValue = SqlBindValue;\n\nexport type SqlRow = Record<string, SqlRowValue>;\n\nconst SQLITE_BUSY_TIMEOUT_MS = 15 * 60 * 1000;\n\ntype DatabaseOperationScope = symbol;\n\nasync function isMissingOrEmptyFile(path: string): Promise<boolean> {\n const stats = await stat(path).catch((error: unknown) => {\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n return undefined;\n }\n\n throw error;\n });\n\n return stats === undefined || stats.size === 0;\n}\n\nexport class Database {\n readonly #database: SqliteDatabase;\n readonly #onWrite: (() => void) | undefined;\n readonly #operationScope = new AsyncLocalStorage<DatabaseOperationScope>();\n #activeTransactionScope: DatabaseOperationScope | undefined;\n #closed = false;\n #operationChain: Promise<void> = Promise.resolve();\n #transactionDepth = 0;\n\n public constructor(\n database: SqliteDatabase,\n options: { readonly onWrite?: () => void } = {},\n ) {\n this.#database = database;\n this.#onWrite = options.onWrite;\n }\n\n public static async open(\n databasePath: string,\n schemaSql = \"\",\n options: {\n readonly onWrite?: () => void;\n readonly readonly?: boolean;\n } = {},\n ): Promise<Database> {\n const resolvedDatabasePath = resolve(databasePath);\n const shouldMarkSchemaWritten =\n options.readonly !== true &&\n schemaSql.trim() !== \"\" &&\n (await isMissingOrEmptyFile(resolvedDatabasePath));\n const database = await openSqliteDatabase(resolvedDatabasePath, options);\n const openedDatabase = new Database(database, options);\n\n await openedDatabase.#executeSql(\n `PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`,\n );\n if (options.readonly !== true && schemaSql.trim() !== \"\") {\n await openedDatabase.#executeSql(schemaSql);\n if (shouldMarkSchemaWritten) {\n openedDatabase.#markWritten();\n }\n }\n\n return openedDatabase;\n }\n\n public static async initialize(\n databasePath: string,\n schemaSql: string,\n ): Promise<void> {\n const database = await Database.open(databasePath);\n\n try {\n if (schemaSql.trim() !== \"\") {\n await database.#executeSql(schemaSql);\n }\n } finally {\n await database.close();\n }\n }\n\n public async queryAll<T>(\n sql: string,\n params: SqlBindParams | undefined,\n mapRow: (row: SqlRow) => T,\n ): Promise<T[]> {\n return await this.#runSerialized(async () => {\n this.#assertOpen();\n const rows = await this.#queryAllRows(sql, params);\n\n return rows.map(mapRow);\n });\n }\n\n public async queryOne<T>(\n sql: string,\n params: SqlBindParams | undefined,\n mapRow: (row: SqlRow) => T,\n ): Promise<T | undefined> {\n return await this.#runSerialized(async () => {\n this.#assertOpen();\n const row = await this.#queryOneRow(sql, params);\n\n return row === undefined ? undefined : mapRow(row);\n });\n }\n\n public async run(sql: string, params?: SqlBindParams): Promise<void> {\n await this.#runSerialized(async () => {\n this.#assertOpen();\n await this.#runStatement(sql, params);\n this.#markWritten();\n });\n }\n\n public async transaction<T>(operation: () => Promise<T> | T): Promise<T> {\n return await this.#runSerialized(async () => {\n this.#assertOpen();\n const isRootTransaction = this.#transactionDepth === 0;\n const transactionScope =\n this.#activeTransactionScope ?? Symbol(\"database transaction scope\");\n\n if (isRootTransaction) {\n await this.#executeSql(\"BEGIN IMMEDIATE\");\n this.#activeTransactionScope = transactionScope;\n }\n\n this.#transactionDepth += 1;\n\n try {\n const result = await this.#operationScope.run(\n transactionScope,\n operation,\n );\n\n if (isRootTransaction) {\n await this.#executeSql(\"COMMIT\");\n }\n\n return result;\n } catch (error) {\n if (isRootTransaction) {\n await this.#executeSql(\"ROLLBACK\");\n }\n\n throw error;\n } finally {\n this.#transactionDepth -= 1;\n if (isRootTransaction) {\n this.#activeTransactionScope = undefined;\n }\n }\n });\n }\n\n public async flush(): Promise<void> {\n await this.#runSerialized(() => {\n this.#assertOpen();\n });\n }\n\n public async close(): Promise<void> {\n await this.#runSerialized(async () => {\n if (this.#closed) {\n return;\n }\n\n await this.#closeDatabase();\n this.#closed = true;\n });\n }\n\n public async getLastInsertRowId(): Promise<number> {\n const row = await this.queryOne(\n \"SELECT last_insert_rowid() AS row_id\",\n undefined,\n (value) => getNumber(value, \"row_id\"),\n );\n\n if (row === undefined) {\n throw new Error(\"Could not read last_insert_rowid()\");\n }\n\n return row;\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new Error(\"Database is already closed\");\n }\n }\n\n async #runSerialized<T>(operation: () => Promise<T> | T): Promise<T> {\n const operationScope = this.#operationScope.getStore();\n\n if (\n operationScope !== undefined &&\n operationScope === this.#activeTransactionScope\n ) {\n return await operation();\n }\n\n const queuedOperation = this.#operationChain.then(operation);\n\n this.#operationChain = queuedOperation.then(\n () => undefined,\n () => undefined,\n );\n\n return await queuedOperation;\n }\n\n #markWritten(): void {\n this.#onWrite?.();\n }\n\n async #closeDatabase(): Promise<void> {\n await new Promise<void>((resolveClose, rejectClose) => {\n this.#database.close((error) => {\n if (error !== null) {\n rejectClose(error);\n return;\n }\n\n resolveClose();\n });\n });\n }\n\n async #executeSql(sql: string): Promise<void> {\n await new Promise<void>((resolveExec, rejectExec) => {\n this.#database.exec(sql, (error) => {\n if (error !== null) {\n rejectExec(error);\n return;\n }\n\n resolveExec();\n });\n });\n }\n\n async #queryAllRows(\n sql: string,\n params: SqlBindParams | undefined,\n ): Promise<SqlRow[]> {\n return await new Promise<SqlRow[]>((resolveAll, rejectAll) => {\n this.#database.all<SqlRow>(\n sql,\n normalizeSqlBindParams(params),\n (error, rows) => {\n if (error !== null) {\n rejectAll(error);\n return;\n }\n\n resolveAll(rows);\n },\n );\n });\n }\n\n async #queryOneRow(\n sql: string,\n params: SqlBindParams | undefined,\n ): Promise<SqlRow | undefined> {\n return await new Promise<SqlRow | undefined>((resolveGet, rejectGet) => {\n this.#database.get<SqlRow>(\n sql,\n normalizeSqlBindParams(params),\n (error, row) => {\n if (error !== null) {\n rejectGet(error);\n return;\n }\n\n resolveGet(row);\n },\n );\n });\n }\n\n async #runStatement(\n sql: string,\n params: SqlBindParams | undefined,\n ): Promise<void> {\n await new Promise<void>((resolveRun, rejectRun) => {\n this.#database.run(sql, normalizeSqlBindParams(params), (error) => {\n if (error !== null) {\n rejectRun(error);\n return;\n }\n\n resolveRun();\n });\n });\n }\n}\n\nexport function getNumber(row: SqlRow, key: string): number {\n const value = row[key];\n\n if (typeof value !== \"number\") {\n throw new TypeError(`Expected ${key} to be a number`);\n }\n\n return value;\n}\n\nexport function getString(row: SqlRow, key: string): string {\n const value = row[key];\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n\nexport function getOptionalString(\n row: SqlRow,\n key: string,\n): string | undefined {\n const value = row[key];\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n\nasync function openSqliteDatabase(\n databasePath: string,\n options: { readonly readonly?: boolean } = {},\n): Promise<SqliteDatabase> {\n const sqlite3 = await loadSqlite3();\n const flags =\n (options.readonly === true\n ? sqlite3.OPEN_READONLY\n : sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE) | sqlite3.OPEN_FULLMUTEX;\n\n return await new Promise<SqliteDatabase>((resolveOpen, rejectOpen) => {\n const database = new sqlite3.Database(databasePath, flags, (error) => {\n if (error !== null) {\n rejectOpen(error);\n return;\n }\n\n resolveOpen(database);\n });\n });\n}\n\nasync function loadSqlite3(): Promise<Sqlite3Module> {\n const module = await import(\"sqlite3\");\n\n return resolveSqlite3Module(module as unknown);\n}\n\nfunction resolveSqlite3Module(module: unknown): Sqlite3Module {\n if (\n typeof module === \"object\" &&\n module !== null &&\n \"default\" in module &&\n typeof module.default === \"object\" &&\n module.default !== null &&\n \"Database\" in module.default\n ) {\n return module.default as Sqlite3Module;\n }\n\n if (typeof module === \"object\" && module !== null && \"Database\" in module) {\n return module as Sqlite3Module;\n }\n\n throw new TypeError(\"Could not load sqlite3\");\n}\n\nfunction normalizeSqlBindParams(\n params: SqlBindParams | undefined,\n): SqlBindParams {\n return params ?? [];\n}\n","export function isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error;\n}\n\nexport function formatError(error: unknown): string {\n const messages: string[] = [];\n const visited = new Set<unknown>();\n let current: unknown = error;\n\n while (current !== undefined && !visited.has(current)) {\n visited.add(current);\n\n if (current instanceof Error) {\n pushErrorMessage(messages, describeError(current));\n current = current.cause;\n continue;\n }\n\n pushErrorMessage(messages, String(current as unknown));\n break;\n }\n\n return messages.join(\": \");\n}\n\nfunction describeError(error: Error): string {\n const errnoError = error as NodeJS.ErrnoException;\n const message = error.message.trim();\n const code =\n typeof errnoError.code === \"string\" && errnoError.code !== \"\"\n ? errnoError.code\n : undefined;\n const path =\n typeof errnoError.path === \"string\" && errnoError.path !== \"\"\n ? errnoError.path\n : undefined;\n\n if (code === \"ENOENT\") {\n return path === undefined\n ? \"File not found (ENOENT)\"\n : `File not found: ${path} (ENOENT)`;\n }\n\n if (code === \"EACCES\" || code === \"EPERM\") {\n return path === undefined\n ? `Permission denied (${code})`\n : `Permission denied: ${path} (${code})`;\n }\n\n if (message === \"\") {\n return code === undefined ? error.name : `${error.name} (${code})`;\n }\n\n return code === undefined ? message : `${message} (${code})`;\n}\n\nfunction pushErrorMessage(messages: string[], message: string): void {\n const normalizedMessage = message.trim();\n\n if (normalizedMessage === \"\") {\n return;\n }\n\n const lastMessage = messages.at(-1);\n\n if (lastMessage === undefined) {\n messages.push(normalizedMessage);\n return;\n }\n\n if (\n lastMessage === normalizedMessage ||\n lastMessage.includes(normalizedMessage)\n ) {\n return;\n }\n\n if (normalizedMessage.includes(lastMessage)) {\n messages[messages.length - 1] = normalizedMessage;\n return;\n }\n\n messages.push(normalizedMessage);\n}\n","import { createHash } from \"crypto\";\nimport {\n chmod,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from \"fs/promises\";\nimport { dirname, resolve } from \"path\";\nimport { setTimeout as sleep } from \"timers/promises\";\n\nimport { isNodeError } from \"../utils/node-error.js\";\n\nimport { Database } from \"./database.js\";\n\nconst INIT_LOCK_RETRY_MS = 50;\nconst INIT_LOCK_STALE_MS = 5 * 60 * 1000;\nconst INIT_LOCK_HEARTBEAT_MS = Math.floor(INIT_LOCK_STALE_MS / 2);\n\ninterface InitLockOwner {\n readonly at: number;\n readonly pid: number;\n}\n\nexport async function openSharedStateDatabase(\n databasePath: string,\n schemaSql: string,\n options: { readonly readonly?: boolean } = {},\n): Promise<Database> {\n await ensureSharedStateDatabaseInitialized(databasePath, schemaSql);\n\n return await Database.open(databasePath, \"\", options);\n}\n\nexport async function ensureSharedStateDatabaseInitialized(\n databasePath: string,\n schemaSql: string,\n): Promise<void> {\n const resolvedDatabasePath = resolve(databasePath);\n const markerPath = createInitMarkerPath(resolvedDatabasePath);\n const schemaHash = hashSchema(schemaSql);\n\n if (await hasInitMarker(markerPath, schemaHash)) {\n await hardenSharedStateFile(resolvedDatabasePath);\n await hardenSharedStateFile(markerPath);\n return;\n }\n\n await mkdir(dirname(resolvedDatabasePath), { recursive: true });\n await withInitLock(resolvedDatabasePath, async () => {\n if (await hasInitMarker(markerPath, schemaHash)) {\n await hardenSharedStateFile(resolvedDatabasePath);\n await hardenSharedStateFile(markerPath);\n return;\n }\n\n await Database.initialize(resolvedDatabasePath, schemaSql);\n await hardenSharedStateFile(resolvedDatabasePath);\n await writeInitMarker(markerPath, schemaHash);\n });\n await hardenSharedStateFile(resolvedDatabasePath);\n await hardenSharedStateFile(markerPath);\n}\n\nasync function writeInitMarker(\n markerPath: string,\n schemaHash: string,\n): Promise<void> {\n const tempPath = `${markerPath}.${process.pid}.tmp`;\n\n await writeFile(tempPath, `${schemaHash}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n await rename(tempPath, markerPath);\n await hardenSharedStateFile(markerPath);\n}\n\nasync function withInitLock<T>(\n databasePath: string,\n operation: () => Promise<T>,\n): Promise<T> {\n const lockPath = `${databasePath}.init.lock`;\n\n while (true) {\n try {\n await mkdir(lockPath, { mode: 0o700 });\n break;\n } catch (error) {\n if (!isNodeError(error) || error.code !== \"EEXIST\") {\n throw error;\n }\n\n await removeStaleInitLock(lockPath);\n await sleep(INIT_LOCK_RETRY_MS);\n }\n }\n\n try {\n await writeInitLockOwner(lockPath);\n const heartbeat = setInterval(() => {\n void writeInitLockOwner(lockPath).catch(() => undefined);\n }, INIT_LOCK_HEARTBEAT_MS);\n\n try {\n return await operation();\n } finally {\n clearInterval(heartbeat);\n }\n } finally {\n await rm(lockPath, { force: true, recursive: true });\n }\n}\n\nasync function writeInitLockOwner(lockPath: string): Promise<void> {\n await writeFile(\n `${lockPath}/owner.json`,\n `${JSON.stringify(\n {\n at: Date.now(),\n pid: process.pid,\n },\n null,\n 2,\n )}\\n`,\n {\n encoding: \"utf8\",\n mode: 0o600,\n },\n );\n}\n\nasync function removeStaleInitLock(lockPath: string): Promise<void> {\n try {\n const owner = await readInitLockOwner(lockPath);\n\n if (owner !== undefined) {\n if (isProcessAlive(owner.pid)) {\n return;\n }\n\n if (Date.now() - owner.at < INIT_LOCK_STALE_MS) {\n return;\n }\n } else if (!(await isPathOlderThan(lockPath, INIT_LOCK_STALE_MS))) {\n return;\n }\n\n await rm(lockPath, { force: true, recursive: true });\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n}\n\nasync function readInitLockOwner(\n lockPath: string,\n): Promise<InitLockOwner | undefined> {\n try {\n const parsed = JSON.parse(\n await readFile(`${lockPath}/owner.json`, \"utf8\"),\n ) as unknown;\n\n if (isInitLockOwner(parsed)) {\n return parsed;\n }\n\n return undefined;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n if (error instanceof SyntaxError) {\n return undefined;\n }\n\n throw error;\n }\n}\n\nfunction isInitLockOwner(value: unknown): value is InitLockOwner {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n typeof record.pid === \"number\" &&\n Number.isInteger(record.pid) &&\n record.pid > 0 &&\n typeof record.at === \"number\"\n );\n}\n\nasync function isPathOlderThan(path: string, ms: number): Promise<boolean> {\n const pathStat = await stat(path);\n\n return Date.now() - pathStat.mtimeMs >= ms;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ESRCH\") {\n return false;\n }\n\n return true;\n }\n}\n\nasync function hasInitMarker(\n markerPath: string,\n schemaHash: string,\n): Promise<boolean> {\n try {\n return (await readFile(markerPath, \"utf8\")).trim() === schemaHash;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return false;\n }\n\n throw error;\n }\n}\n\nfunction createInitMarkerPath(databasePath: string): string {\n return `${databasePath}.initialized`;\n}\n\nasync function hardenSharedStateFile(path: string): Promise<void> {\n try {\n await chmod(path, 0o600);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n}\n\nfunction hashSchema(schemaSql: string): string {\n return createHash(\"sha256\").update(schemaSql).digest(\"hex\");\n}\n","type EnumLike = Record<string, string>;\n\nexport function createEnumValueGuard<TEnum extends EnumLike>(enumType: TEnum) {\n const valueSet = new Set<string>(Object.values(enumType));\n\n return (value: string): value is TEnum[keyof TEnum] => valueSet.has(value);\n}\n\nexport function createEnumValueAsserter<TEnum extends EnumLike>(\n enumType: TEnum,\n label: string,\n) {\n const isEnumValue = createEnumValueGuard(enumType);\n\n return (value: string): TEnum[keyof TEnum] => {\n if (isEnumValue(value)) {\n return value;\n }\n\n throw new Error(`Unknown ${label}: ${value}`);\n };\n}\n","import {\n createEnumValueAsserter,\n createEnumValueGuard,\n} from \"../utils/enum.js\";\n\nexport enum ChunkRetention {\n Verbatim = \"verbatim\",\n Detailed = \"detailed\",\n Focused = \"focused\",\n Relevant = \"relevant\",\n}\n\nexport const isChunkRetention = createEnumValueGuard(ChunkRetention);\nexport const expectChunkRetention = createEnumValueAsserter(\n ChunkRetention,\n \"chunk retention\",\n);\n\nexport enum ChunkImportance {\n Critical = \"critical\",\n Important = \"important\",\n Helpful = \"helpful\",\n}\n\nexport const isChunkImportance = createEnumValueGuard(ChunkImportance);\nexport const expectChunkImportance = createEnumValueAsserter(\n ChunkImportance,\n \"chunk importance\",\n);\n\nexport type SentenceId = readonly [chapterId: number, sentenceIndex: number];\n\nexport interface SentenceRecord {\n readonly text: string;\n readonly wordsCount: number;\n}\n\nexport class Sentence implements SentenceRecord {\n #text: string | undefined;\n public readonly rawText: string;\n public readonly wordsCount: number;\n\n public constructor(rawText: string, wordsCount: number) {\n this.rawText = rawText;\n this.wordsCount = wordsCount;\n }\n\n public get text(): string {\n this.#text ??= normalizeSentenceText(this.rawText);\n\n return this.#text;\n }\n\n public toJSON(): SentenceRecord {\n return {\n text: this.text,\n wordsCount: this.wordsCount,\n };\n }\n}\n\nexport interface FragmentRecord {\n readonly serialId: number;\n readonly fragmentId: number;\n readonly summary: string;\n readonly sentences: readonly SentenceRecord[];\n}\n\nexport interface SerialRecord {\n readonly documentOrder: number;\n readonly knowledgeGraphReady: boolean;\n readonly knowledgeGraphParameterHash?: string;\n readonly id: number;\n readonly revision: number;\n readonly topologyParameterHash?: string;\n readonly topologyReady: boolean;\n}\n\nexport interface GraphBuildParameterRecord {\n readonly createdAt: string;\n readonly hash: string;\n readonly language?: string;\n readonly prompt: string;\n}\n\nexport interface ChunkRecord {\n readonly id: number;\n readonly generation: number;\n readonly sentenceId: SentenceId;\n readonly label: string;\n readonly content: string;\n readonly sentenceIds: readonly SentenceId[];\n readonly retention?: ChunkRetention;\n readonly importance?: ChunkImportance;\n readonly wordsCount: number;\n readonly weight: number;\n}\n\nexport type CreateChunkRecord = Omit<ChunkRecord, \"id\">;\n\nexport interface ReadingEdgeRecord {\n readonly fromId: number;\n readonly toId: number;\n readonly strength?: string;\n readonly weight: number;\n}\n\nexport interface MentionRecord {\n readonly id: string;\n readonly chapterId: number;\n readonly fragmentId?: number;\n readonly sentenceIndex?: number;\n readonly rangeStart: number;\n readonly rangeEnd: number;\n readonly surface: string;\n readonly qid: string;\n readonly confidence?: number;\n readonly note?: string;\n}\n\nexport interface MentionLinkRecord {\n readonly id: string;\n readonly sourceMentionId: string;\n readonly targetMentionId: string;\n readonly predicate: string;\n readonly evidenceSentenceIds: readonly SentenceId[];\n readonly confidence?: number;\n readonly note?: string;\n}\n\nexport interface SnakeRecord {\n readonly id: number;\n readonly serialId: number;\n readonly groupId: number;\n readonly localSnakeId: number;\n readonly size: number;\n readonly firstLabel: string;\n readonly lastLabel: string;\n readonly wordsCount: number;\n readonly weight: number;\n}\n\nexport interface CreateSnakeRecord {\n readonly serialId: number;\n readonly groupId: number;\n readonly localSnakeId: number;\n readonly size: number;\n readonly firstLabel: string;\n readonly lastLabel: string;\n readonly wordsCount?: number;\n readonly weight?: number;\n}\n\nexport interface SnakeChunkRecord {\n readonly snakeId: number;\n readonly chunkId: number;\n readonly position: number;\n}\n\nexport interface SnakeEdgeRecord {\n readonly fromSnakeId: number;\n readonly toSnakeId: number;\n readonly weight: number;\n}\n\nfunction normalizeSentenceText(text: string): string {\n return text.replace(/\\s+/gu, \" \").trim();\n}\n\nexport interface SentenceGroupRecord {\n readonly serialId: number;\n readonly groupId: number;\n readonly startSentenceIndex: number;\n readonly endSentenceIndex: number;\n readonly fragmentId?: number;\n}\n\nexport type FragmentGroupRecord = SentenceGroupRecord;\n\nexport const enum ObjectMetadataKind {\n Archive = 1,\n Chapter = 2,\n Chunk = 3,\n Entity = 4,\n Triple = 5,\n Object = 6,\n}\n\nexport interface ObjectMetadataTarget {\n readonly kind: ObjectMetadataKind;\n readonly objectPath: string;\n readonly chapterId?: number;\n readonly chunkId?: number;\n readonly entityQid?: string;\n readonly tripleSubjectQid?: string;\n readonly triplePredicate?: string;\n readonly tripleObjectQid?: string;\n}\n","import { mkdir, readFile, readdir, rm, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\n\nimport { isNodeError } from \"../utils/node-error.js\";\nimport type { Database } from \"./database.js\";\nimport {\n Sentence,\n type FragmentRecord,\n type SentenceId,\n type SentenceRecord,\n} from \"./types.js\";\n\nconst DEFAULT_FRAGMENT_WORDS_COUNT = 600;\nconst TEXT_STREAM_KIND = {\n source: 1,\n summary: 2,\n} as const;\n\ntype TextStreamName = keyof typeof TEXT_STREAM_KIND;\n\ninterface TextSentenceLocation {\n readonly byteLength: number;\n readonly byteOffset: number;\n readonly sentenceIndex: number;\n readonly wordsCount: number;\n}\n\ninterface TextStreamDraftState {\n draftOpen: boolean;\n nextSentenceIndex?: number;\n}\n\ninterface TextStreamFileAccess {\n deleteTree(path: string): Promise<void>;\n ensureDirectory(path: string): Promise<void>;\n listFiles(path: string): Promise<readonly string[]>;\n readFile(path: string): Promise<Uint8Array | undefined>;\n writeFile(\n path: string,\n content: string | Uint8Array,\n options: { readonly overwrite?: boolean },\n ): Promise<void>;\n}\n\ninterface TextStreamSentenceSegmenter {\n pipe(stream: Iterable<string>): AsyncIterable<{\n readonly offset: number;\n readonly text: string;\n readonly wordsCount: number;\n }>;\n}\n\ninterface WriteTextStreamOptions {\n readonly segmenter?: TextStreamSentenceSegmenter;\n}\n\nconst DEFAULT_FILE_ACCESS: TextStreamFileAccess = {\n deleteTree: async (path) => {\n await rm(path, { force: true, recursive: true });\n },\n ensureDirectory: async (path) => {\n await mkdir(path, { recursive: true });\n },\n listFiles: async (path) =>\n (await readdir(path, { withFileTypes: true }))\n .filter((entry) => entry.isFile())\n .map((entry) => entry.name),\n readFile: async (path) => {\n try {\n return await readFile(path);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n }\n },\n writeFile: async (path, content, options) => {\n await writeFile(path, content, {\n ...(typeof content === \"string\" ? { encoding: \"utf8\" as const } : {}),\n flag: options.overwrite === true ? \"w\" : \"wx\",\n });\n },\n};\n\nexport interface ReadonlyTextStreams {\n getSentence(sentenceId: SentenceId): Promise<string>;\n getSerial(serialId: number): ReadonlySerialTextStream;\n getSummarySerial(serialId: number): ReadonlySerialTextStream;\n}\n\nexport interface ReadonlySerialTextStream {\n getFragment(fragmentId: number): Promise<FragmentRecord>;\n getSentence?(sentenceIndex: number): Promise<SentenceRecord>;\n listSentencesInRange?(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<readonly SentenceRecord[]>;\n listFragmentIds(): Promise<readonly number[]>;\n listSentences?(): Promise<readonly SentenceRecord[]>;\n readText?(): Promise<string | undefined>;\n readTextInRange?(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<string | undefined>;\n readonly path: string;\n readonly serialId: number;\n}\n\nexport class TextStreams implements ReadonlyTextStreams {\n readonly #database: Database;\n readonly #documentPath: string;\n readonly #fileAccess: TextStreamFileAccess;\n\n public constructor(\n documentPath: string,\n database: Database,\n fileAccess: TextStreamFileAccess = DEFAULT_FILE_ACCESS,\n ) {\n this.#database = database;\n this.#documentPath = resolve(documentPath);\n this.#fileAccess = fileAccess;\n }\n\n public async ensureCreated(): Promise<void> {\n await this.#fileAccess.ensureDirectory(this.#getRootPath(\"source\"));\n await this.#fileAccess.ensureDirectory(this.#getRootPath(\"summary\"));\n }\n\n public async getSentence(sentenceId: SentenceId): Promise<string> {\n return (await this.getSerial(sentenceId[0]).getSentence(sentenceId[1]))\n .text;\n }\n\n public getSerial(serialId: number): SerialTextStream {\n return new SerialTextStream(\n this.#documentPath,\n this.#database,\n this.#fileAccess,\n \"source\",\n serialId,\n );\n }\n\n public getSummarySerial(serialId: number): SerialTextStream {\n return new SerialTextStream(\n this.#documentPath,\n this.#database,\n this.#fileAccess,\n \"summary\",\n serialId,\n );\n }\n\n #getRootPath(stream: TextStreamName): string {\n return join(this.#documentPath, \"texts\", stream);\n }\n}\n\nexport class SerialTextStream implements ReadonlySerialTextStream {\n static readonly #draftStates = new Map<string, TextStreamDraftState>();\n\n readonly #database: Database;\n readonly #documentPath: string;\n readonly #fileAccess: TextStreamFileAccess;\n readonly #stream: TextStreamName;\n readonly #serialId: number;\n\n public constructor(\n documentPath: string,\n database: Database,\n fileAccess: TextStreamFileAccess,\n stream: TextStreamName,\n serialId: number,\n ) {\n this.#database = database;\n this.#documentPath = resolve(documentPath);\n this.#fileAccess = fileAccess;\n this.#stream = stream;\n this.#serialId = serialId;\n }\n\n public async createDraft(): Promise<TextStreamDraft> {\n const draftState = this.#getDraftState();\n\n if (draftState.draftOpen) {\n throw new Error(\"Only one text stream draft can be open at a time\");\n }\n\n await this.#fileAccess.ensureDirectory(this.#getDirectoryPath());\n draftState.draftOpen = true;\n\n return new TextStreamDraft(this.#serialId, await this.#peekNextIndex(), {\n discard: () => {\n draftState.draftOpen = false;\n },\n finalize: async (startIndex, summary, sentences) =>\n await this.#commitDraft(startIndex, summary, sentences),\n });\n }\n\n public async getFragment(fragmentId: number): Promise<FragmentRecord> {\n const fragments = await this.#listFragments();\n const fragment = fragments.find((item) => item.fragmentId === fragmentId);\n\n if (fragment === undefined) {\n throw new Error(`Fragment ${fragmentId} does not exist`);\n }\n\n return fragment;\n }\n\n public async getSentence(sentenceIndex: number): Promise<SentenceRecord> {\n const location = await this.#getSentenceLocation(sentenceIndex);\n\n if (location === undefined) {\n throw new RangeError(`Sentence ${sentenceIndex} does not exist`);\n }\n\n return this.#readSentenceLocation(location, await this.#readContent());\n }\n\n public async listFragmentIds(): Promise<readonly number[]> {\n return (await this.#listFragments()).map((fragment) => fragment.fragmentId);\n }\n\n public async listSentences(): Promise<readonly SentenceRecord[]> {\n const rows = await this.#listSentenceLocations();\n const content = await this.#readContent();\n\n return rows.map((row) => this.#readSentenceLocation(row, content));\n }\n\n public async listSentencesInRange(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<readonly SentenceRecord[]> {\n if (endSentenceIndex < startSentenceIndex) {\n return [];\n }\n\n const rows = await this.#database.queryAll(\n `\n SELECT sentence_index, byte_offset, byte_length, words_count\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ?\n AND sentence_index BETWEEN ? AND ?\n ORDER BY sentence_index\n `,\n [\n TEXT_STREAM_KIND[this.#stream],\n this.#serialId,\n startSentenceIndex,\n endSentenceIndex,\n ],\n mapTextSentenceLocation,\n );\n const content = await this.#readContent();\n\n return rows.map((row) => this.#readSentenceLocation(row, content));\n }\n\n public async readTextInRange(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<string | undefined> {\n if (endSentenceIndex < startSentenceIndex) {\n return \"\";\n }\n\n const rows = await this.#database.queryAll(\n `\n SELECT sentence_index, byte_offset, byte_length, words_count\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ?\n AND sentence_index BETWEEN ? AND ?\n ORDER BY sentence_index\n `,\n [\n TEXT_STREAM_KIND[this.#stream],\n this.#serialId,\n startSentenceIndex,\n endSentenceIndex,\n ],\n mapTextSentenceLocation,\n );\n const first = rows[0];\n const last = rows[rows.length - 1];\n\n if (first === undefined || last === undefined) {\n return undefined;\n }\n\n const content = await this.#readContent();\n\n return content\n .subarray(first.byteOffset, last.byteOffset + last.byteLength)\n .toString(\"utf8\");\n }\n\n async #getSentenceLocation(\n sentenceIndex: number,\n ): Promise<TextSentenceLocation | undefined> {\n return await this.#database.queryOne(\n `\n SELECT sentence_index, byte_offset, byte_length, words_count\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ? AND sentence_index = ?\n `,\n [TEXT_STREAM_KIND[this.#stream], this.#serialId, sentenceIndex],\n mapTextSentenceLocation,\n );\n }\n\n async #listSentenceLocations(): Promise<readonly TextSentenceLocation[]> {\n return await this.#database.queryAll(\n `\n SELECT sentence_index, byte_offset, byte_length, words_count\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ?\n ORDER BY sentence_index\n `,\n [TEXT_STREAM_KIND[this.#stream], this.#serialId],\n mapTextSentenceLocation,\n );\n }\n\n #readSentenceLocation(\n location: TextSentenceLocation,\n content: Buffer,\n ): SentenceRecord {\n return new Sentence(\n content\n .subarray(\n location.byteOffset,\n location.byteOffset + location.byteLength,\n )\n .toString(\"utf8\"),\n location.wordsCount,\n );\n }\n\n public async readText(): Promise<string | undefined> {\n const content = await this.#fileAccess.readFile(this.#getTextPath());\n\n return content === undefined\n ? undefined\n : Buffer.from(content).toString(\"utf8\");\n }\n\n public async writeTextStream(\n text: string,\n options: WriteTextStreamOptions = {},\n ): Promise<void> {\n await this.delete();\n const sentences = await splitTextIntoSentenceSpans(text, options.segmenter);\n const draft = await this.createDraft();\n\n for (const sentence of sentences) {\n draft.addSentence(sentence.text, sentence.wordsCount, {\n byteOffset: sentence.byteOffset,\n byteLength: sentence.byteLength,\n });\n }\n\n await draft.commitWithText(text);\n }\n\n public async delete(): Promise<void> {\n await this.#fileAccess.deleteTree(this.#getTextPath());\n await this.#database.run(\n `\n DELETE FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ?\n `,\n [TEXT_STREAM_KIND[this.#stream], this.#serialId],\n );\n delete this.#getDraftState().nextSentenceIndex;\n }\n\n public get path(): string {\n return this.#getTextPath();\n }\n\n public get serialId(): number {\n return this.#serialId;\n }\n\n async #commitDraft(\n startIndex: number,\n textOverride: string,\n sentences: readonly SentenceRecord[],\n ): Promise<FragmentRecord | undefined> {\n const draftState = this.#getDraftState();\n\n draftState.draftOpen = false;\n\n const existing = await this.#fileAccess.readFile(this.#getTextPath());\n const existingBuffer =\n existing === undefined ? Buffer.alloc(0) : Buffer.from(existing);\n const text =\n textOverride === \"\"\n ? sentences.map(getSentenceRawText).join(\"\")\n : textOverride;\n const appendBuffer = Buffer.from(text, \"utf8\");\n let offset = existingBuffer.length;\n\n await this.#fileAccess.ensureDirectory(this.#getDirectoryPath());\n await this.#fileAccess.writeFile(\n this.#getTextPath(),\n Buffer.concat([existingBuffer, appendBuffer]),\n { overwrite: true },\n );\n\n if (sentences.length === 0) {\n return undefined;\n }\n\n for (let index = 0; index < sentences.length; index += 1) {\n const sentence = sentences[index];\n\n if (sentence === undefined) {\n continue;\n }\n\n const length = getSentenceByteLength(sentence);\n const explicitOffset = getSentenceByteOffset(sentence);\n const sentenceOffset = offset + explicitOffset;\n\n await this.#database.run(\n `\n INSERT OR REPLACE INTO text_sentence_records (\n kind,\n chapter_id,\n sentence_index,\n words_count,\n byte_offset,\n byte_length\n )\n VALUES (?, ?, ?, ?, ?, ?)\n `,\n [\n TEXT_STREAM_KIND[this.#stream],\n this.#serialId,\n startIndex + index,\n sentence.wordsCount,\n sentenceOffset,\n length,\n ],\n );\n if (!hasSentenceByteOffset(sentence)) {\n offset += length;\n }\n }\n\n draftState.nextSentenceIndex = startIndex + sentences.length;\n\n return {\n fragmentId: startIndex,\n sentences,\n serialId: this.#serialId,\n summary: \"\",\n };\n }\n\n async #listFragments(): Promise<readonly FragmentRecord[]> {\n const sentences = await this.listSentences();\n const fragments: FragmentRecord[] = [];\n let current: SentenceRecord[] = [];\n let currentWords = 0;\n let fragmentStart = 0;\n\n for (let index = 0; index < sentences.length; index += 1) {\n const sentence = sentences[index];\n\n if (sentence === undefined) {\n continue;\n }\n if (\n current.length > 0 &&\n currentWords + sentence.wordsCount > DEFAULT_FRAGMENT_WORDS_COUNT\n ) {\n fragments.push({\n fragmentId: fragmentStart,\n sentences: current,\n serialId: this.#serialId,\n summary: \"\",\n });\n fragmentStart = index;\n current = [];\n currentWords = 0;\n }\n\n current.push(sentence);\n currentWords += sentence.wordsCount;\n }\n\n if (current.length > 0) {\n fragments.push({\n fragmentId: fragmentStart,\n sentences: current,\n serialId: this.#serialId,\n summary: \"\",\n });\n }\n\n return fragments;\n }\n\n async #peekNextIndex(): Promise<number> {\n const draftState = this.#getDraftState();\n\n if (draftState.nextSentenceIndex !== undefined) {\n return draftState.nextSentenceIndex;\n }\n\n draftState.nextSentenceIndex =\n (await this.#database.queryOne(\n `\n SELECT COALESCE(MAX(sentence_index), -1) + 1 AS next_index\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ?\n `,\n [TEXT_STREAM_KIND[this.#stream], this.#serialId],\n (row) => Number(row.next_index),\n )) ?? 0;\n\n return draftState.nextSentenceIndex;\n }\n\n async #readContent(): Promise<Buffer> {\n const content = await this.#fileAccess.readFile(this.#getTextPath());\n\n return Buffer.from(content ?? new Uint8Array());\n }\n\n #getDirectoryPath(): string {\n return join(this.#documentPath, \"texts\", this.#stream);\n }\n\n #getTextPath(): string {\n return join(this.#getDirectoryPath(), `${this.#serialId}.txt`);\n }\n\n #getDraftState(): TextStreamDraftState {\n const key = `${this.#documentPath}\\0${this.#stream}\\0${this.#serialId}`;\n let state = SerialTextStream.#draftStates.get(key);\n\n if (state === undefined) {\n state = { draftOpen: false };\n SerialTextStream.#draftStates.set(key, state);\n }\n\n return state;\n }\n}\n\nfunction mapTextSentenceLocation(\n row: Record<string, unknown>,\n): TextSentenceLocation {\n return {\n byteLength: Number(row.byte_length),\n byteOffset: Number(row.byte_offset),\n sentenceIndex: Number(row.sentence_index),\n wordsCount: Number(row.words_count),\n };\n}\n\nexport class TextStreamDraft {\n #committed = false;\n readonly #discard: () => void;\n readonly #finalize: (\n startSentenceIndex: number,\n summary: string,\n sentences: readonly SentenceRecord[],\n ) => Promise<FragmentRecord | undefined>;\n readonly #sentences: SentenceRecord[] = [];\n readonly #serialId: number;\n readonly #startSentenceIndex: number;\n #summary = \"\";\n\n public constructor(\n serialId: number,\n startSentenceIndex: number,\n callbacks: {\n readonly discard: () => void;\n readonly finalize: (\n startSentenceIndex: number,\n summary: string,\n sentences: readonly SentenceRecord[],\n ) => Promise<FragmentRecord | undefined>;\n },\n ) {\n this.#discard = callbacks.discard;\n this.#finalize = callbacks.finalize;\n this.#serialId = serialId;\n this.#startSentenceIndex = startSentenceIndex;\n }\n\n public addSentence(\n text: string,\n wordsCount: number,\n location?: {\n readonly byteOffset: number;\n readonly byteLength: number;\n },\n ): SentenceId {\n this.#assertActive();\n const sentenceIndex = this.#startSentenceIndex + this.#sentences.length;\n const sentence = new Sentence(text, wordsCount);\n\n if (location !== undefined) {\n Object.assign(sentence, {\n byteLength: location.byteLength,\n byteOffset: location.byteOffset,\n });\n }\n\n this.#sentences.push(sentence);\n\n return [this.#serialId, sentenceIndex];\n }\n\n public async commit(): Promise<FragmentRecord | undefined> {\n this.#assertActive();\n this.#committed = true;\n\n return await this.#finalize(\n this.#startSentenceIndex,\n this.#summary,\n this.#sentences,\n );\n }\n\n public async commitWithText(\n text: string,\n ): Promise<FragmentRecord | undefined> {\n this.#assertActive();\n this.#committed = true;\n\n return await this.#finalize(\n this.#startSentenceIndex,\n text,\n this.#sentences,\n );\n }\n\n public discard(): void {\n this.#assertActive();\n this.#committed = true;\n this.#discard();\n }\n\n public setSummary(summary: string): void {\n this.#assertActive();\n this.#summary = summary;\n }\n\n public get fragmentId(): number {\n return this.#startSentenceIndex;\n }\n\n #assertActive(): void {\n if (this.#committed) {\n throw new Error(\"Text stream draft is already finalized\");\n }\n }\n}\n\nasync function splitTextIntoSentenceSpans(\n text: string,\n segmenter: TextStreamSentenceSegmenter | undefined,\n): Promise<\n ReadonlyArray<\n SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n }\n >\n> {\n if (segmenter !== undefined) {\n return await splitTextIntoCustomSentenceSpans(text, segmenter);\n }\n\n const spans: Array<\n SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n }\n > = [];\n\n for (const segment of createSentenceSegmenter().segment(text)) {\n const rawText = segment.segment;\n\n if (rawText.trim() === \"\") {\n continue;\n }\n const sentence = new Sentence(rawText, countWords(rawText));\n\n Object.assign(sentence, {\n byteLength: Buffer.byteLength(rawText, \"utf8\"),\n byteOffset: Buffer.byteLength(text.slice(0, segment.index), \"utf8\"),\n });\n spans.push(\n sentence as unknown as SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n },\n );\n }\n\n return spans;\n}\n\nasync function splitTextIntoCustomSentenceSpans(\n text: string,\n segmenter: TextStreamSentenceSegmenter,\n): Promise<\n ReadonlyArray<\n SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n }\n >\n> {\n const spans: Array<\n SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n }\n > = [];\n\n for await (const segment of segmenter.pipe([text])) {\n const rawText = text.slice(\n segment.offset,\n segment.offset + segment.text.length,\n );\n\n if (rawText.trim() === \"\") {\n continue;\n }\n const sentence = new Sentence(rawText, segment.wordsCount);\n\n Object.assign(sentence, {\n byteLength: Buffer.byteLength(rawText, \"utf8\"),\n byteOffset: Buffer.byteLength(text.slice(0, segment.offset), \"utf8\"),\n });\n spans.push(\n sentence as unknown as SentenceRecord & {\n readonly byteOffset: number;\n readonly byteLength: number;\n },\n );\n }\n\n return spans;\n}\n\nlet SENTENCE_SEGMENTER: Intl.Segmenter | undefined;\n\nfunction createSentenceSegmenter(): Intl.Segmenter {\n SENTENCE_SEGMENTER ??= new Intl.Segmenter(undefined, {\n granularity: \"sentence\",\n });\n\n return SENTENCE_SEGMENTER;\n}\n\nfunction countWords(text: string): number {\n const trimmed = text.trim();\n\n return trimmed === \"\" ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nfunction getSentenceByteOffset(sentence: SentenceRecord): number {\n const value = (sentence as { readonly byteOffset?: unknown }).byteOffset;\n\n return typeof value === \"number\" ? value : 0;\n}\n\nfunction hasSentenceByteOffset(sentence: SentenceRecord): boolean {\n return (\n typeof (sentence as { readonly byteOffset?: unknown }).byteOffset ===\n \"number\"\n );\n}\n\nfunction getSentenceByteLength(sentence: SentenceRecord): number {\n const value = (sentence as { readonly byteLength?: unknown }).byteLength;\n\n return typeof value === \"number\"\n ? value\n : Buffer.byteLength(getSentenceRawText(sentence), \"utf8\");\n}\n\nfunction getSentenceRawText(sentence: SentenceRecord): string {\n const value = (sentence as { readonly rawText?: unknown }).rawText;\n\n return typeof value === \"string\" ? value : sentence.text;\n}\n","import { mkdir, readFile, readdir, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\n\nimport { isNodeError } from \"../utils/node-error.js\";\nimport type { FragmentRecord, SentenceId, SentenceRecord } from \"./types.js\";\n\nconst SERIAL_DIRECTORY_PREFIX = \"serial-\";\nconst FRAGMENT_FILE_PATTERN = /^fragment_(\\d+)\\.json$/;\nconst DEFAULT_FRAGMENT_WORDS_COUNT = 600;\n\ninterface FragmentFileContent {\n readonly summary: string;\n readonly sentences: readonly SentenceRecord[];\n}\n\ninterface FragmentWriter {\n write(path: string, content: string): Promise<void>;\n}\n\ninterface FragmentFileAccess {\n ensureDirectory(path: string): Promise<void>;\n listFileContents?(path: string): Promise<ReadonlyMap<string, Uint8Array>>;\n listFiles(path: string): Promise<readonly string[]>;\n readFile(path: string): Promise<Uint8Array | undefined>;\n}\n\nconst DEFAULT_FRAGMENT_WRITER: FragmentWriter = {\n write: async (path, content) => {\n await writeFile(path, content, \"utf8\");\n },\n};\n\nconst DEFAULT_FRAGMENT_FILE_ACCESS: FragmentFileAccess = {\n ensureDirectory: async (path) => {\n await mkdir(path, { recursive: true });\n },\n listFiles: async (path) =>\n (await readdir(path, { withFileTypes: true }))\n .filter((entry) => entry.isFile())\n .map((entry) => entry.name),\n readFile: async (path) => {\n try {\n return await readFile(path);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n }\n },\n};\n\nexport interface ReadonlyFragments {\n getSerial(serialId: number): ReadonlySerialFragments;\n getSummarySerial(serialId: number): ReadonlySerialFragments;\n getSentence(sentenceId: SentenceId): Promise<string>;\n getSummary(serialId: number, fragmentId: number): Promise<string>;\n getWordsCount(serialId: number, fragmentId: number): Promise<number>;\n readonly path: string;\n}\n\nexport interface TextStreamWriteOptions {\n readonly maxWordsCount?: number;\n}\n\nexport interface ReadonlySerialFragments {\n getFragment(fragmentId: number): Promise<FragmentRecord>;\n listFragmentIds(): Promise<readonly number[]>;\n readonly serialId: number;\n readonly path: string;\n}\n\nexport class Fragments implements ReadonlyFragments {\n readonly #documentPath: string;\n readonly #fileAccess: FragmentFileAccess;\n readonly #writer: FragmentWriter;\n\n public constructor(\n documentPath: string,\n writer?: FragmentWriter,\n fileAccess?: FragmentFileAccess,\n ) {\n this.#documentPath = resolve(documentPath);\n this.#fileAccess = fileAccess ?? DEFAULT_FRAGMENT_FILE_ACCESS;\n this.#writer = writer ?? DEFAULT_FRAGMENT_WRITER;\n }\n\n public async ensureCreated(): Promise<void> {\n await this.#fileAccess.ensureDirectory(this.path);\n }\n\n public getSerial(serialId: number): SerialFragments {\n return new SerialFragments(\n this.#documentPath,\n serialId,\n \"fragments\",\n this.#writer,\n this.#fileAccess,\n );\n }\n\n public getSummarySerial(serialId: number): SerialFragments {\n return new SerialFragments(\n this.#documentPath,\n serialId,\n \"summaries\",\n this.#writer,\n this.#fileAccess,\n );\n }\n\n public async getSentence(sentenceId: SentenceId): Promise<string> {\n const [serialId, sentenceIndex] = sentenceId;\n const fragmentIds = await this.getSerial(serialId).listFragmentIds();\n let remainingSentenceIndex = sentenceIndex;\n\n for (const fragmentId of fragmentIds) {\n const fragment = await this.getSerial(serialId).getFragment(fragmentId);\n const sentence = fragment.sentences[remainingSentenceIndex];\n\n if (sentence !== undefined) {\n return sentence.text;\n }\n\n remainingSentenceIndex -= fragment.sentences.length;\n }\n\n throw new RangeError(`Sentence ${sentenceIndex} does not exist`);\n }\n\n public async getSummary(\n serialId: number,\n fragmentId: number,\n ): Promise<string> {\n return (await this.getSerial(serialId).getFragment(fragmentId)).summary;\n }\n\n public async getWordsCount(\n serialId: number,\n fragmentId: number,\n ): Promise<number> {\n const fragment = await this.getSerial(serialId).getFragment(fragmentId);\n\n return fragment.sentences.reduce(\n (total, sentence) => total + sentence.wordsCount,\n 0,\n );\n }\n\n public get path(): string {\n return join(this.#documentPath, \"fragments\");\n }\n}\n\nexport class SerialFragments implements ReadonlySerialFragments {\n readonly #serialId: number;\n #draftOpen = false;\n readonly #documentPath: string;\n readonly #fileAccess: FragmentFileAccess;\n #fileContents: Promise<ReadonlyMap<string, Uint8Array>> | undefined;\n readonly #rootDirectoryName: string;\n #nextFragmentId: number | undefined;\n #nextSentenceIndex: number | undefined;\n readonly #writer: FragmentWriter;\n\n public constructor(\n documentPath: string,\n serialId: number,\n rootDirectoryName = \"fragments\",\n writer?: FragmentWriter,\n fileAccess?: FragmentFileAccess,\n ) {\n this.#documentPath = resolve(documentPath);\n this.#serialId = serialId;\n this.#fileAccess = fileAccess ?? DEFAULT_FRAGMENT_FILE_ACCESS;\n this.#rootDirectoryName = rootDirectoryName;\n this.#writer = writer ?? DEFAULT_FRAGMENT_WRITER;\n }\n\n public async createDraft(): Promise<FragmentDraft> {\n if (this.#draftOpen) {\n throw new Error(\"Only one fragment draft can be open at a time\");\n }\n\n await this.#fileAccess.ensureDirectory(this.path);\n this.#draftOpen = true;\n\n return new FragmentDraft(\n this.#serialId,\n await this.#peekNextFragmentId(),\n await this.#peekNextSentenceIndex(),\n {\n discard: () => {\n this.#discardDraft();\n },\n finalize: async (fragmentId, startSentenceIndex, summary, sentences) =>\n await this.#commitDraft(\n fragmentId,\n startSentenceIndex,\n summary,\n sentences,\n ),\n },\n );\n }\n\n public async getFragment(fragmentId: number): Promise<FragmentRecord> {\n const fileContent =\n this.#fileAccess.listFileContents === undefined\n ? await readFragmentFile(\n this.#getFragmentPath(fragmentId),\n this.#fileAccess,\n )\n : parseFragmentFileContent(\n this.#getFragmentPath(fragmentId),\n (await this.#getFileContents()).get(`fragment_${fragmentId}.json`),\n );\n\n return {\n serialId: this.#serialId,\n fragmentId,\n summary: fileContent.summary,\n sentences: fileContent.sentences,\n };\n }\n\n public async listFragmentIds(): Promise<readonly number[]> {\n try {\n const entries =\n this.#fileAccess.listFileContents === undefined\n ? await this.#fileAccess.listFiles(this.path)\n : [...(await this.#getFileContents()).keys()];\n\n return entries\n .map((entry) => FRAGMENT_FILE_PATTERN.exec(entry))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => Number(match[1]))\n .sort((left, right) => left - right);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return [];\n }\n\n throw error;\n }\n }\n\n public async writeTextStream(\n text: string,\n options: TextStreamWriteOptions = {},\n ): Promise<void> {\n await this.#fileAccess.ensureDirectory(this.path);\n const sentences = splitTextIntoSentences(text);\n\n if (sentences.length === 0) {\n const fragmentId = await this.#peekNextFragmentId();\n\n await this.#writer.write(\n this.#getFragmentPath(fragmentId),\n JSON.stringify(\n {\n sentences: [],\n summary: \"\",\n },\n undefined,\n 2,\n ),\n );\n this.#nextFragmentId = fragmentId + 1;\n return;\n }\n\n const maxWordsCount = options.maxWordsCount ?? DEFAULT_FRAGMENT_WORDS_COUNT;\n let draft = await this.createDraft();\n let draftWordsCount = 0;\n\n for (const sentence of sentences) {\n if (\n draftWordsCount > 0 &&\n draftWordsCount + sentence.wordsCount > maxWordsCount\n ) {\n await draft.commit();\n draft = await this.createDraft();\n draftWordsCount = 0;\n }\n\n draft.addSentence(sentence.text, sentence.wordsCount);\n draftWordsCount += sentence.wordsCount;\n }\n\n await draft.commit();\n }\n\n public get serialId(): number {\n return this.#serialId;\n }\n\n public get path(): string {\n return join(\n this.#documentPath,\n this.#rootDirectoryName,\n `${SERIAL_DIRECTORY_PREFIX}${this.#serialId}`,\n );\n }\n\n async #commitDraft(\n fragmentId: number,\n startSentenceIndex: number,\n summary: string,\n sentences: readonly SentenceRecord[],\n ): Promise<FragmentRecord | undefined> {\n this.#draftOpen = false;\n\n if (sentences.length === 0) {\n return undefined;\n }\n\n await this.#fileAccess.ensureDirectory(this.path);\n await this.#writer.write(\n this.#getFragmentPath(fragmentId),\n JSON.stringify(\n {\n sentences,\n summary,\n },\n undefined,\n 2,\n ),\n );\n\n this.#nextFragmentId = fragmentId + 1;\n this.#nextSentenceIndex = startSentenceIndex + sentences.length;\n\n return {\n serialId: this.#serialId,\n fragmentId,\n summary,\n sentences,\n };\n }\n\n #discardDraft(): void {\n this.#draftOpen = false;\n }\n\n async #peekNextFragmentId(): Promise<number> {\n if (this.#nextFragmentId !== undefined) {\n return this.#nextFragmentId;\n }\n\n const fragmentIds = await this.listFragmentIds();\n const lastFragmentId = fragmentIds[fragmentIds.length - 1];\n\n this.#nextFragmentId =\n lastFragmentId === undefined ? 0 : lastFragmentId + 1;\n\n return this.#nextFragmentId;\n }\n\n async #peekNextSentenceIndex(): Promise<number> {\n if (this.#nextSentenceIndex !== undefined) {\n return this.#nextSentenceIndex;\n }\n\n let nextSentenceIndex = 0;\n\n for (const fragmentId of await this.listFragmentIds()) {\n nextSentenceIndex += (await this.getFragment(fragmentId)).sentences\n .length;\n }\n\n this.#nextSentenceIndex = nextSentenceIndex;\n return this.#nextSentenceIndex;\n }\n\n #getFragmentPath(fragmentId: number): string {\n return join(this.path, `fragment_${fragmentId}.json`);\n }\n\n async #getFileContents(): Promise<ReadonlyMap<string, Uint8Array>> {\n if (this.#fileAccess.listFileContents === undefined) {\n throw new Error(\"Fragment file access does not support batch reads.\");\n }\n\n this.#fileContents ??= this.#fileAccess.listFileContents(this.path);\n return await this.#fileContents;\n }\n}\n\nfunction splitTextIntoSentences(text: string): readonly SentenceRecord[] {\n return text\n .split(/\\n+/u)\n .map((sentence) => sentence.trim())\n .filter((sentence) => sentence !== \"\")\n .map((sentence) => ({\n text: sentence,\n wordsCount: countWords(sentence),\n }));\n}\n\nfunction countWords(text: string): number {\n const trimmed = text.trim();\n\n return trimmed === \"\" ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nexport class FragmentDraft {\n #committed = false;\n readonly #serialId: number;\n readonly #discard: () => void;\n readonly #finalize: (\n fragmentId: number,\n startSentenceIndex: number,\n summary: string,\n sentences: readonly SentenceRecord[],\n ) => Promise<FragmentRecord | undefined>;\n readonly #fragmentId: number;\n readonly #sentences: SentenceRecord[] = [];\n #summary = \"\";\n readonly #startSentenceIndex: number;\n\n public constructor(\n serialId: number,\n fragmentId: number,\n startSentenceIndex: number,\n callbacks: {\n readonly discard: () => void;\n readonly finalize: (\n fragmentId: number,\n startSentenceIndex: number,\n summary: string,\n sentences: readonly SentenceRecord[],\n ) => Promise<FragmentRecord | undefined>;\n },\n ) {\n this.#serialId = serialId;\n this.#discard = callbacks.discard;\n this.#finalize = callbacks.finalize;\n this.#fragmentId = fragmentId;\n this.#startSentenceIndex = startSentenceIndex;\n }\n\n public addSentence(text: string, wordsCount: number): SentenceId {\n this.#assertActive();\n const sentenceIndex = this.#startSentenceIndex + this.#sentences.length;\n\n this.#sentences.push({\n text,\n wordsCount,\n });\n\n return [this.#serialId, sentenceIndex];\n }\n\n public async commit(): Promise<FragmentRecord | undefined> {\n this.#assertActive();\n this.#committed = true;\n\n return await this.#finalize(\n this.#fragmentId,\n this.#startSentenceIndex,\n this.#summary,\n this.#sentences,\n );\n }\n\n public discard(): void {\n this.#assertActive();\n this.#committed = true;\n this.#discard();\n }\n\n public setSummary(summary: string): void {\n this.#assertActive();\n this.#summary = summary;\n }\n\n public get fragmentId(): number {\n return this.#fragmentId;\n }\n\n #assertActive(): void {\n if (this.#committed) {\n throw new Error(\"Fragment draft is already finalized\");\n }\n }\n}\n\nasync function readFragmentFile(\n fragmentPath: string,\n fileAccess: FragmentFileAccess = DEFAULT_FRAGMENT_FILE_ACCESS,\n): Promise<FragmentFileContent> {\n return parseFragmentFileContent(\n fragmentPath,\n await readFragmentFileContent(fragmentPath, fileAccess),\n );\n}\n\nfunction parseFragmentFileContent(\n fragmentPath: string,\n content: Uint8Array | undefined,\n): FragmentFileContent {\n if (content === undefined) {\n throw new Error(`Fragment file does not exist: ${fragmentPath}`);\n }\n\n const rawContent = JSON.parse(\n Buffer.from(content).toString(\"utf8\"),\n ) as unknown;\n\n if (typeof rawContent !== \"object\" || rawContent === null) {\n throw new TypeError(\"Fragment file must be an object\");\n }\n\n if (!(\"summary\" in rawContent) || typeof rawContent.summary !== \"string\") {\n throw new TypeError(\"Fragment file summary must be a string\");\n }\n if (!(\"sentences\" in rawContent) || !Array.isArray(rawContent.sentences)) {\n throw new TypeError(\"Fragment file sentences must be an array\");\n }\n\n return {\n sentences: rawContent.sentences.map(parseSentenceRecord),\n summary: rawContent.summary,\n };\n}\n\nasync function readFragmentFileContent(\n fragmentPath: string,\n fileAccess: FragmentFileAccess,\n): Promise<Uint8Array | undefined> {\n if (fileAccess.listFileContents === undefined) {\n return await fileAccess.readFile(fragmentPath);\n }\n\n const directoryPath = resolve(join(fragmentPath, \"..\"));\n const fileName = fragmentPath.slice(directoryPath.length + 1);\n\n return (await fileAccess.listFileContents(directoryPath)).get(fileName);\n}\n\nfunction parseSentenceRecord(value: unknown): SentenceRecord {\n if (typeof value !== \"object\" || value === null) {\n throw new TypeError(\"Sentence entry must be an object\");\n }\n\n const { text, wordsCount } = value as {\n readonly text?: unknown;\n readonly wordsCount?: unknown;\n };\n\n if (typeof text !== \"string\" || typeof wordsCount !== \"number\") {\n throw new TypeError(\"Sentence entry is invalid\");\n }\n\n return {\n text,\n wordsCount,\n };\n}\n","import { z } from \"zod\";\n\nexport const SOURCE_FORMATS = [\"epub\", \"pdf\", \"txt\", \"markdown\"] as const;\nexport const BOOK_META_VERSION = 1;\n\nexport const sourceFormatSchema = z.enum(SOURCE_FORMATS);\n\nexport const bookMetaSchema = z.object({\n version: z.literal(BOOK_META_VERSION),\n sourceFormat: sourceFormatSchema,\n title: z.string().min(1).nullable(),\n authors: z.array(z.string().min(1)),\n language: z.string().min(1).nullable(),\n identifier: z.string().min(1).nullable(),\n publisher: z.string().min(1).nullable(),\n publishedAt: z.string().min(1).nullable(),\n description: z.string().min(1).nullable(),\n});\n\nexport type SourceFormat = z.infer<typeof sourceFormatSchema>;\nexport type BookMeta = z.infer<typeof bookMetaSchema>;\n","import { z } from \"zod\";\n\nexport const TOC_FILE_VERSION = 1;\n\nexport interface TocItem {\n readonly title?: string | null | undefined;\n readonly serialId?: number | undefined;\n readonly children: readonly TocItem[];\n}\n\nexport const tocItemSchema: z.ZodType<TocItem> = z.object({\n title: z.string().min(1).nullable().optional(),\n serialId: z.number().int().nonnegative().optional(),\n children: z.lazy(() => z.array(tocItemSchema)),\n});\n\nexport const tocFileSchema = z.object({\n version: z.literal(TOC_FILE_VERSION),\n items: z.array(tocItemSchema),\n});\n\nexport type TocFile = z.infer<typeof tocFileSchema>;\n","import type { Database } from \"./database.js\";\n\nexport const SCHEMA_SQL = `\n CREATE TABLE IF NOT EXISTS serials (\n id INTEGER PRIMARY KEY,\n document_order INTEGER NOT NULL DEFAULT 0\n );\n\n CREATE TABLE IF NOT EXISTS serial_states (\n serial_id INTEGER PRIMARY KEY,\n revision INTEGER NOT NULL DEFAULT 0,\n topology_ready INTEGER NOT NULL DEFAULT 0,\n topology_parameter_hash TEXT,\n knowledge_graph_ready INTEGER NOT NULL DEFAULT 0,\n knowledge_graph_parameter_hash TEXT,\n FOREIGN KEY (serial_id) REFERENCES serials(id)\n );\n\n CREATE TABLE IF NOT EXISTS archive_revisions (\n key TEXT PRIMARY KEY,\n value INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS archive_index_settings (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n fts_embedded INTEGER NOT NULL DEFAULT 0\n );\n\n CREATE TABLE IF NOT EXISTS graph_build_parameters (\n hash TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n language TEXT,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY,\n generation INTEGER NOT NULL,\n serial_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n label TEXT NOT NULL,\n content TEXT NOT NULL,\n retention TEXT,\n importance TEXT,\n wordsCount INTEGER NOT NULL DEFAULT 0,\n weight REAL NOT NULL DEFAULT 0.0\n );\n\n CREATE INDEX IF NOT EXISTS idx_chunks_sentence\n ON chunks(serial_id, sentence_index);\n\n CREATE INDEX IF NOT EXISTS idx_chunks_serial_id\n ON chunks(serial_id, id);\n\n CREATE TABLE IF NOT EXISTS chunk_sentences (\n chunk_id INTEGER NOT NULL,\n serial_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n FOREIGN KEY (chunk_id) REFERENCES chunks(id),\n PRIMARY KEY (chunk_id, serial_id, sentence_index)\n );\n\n CREATE TABLE IF NOT EXISTS reading_edges (\n from_id INTEGER NOT NULL,\n to_id INTEGER NOT NULL,\n strength TEXT,\n weight REAL NOT NULL DEFAULT 0.1,\n PRIMARY KEY (from_id, to_id),\n FOREIGN KEY (from_id) REFERENCES chunks(id),\n FOREIGN KEY (to_id) REFERENCES chunks(id)\n );\n\n CREATE TABLE IF NOT EXISTS snakes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n serial_id INTEGER NOT NULL,\n group_id INTEGER NOT NULL,\n local_snake_id INTEGER NOT NULL,\n size INTEGER NOT NULL,\n first_label TEXT NOT NULL,\n last_label TEXT NOT NULL,\n wordsCount INTEGER NOT NULL DEFAULT 0,\n weight REAL NOT NULL DEFAULT 0.0,\n UNIQUE(serial_id, group_id, local_snake_id)\n );\n\n CREATE TABLE IF NOT EXISTS snake_chunks (\n snake_id INTEGER NOT NULL,\n chunk_id INTEGER NOT NULL,\n position INTEGER NOT NULL,\n FOREIGN KEY (snake_id) REFERENCES snakes(id),\n FOREIGN KEY (chunk_id) REFERENCES chunks(id),\n PRIMARY KEY (snake_id, chunk_id)\n );\n\n CREATE TABLE IF NOT EXISTS snake_edges (\n from_snake_id INTEGER NOT NULL,\n to_snake_id INTEGER NOT NULL,\n weight REAL NOT NULL DEFAULT 0.1,\n PRIMARY KEY (from_snake_id, to_snake_id),\n FOREIGN KEY (from_snake_id) REFERENCES snakes(id),\n FOREIGN KEY (to_snake_id) REFERENCES snakes(id)\n );\n\n CREATE TABLE IF NOT EXISTS sentence_groups (\n serial_id INTEGER NOT NULL,\n group_id INTEGER NOT NULL,\n start_sentence_index INTEGER NOT NULL,\n end_sentence_index INTEGER NOT NULL,\n PRIMARY KEY (serial_id, group_id, start_sentence_index, end_sentence_index)\n );\n\n CREATE TABLE IF NOT EXISTS mentions (\n id TEXT PRIMARY KEY,\n chapter_id INTEGER NOT NULL,\n sentence_index INTEGER,\n range_start INTEGER NOT NULL,\n range_end INTEGER NOT NULL,\n surface TEXT NOT NULL,\n qid TEXT NOT NULL,\n confidence REAL,\n note TEXT,\n FOREIGN KEY (chapter_id) REFERENCES serials(id)\n );\n\n CREATE INDEX IF NOT EXISTS idx_mentions_chapter\n ON mentions(chapter_id);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_chapter_position\n ON mentions(chapter_id, sentence_index, range_start, range_end, id);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_chapter_qid\n ON mentions(chapter_id, qid);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_qid\n ON mentions(qid);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_qid_position\n ON mentions(qid, chapter_id, sentence_index, range_start, range_end, id);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_surface\n ON mentions(surface);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_surface_position\n ON mentions(surface, chapter_id, sentence_index, range_start, range_end, id);\n\n CREATE INDEX IF NOT EXISTS idx_mentions_sentence\n ON mentions(chapter_id, sentence_index);\n\n CREATE TABLE IF NOT EXISTS mention_links (\n id TEXT PRIMARY KEY,\n source_mention_id TEXT NOT NULL,\n target_mention_id TEXT NOT NULL,\n predicate TEXT NOT NULL,\n confidence REAL,\n note TEXT,\n FOREIGN KEY (source_mention_id) REFERENCES mentions(id),\n FOREIGN KEY (target_mention_id) REFERENCES mentions(id)\n );\n\n CREATE TABLE IF NOT EXISTS mention_link_evidence_sentences (\n link_id TEXT NOT NULL,\n chapter_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n FOREIGN KEY (link_id) REFERENCES mention_links(id),\n PRIMARY KEY (link_id, chapter_id, sentence_index)\n );\n\n CREATE INDEX IF NOT EXISTS idx_mention_link_evidence_sentences_sentence\n ON mention_link_evidence_sentences(chapter_id, sentence_index);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_source\n ON mention_links(source_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_target\n ON mention_links(target_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_predicate\n ON mention_links(predicate);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_predicate_source_target\n ON mention_links(predicate, source_mention_id, target_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_predicate_target_source\n ON mention_links(predicate, target_mention_id, source_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_source_predicate_target\n ON mention_links(source_mention_id, predicate, target_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_target_predicate_source\n ON mention_links(target_mention_id, predicate, source_mention_id);\n\n CREATE INDEX IF NOT EXISTS idx_mention_links_source_target_predicate\n ON mention_links(source_mention_id, target_mention_id, predicate);\n\n CREATE INDEX IF NOT EXISTS idx_reading_edges_target\n ON reading_edges(to_id, from_id);\n\n CREATE INDEX IF NOT EXISTS idx_snake_edges_target\n ON snake_edges(to_snake_id, from_snake_id);\n\n CREATE VIEW IF NOT EXISTS chapter_entities AS\n SELECT\n chapter_id,\n qid,\n COUNT(*) AS mention_count\n FROM mentions\n GROUP BY chapter_id, qid;\n\n CREATE VIEW IF NOT EXISTS book_entities AS\n SELECT\n qid,\n SUM(mention_count) AS mention_count\n FROM chapter_entities\n GROUP BY qid;\n\n CREATE VIEW IF NOT EXISTS chapter_entity_relations AS\n SELECT\n source_mentions.chapter_id AS chapter_id,\n source_mentions.qid AS subject_qid,\n mention_links.predicate AS predicate,\n target_mentions.qid AS object_qid,\n COUNT(*) AS evidence_count\n FROM mention_links\n JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.chapter_id = target_mentions.chapter_id\n GROUP BY\n source_mentions.chapter_id,\n source_mentions.qid,\n mention_links.predicate,\n target_mentions.qid;\n\n CREATE VIEW IF NOT EXISTS book_entity_relations AS\n SELECT\n subject_qid,\n predicate,\n object_qid,\n SUM(evidence_count) AS evidence_count\n FROM chapter_entity_relations\n GROUP BY subject_qid, predicate, object_qid;\n\n CREATE TABLE IF NOT EXISTS text_sentence_records (\n id INTEGER PRIMARY KEY,\n kind INTEGER NOT NULL,\n chapter_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n words_count INTEGER NOT NULL DEFAULT 0,\n byte_offset INTEGER NOT NULL DEFAULT 0,\n byte_length INTEGER NOT NULL DEFAULT 0,\n UNIQUE(kind, chapter_id, sentence_index)\n );\n\n CREATE INDEX IF NOT EXISTS idx_text_sentence_records_chapter\n ON text_sentence_records(kind, chapter_id, sentence_index);\n\n CREATE TABLE IF NOT EXISTS object_metadata (\n id INTEGER PRIMARY KEY,\n object_kind INTEGER NOT NULL,\n object_path TEXT NOT NULL,\n key TEXT NOT NULL,\n value_json TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n chapter_id INTEGER,\n chunk_id INTEGER,\n entity_qid TEXT,\n triple_subject_qid TEXT,\n triple_predicate TEXT,\n triple_object_qid TEXT,\n UNIQUE(object_path, key)\n );\n\n CREATE INDEX IF NOT EXISTS idx_object_metadata_object\n ON object_metadata(object_path);\n\n CREATE INDEX IF NOT EXISTS idx_object_metadata_chapter\n ON object_metadata(chapter_id);\n\n CREATE INDEX IF NOT EXISTS idx_object_metadata_chunk\n ON object_metadata(chunk_id);\n\n CREATE INDEX IF NOT EXISTS idx_object_metadata_entity\n ON object_metadata(entity_qid);\n\n CREATE INDEX IF NOT EXISTS idx_object_metadata_triple\n ON object_metadata(\n triple_subject_qid,\n triple_predicate,\n triple_object_qid\n );\n`;\n\nexport const SEARCH_INDEX_SCHEMA_SQL = `\n CREATE TABLE IF NOT EXISTS search_index_state (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS text_sentence_records (\n id INTEGER PRIMARY KEY,\n kind INTEGER NOT NULL,\n chapter_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n words_count INTEGER NOT NULL DEFAULT 0,\n byte_offset INTEGER NOT NULL DEFAULT 0,\n byte_length INTEGER NOT NULL DEFAULT 0,\n UNIQUE(kind, chapter_id, sentence_index)\n );\n\n CREATE INDEX IF NOT EXISTS idx_text_sentence_records_chapter\n ON text_sentence_records(kind, chapter_id, sentence_index);\n\n CREATE VIRTUAL TABLE IF NOT EXISTS text_sentence_fts USING fts5(\n tier1,\n tier2,\n tier3,\n tokenize='ascii'\n );\n\n CREATE TABLE IF NOT EXISTS search_object_properties_records (\n id INTEGER PRIMARY KEY,\n owner_kind INTEGER NOT NULL,\n owner_id TEXT NOT NULL,\n property_kind INTEGER NOT NULL,\n chapter_id INTEGER\n );\n\n CREATE INDEX IF NOT EXISTS idx_search_object_properties_records_owner\n ON search_object_properties_records(owner_kind, owner_id);\n\n CREATE INDEX IF NOT EXISTS idx_search_object_properties_records_chapter\n ON search_object_properties_records(chapter_id, owner_kind, owner_id);\n\n CREATE VIRTUAL TABLE IF NOT EXISTS search_object_properties_fts USING fts5(\n tier1,\n tier2,\n tier3,\n tokenize='ascii'\n );\n`;\n\nexport async function initializeDocumentSchema(\n database: Database,\n): Promise<void> {\n await migrateSerialDocumentOrder(database);\n await ensureGraphBuildParameterTable(database);\n await migrateSerialStateRevision(database);\n await migrateSerialStateKnowledgeGraphReady(database);\n await migrateSerialStateGraphParameterHashes(database);\n await ensureSerialDocumentOrderIndex(database);\n await ensureGraphBuildParameterIndexes(database);\n}\n\nasync function migrateSerialDocumentOrder(database: Database): Promise<void> {\n const columns = await listTableColumns(database, \"serials\");\n\n if (columns.has(\"document_order\")) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(database, \"serials\");\n\n if (transactionColumns.has(\"document_order\")) {\n return;\n }\n\n await database.run(`\n ALTER TABLE serials\n ADD COLUMN document_order INTEGER NOT NULL DEFAULT 0\n `);\n });\n}\n\nasync function ensureSerialDocumentOrderIndex(\n database: Database,\n): Promise<void> {\n await database.run(`\n CREATE INDEX IF NOT EXISTS idx_serials_document_order\n ON serials(document_order, id)\n `);\n}\n\nasync function migrateSerialStateRevision(database: Database): Promise<void> {\n const columns = await listTableColumns(database, \"serial_states\");\n\n if (columns.has(\"revision\")) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(\n database,\n \"serial_states\",\n );\n\n if (transactionColumns.has(\"revision\")) {\n return;\n }\n\n await database.run(`\n ALTER TABLE serial_states\n ADD COLUMN revision INTEGER NOT NULL DEFAULT 0\n `);\n });\n}\n\nasync function ensureGraphBuildParameterTable(\n database: Database,\n): Promise<void> {\n await database.run(`\n CREATE TABLE IF NOT EXISTS graph_build_parameters (\n hash TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n language TEXT,\n created_at TEXT NOT NULL\n )\n `);\n}\n\nasync function ensureGraphBuildParameterIndexes(\n database: Database,\n): Promise<void> {\n await database.run(`\n CREATE INDEX IF NOT EXISTS idx_serial_states_topology_parameter_hash\n ON serial_states(topology_parameter_hash)\n `);\n await database.run(`\n CREATE INDEX IF NOT EXISTS idx_serial_states_knowledge_graph_parameter_hash\n ON serial_states(knowledge_graph_parameter_hash)\n `);\n}\n\nasync function migrateSerialStateKnowledgeGraphReady(\n database: Database,\n): Promise<void> {\n const columns = await listTableColumns(database, \"serial_states\");\n\n if (columns.has(\"knowledge_graph_ready\")) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(\n database,\n \"serial_states\",\n );\n\n if (transactionColumns.has(\"knowledge_graph_ready\")) {\n return;\n }\n\n await database.run(`\n ALTER TABLE serial_states\n ADD COLUMN knowledge_graph_ready INTEGER NOT NULL DEFAULT 0\n `);\n });\n}\n\nasync function migrateSerialStateGraphParameterHashes(\n database: Database,\n): Promise<void> {\n const columns = await listTableColumns(database, \"serial_states\");\n const parameterColumns = [\n [\"topology_parameter_hash\", \"TEXT\"],\n [\"knowledge_graph_parameter_hash\", \"TEXT\"],\n ] as const;\n const missingColumns: Array<(typeof parameterColumns)[number]> = [];\n\n for (const column of parameterColumns) {\n if (!columns.has(column[0])) {\n missingColumns.push(column);\n }\n }\n\n if (missingColumns.length === 0) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(\n database,\n \"serial_states\",\n );\n\n for (const [name, type] of missingColumns) {\n if (transactionColumns.has(name)) {\n continue;\n }\n\n await database.run(`\n ALTER TABLE serial_states\n ADD COLUMN ${name} ${type}\n `);\n }\n });\n}\n\nasync function listTableColumns(\n database: Database,\n table: string,\n): Promise<ReadonlySet<string>> {\n const columns = await database.queryAll(\n `PRAGMA table_info(${table})`,\n undefined,\n (row) => String(row.name),\n );\n\n return new Set(columns);\n}\n","import { createHash as createNodeHash } from \"crypto\";\n\nconst UNSERIALIZABLE = Symbol(\"UNSERIALIZABLE\");\n\ntype ResolvedHashValue = boolean | number | object | string | null;\n\nexport function createHash(value: unknown): string {\n return new HashComputation().run(value);\n}\n\nclass HashComputation {\n readonly #activeObjects = new Set<object>();\n readonly #hash = createNodeHash(\"sha512\");\n readonly #resolvingObjects = new Set<object>();\n\n public run(value: unknown): string {\n const hasSerializedValue = this.#writeValue(value, \"\");\n\n if (!hasSerializedValue) {\n throw new TypeError(\"Hash input must be JSON-serializable\");\n }\n\n return this.#hash.digest(\"hex\");\n }\n\n #writeValue(value: unknown, key: string): boolean {\n const resolvedValue = this.#resolveValue(value, key);\n\n if (resolvedValue === UNSERIALIZABLE) {\n return false;\n }\n\n this.#writeResolvedValue(resolvedValue);\n return true;\n }\n\n #writeResolvedValue(value: ResolvedHashValue): void {\n if (value === null) {\n this.#hash.update(\"null\", \"utf8\");\n return;\n }\n\n if (typeof value === \"string\") {\n this.#hash.update(JSON.stringify(value), \"utf8\");\n return;\n }\n\n if (typeof value === \"number\") {\n this.#hash.update(\n Number.isFinite(value) ? String(value) : \"null\",\n \"utf8\",\n );\n return;\n }\n\n if (typeof value === \"boolean\") {\n this.#hash.update(value ? \"true\" : \"false\", \"utf8\");\n return;\n }\n\n if (this.#activeObjects.has(value)) {\n throw new TypeError(\"Hash input must be JSON-serializable\");\n }\n\n this.#activeObjects.add(value);\n\n try {\n if (Array.isArray(value)) {\n this.#writeArray(value);\n return;\n }\n\n this.#writeObject(value);\n } finally {\n this.#activeObjects.delete(value);\n }\n }\n\n #writeArray(values: readonly unknown[]): void {\n this.#hash.update(\"[\", \"utf8\");\n\n for (const [index, value] of values.entries()) {\n if (index > 0) {\n this.#hash.update(\",\", \"utf8\");\n }\n\n const resolvedValue = this.#resolveValue(value, String(index));\n\n if (resolvedValue === UNSERIALIZABLE) {\n this.#hash.update(\"null\", \"utf8\");\n continue;\n }\n\n this.#writeResolvedValue(resolvedValue);\n }\n\n this.#hash.update(\"]\", \"utf8\");\n }\n\n #writeObject(value: object): void {\n this.#hash.update(\"{\", \"utf8\");\n\n let hasWrittenProperty = false;\n\n for (const key of Object.keys(value)) {\n const propertyValue = (value as Record<string, unknown>)[key];\n const resolvedValue = this.#resolveValue(propertyValue, key);\n\n if (resolvedValue === UNSERIALIZABLE) {\n continue;\n }\n\n if (hasWrittenProperty) {\n this.#hash.update(\",\", \"utf8\");\n }\n\n this.#hash.update(JSON.stringify(key), \"utf8\");\n this.#hash.update(\":\", \"utf8\");\n this.#writeResolvedValue(resolvedValue);\n hasWrittenProperty = true;\n }\n\n this.#hash.update(\"}\", \"utf8\");\n }\n\n #resolveValue(\n value: unknown,\n key: string,\n ): ResolvedHashValue | typeof UNSERIALIZABLE {\n if (value === null) {\n return null;\n }\n\n if (typeof value === \"string\" || typeof value === \"boolean\") {\n return value;\n }\n\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n\n if (\n value === undefined ||\n typeof value === \"function\" ||\n typeof value === \"symbol\"\n ) {\n return UNSERIALIZABLE;\n }\n\n if (typeof value === \"bigint\") {\n throw new TypeError(\"Hash input must be JSON-serializable\");\n }\n\n if (\n value instanceof String ||\n value instanceof Number ||\n value instanceof Boolean\n ) {\n return this.#resolveValue(value.valueOf(), key);\n }\n\n if (!hasToJson(value)) {\n return value;\n }\n\n if (this.#resolvingObjects.has(value)) {\n throw new TypeError(\"Hash input must be JSON-serializable\");\n }\n\n this.#resolvingObjects.add(value);\n\n try {\n return this.#resolveValue(value.toJSON(key), key);\n } finally {\n this.#resolvingObjects.delete(value);\n }\n }\n}\n\nfunction hasToJson(\n value: object,\n): value is object & { toJSON(key: string): unknown } {\n return \"toJSON\" in value && typeof value.toJSON === \"function\";\n}\n","import { getNumber, getOptionalString, getString } from \"./database.js\";\nimport type { Database, SqlRow } from \"./database.js\";\nimport {\n isChunkImportance,\n isChunkRetention,\n type ChunkImportance,\n type ChunkRecord,\n type ChunkRetention,\n type CreateChunkRecord,\n type CreateSnakeRecord,\n type SentenceGroupRecord,\n type GraphBuildParameterRecord,\n type ReadingEdgeRecord,\n type MentionLinkRecord,\n type MentionRecord,\n type ObjectMetadataTarget,\n type SerialRecord,\n type SentenceId,\n type SnakeChunkRecord,\n type SnakeEdgeRecord,\n type SnakeRecord,\n} from \"./types.js\";\nimport { createHash } from \"../utils/hash.js\";\n\nconst MAX_SQL_BIND_PARAMS = 900;\n\nexport interface ReadonlySerialStore {\n getById(serialId: number): Promise<SerialRecord | undefined>;\n getRevision(serialId: number): Promise<number>;\n getRevisions(\n serialIds: readonly number[],\n ): Promise<ReadonlyMap<number, number>>;\n getMaxId(): Promise<number>;\n getChaptersRevision(): Promise<number>;\n listIds(): Promise<number[]>;\n listDocumentOrders(): Promise<ReadonlyMap<number, number>>;\n}\n\nexport interface ReadonlyGraphBuildParameterStore {\n getByHash(hash: string): Promise<GraphBuildParameterRecord | undefined>;\n}\n\nexport interface ReadonlyChunkStore {\n countAll(): Promise<number>;\n getById(chunkId: number): Promise<ChunkRecord | undefined>;\n listAll(): Promise<ChunkRecord[]>;\n listBySentenceStartIndexes(\n serialId: number,\n sentenceStartIndexes: readonly number[],\n ): Promise<ChunkRecord[]>;\n listBySentenceRange(\n serialId: number,\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<ChunkRecord[]>;\n listBySerial(serialId: number): Promise<ChunkRecord[]>;\n getMaxId(): Promise<number>;\n listFragmentPairs(): Promise<ReadonlyArray<readonly [number, number]>>;\n}\n\nexport interface ReadonlyReadingEdgeStore {\n countAll(): Promise<number>;\n listAll(): Promise<ReadingEdgeRecord[]>;\n listBySerial(serialId: number): Promise<ReadingEdgeRecord[]>;\n listIncoming(chunkId: number): Promise<ReadingEdgeRecord[]>;\n listOutgoing(chunkId: number): Promise<ReadingEdgeRecord[]>;\n}\n\nexport interface ReadonlyMentionStore {\n getById(mentionId: string): Promise<MentionRecord | undefined>;\n listAll(): Promise<MentionRecord[]>;\n listBySurfaceTerms(terms: readonly string[]): Promise<MentionRecord[]>;\n listBySurfaces(surfaces: readonly string[]): Promise<MentionRecord[]>;\n listByQid(qid: string): Promise<MentionRecord[]>;\n listByChapter(chapterId: number): Promise<MentionRecord[]>;\n}\n\nexport interface ReadonlyMentionLinkStore {\n getById(linkId: string): Promise<MentionLinkRecord | undefined>;\n listByTriple(input: {\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n }): Promise<MentionLinkRecord[]>;\n listByChapter(chapterId: number): Promise<MentionLinkRecord[]>;\n}\n\nexport interface ReadonlySnakeStore {\n getById(snakeId: number): Promise<SnakeRecord | undefined>;\n listIdsByGroup(serialId: number, groupId: number): Promise<number[]>;\n listBySerial(serialId: number): Promise<SnakeRecord[]>;\n}\n\nexport interface ReadonlySnakeChunkStore {\n listChunkIds(snakeId: number): Promise<number[]>;\n listBySnake(snakeId: number): Promise<SnakeChunkRecord[]>;\n}\n\nexport interface ReadonlySnakeEdgeStore {\n listIncoming(snakeId: number): Promise<SnakeEdgeRecord[]>;\n listOutgoing(snakeId: number): Promise<SnakeEdgeRecord[]>;\n listWithin(snakeIds: readonly number[]): Promise<SnakeEdgeRecord[]>;\n listBySerial(serialId: number): Promise<SnakeEdgeRecord[]>;\n}\n\nexport interface ReadonlyFragmentGroupStore {\n listBySerial(serialId: number): Promise<SentenceGroupRecord[]>;\n listSerialIds(): Promise<number[]>;\n listGroupIdsForSerial(serialId: number): Promise<number[]>;\n}\n\nexport interface ReadonlyObjectMetadataStore {\n getMap(objectPath: string): Promise<Readonly<Record<string, unknown>>>;\n}\n\nexport class ObjectMetadataStore implements ReadonlyObjectMetadataStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async getMap(\n objectPath: string,\n ): Promise<Readonly<Record<string, unknown>>> {\n const rows = await this.#database.queryAll(\n `\n SELECT key, value_json\n FROM object_metadata\n WHERE object_path = ?\n ORDER BY key\n `,\n [objectPath],\n (row) => ({\n key: getString(row, \"key\"),\n value: parseMetadataValue(getString(row, \"value_json\")),\n }),\n );\n const result: Record<string, unknown> = {};\n\n for (const row of rows) {\n result[row.key] = row.value;\n }\n\n return result;\n }\n\n public async replaceMap(\n target: ObjectMetadataTarget,\n map: Readonly<Record<string, unknown>>,\n ): Promise<void> {\n await this.#database.transaction(async () => {\n await this.clear(target.objectPath);\n for (const [key, value] of Object.entries(map)) {\n await this.put(target, key, value);\n }\n });\n }\n\n public async put(\n target: ObjectMetadataTarget,\n key: string,\n value: unknown,\n ): Promise<void> {\n await this.#database.run(\n `\n INSERT INTO object_metadata (\n object_kind,\n object_path,\n key,\n value_json,\n updated_at,\n chapter_id,\n chunk_id,\n entity_qid,\n triple_subject_qid,\n triple_predicate,\n triple_object_qid\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(object_path, key) DO UPDATE SET\n object_kind = excluded.object_kind,\n value_json = excluded.value_json,\n updated_at = excluded.updated_at,\n chapter_id = excluded.chapter_id,\n chunk_id = excluded.chunk_id,\n entity_qid = excluded.entity_qid,\n triple_subject_qid = excluded.triple_subject_qid,\n triple_predicate = excluded.triple_predicate,\n triple_object_qid = excluded.triple_object_qid\n `,\n [\n target.kind,\n target.objectPath,\n key,\n JSON.stringify(value),\n new Date().toISOString(),\n target.chapterId ?? null,\n target.chunkId ?? null,\n target.entityQid ?? null,\n target.tripleSubjectQid ?? null,\n target.triplePredicate ?? null,\n target.tripleObjectQid ?? null,\n ],\n );\n }\n\n public async deleteKey(objectPath: string, key: string): Promise<void> {\n await this.#database.run(\n `\n DELETE FROM object_metadata\n WHERE object_path = ?\n AND key = ?\n `,\n [objectPath, key],\n );\n }\n\n public async clear(objectPath: string): Promise<void> {\n await this.#database.run(\n `\n DELETE FROM object_metadata\n WHERE object_path = ?\n `,\n [objectPath],\n );\n }\n\n public async deleteChapterSubtree(chapterId: number): Promise<void> {\n await this.#database.run(\n `\n DELETE FROM object_metadata\n WHERE chapter_id = ?\n `,\n [chapterId],\n );\n }\n\n public async deleteDeletedChunks(): Promise<void> {\n await this.#database.run(`\n DELETE FROM object_metadata\n WHERE chunk_id IS NOT NULL\n AND NOT EXISTS (\n SELECT 1\n FROM chunks\n WHERE chunks.id = object_metadata.chunk_id\n )\n `);\n }\n\n public async deleteDeletedEntitiesAndTriples(): Promise<void> {\n await this.#database.run(`\n DELETE FROM object_metadata\n WHERE entity_qid IS NOT NULL\n AND NOT EXISTS (\n SELECT 1\n FROM mentions\n WHERE mentions.qid = object_metadata.entity_qid\n )\n `);\n await this.#database.run(`\n DELETE FROM object_metadata\n WHERE triple_subject_qid IS NOT NULL\n AND NOT EXISTS (\n SELECT 1\n FROM mention_links\n JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.qid = object_metadata.triple_subject_qid\n AND mention_links.predicate = object_metadata.triple_predicate\n AND target_mentions.qid = object_metadata.triple_object_qid\n )\n `);\n }\n}\n\nexport class SerialStore implements ReadonlySerialStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async create(): Promise<number> {\n return await this.#database.transaction(async () => {\n await this.#database.run(\n `\n INSERT INTO serials DEFAULT VALUES\n `,\n );\n\n const serialId = await this.#database.getLastInsertRowId();\n\n await this.#database.run(\n `\n INSERT INTO serial_states (\n serial_id, revision, topology_ready, knowledge_graph_ready\n )\n VALUES (?, ?, ?, ?)\n `,\n [serialId, 0, 0, 0],\n );\n\n return serialId;\n });\n }\n\n public async createWithId(serialId: number): Promise<void> {\n try {\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n INSERT INTO serials (id)\n VALUES (?)\n `,\n [serialId],\n );\n\n await this.#database.run(\n `\n INSERT INTO serial_states (\n serial_id, revision, topology_ready, knowledge_graph_ready\n )\n VALUES (?, ?, ?, ?)\n `,\n [serialId, 0, 0, 0],\n );\n });\n } catch (error) {\n if (isSqliteConstraintError(error)) {\n throw new Error(`Serial ${serialId} already exists`);\n }\n\n throw error;\n }\n }\n\n public async ensure(serialId: number): Promise<void> {\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n INSERT OR IGNORE INTO serials (id)\n VALUES (?)\n `,\n [serialId],\n );\n\n await this.#database.run(\n `\n INSERT OR IGNORE INTO serial_states (\n serial_id, revision, topology_ready, knowledge_graph_ready\n )\n VALUES (?, ?, ?, ?)\n `,\n [serialId, 0, 0, 0],\n );\n });\n }\n\n public async getById(serialId: number): Promise<SerialRecord | undefined> {\n return await this.#database.queryOne(\n `\n SELECT\n serials.id AS id,\n serials.document_order AS document_order,\n COALESCE(serial_states.revision, 0) AS revision,\n COALESCE(serial_states.topology_ready, 0) AS topology_ready,\n serial_states.topology_parameter_hash AS topology_parameter_hash,\n COALESCE(serial_states.knowledge_graph_ready, 0) AS knowledge_graph_ready,\n serial_states.knowledge_graph_parameter_hash AS knowledge_graph_parameter_hash\n FROM serials\n LEFT JOIN serial_states\n ON serial_states.serial_id = serials.id\n WHERE serials.id = ?\n `,\n [serialId],\n mapSerialRow,\n );\n }\n\n public async getRevision(serialId: number): Promise<number> {\n return (\n (await this.#database.queryOne(\n `\n SELECT COALESCE(revision, 0) AS revision\n FROM serial_states\n WHERE serial_id = ?\n `,\n [serialId],\n (row) => getNumber(row, \"revision\"),\n )) ?? 0\n );\n }\n\n public async getRevisions(\n serialIds: readonly number[],\n ): Promise<ReadonlyMap<number, number>> {\n const uniqueIds = [...new Set(serialIds)].sort(compareNumber);\n\n if (uniqueIds.length === 0) {\n return new Map();\n }\n\n const rows = await this.#database.queryAll(\n `\n SELECT serial_id, COALESCE(revision, 0) AS revision\n FROM serial_states\n WHERE serial_id IN (${uniqueIds.map(() => \"?\").join(\", \")})\n ORDER BY serial_id\n `,\n uniqueIds,\n (row) =>\n [getNumber(row, \"serial_id\"), getNumber(row, \"revision\")] as const,\n );\n\n return new Map(rows);\n }\n\n public async bumpRevision(serialId: number): Promise<void> {\n await this.ensure(serialId);\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n UPDATE serial_states\n SET revision = revision + 1\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.bumpChaptersRevision();\n });\n }\n\n public async bumpChaptersRevision(): Promise<void> {\n await this.#database.run(\n `\n INSERT INTO archive_revisions (key, value)\n VALUES ('chapters', 1)\n ON CONFLICT(key) DO UPDATE SET value = value + 1\n `,\n );\n }\n\n public async getChaptersRevision(): Promise<number> {\n return (\n (await this.#database.queryOne(\n `\n SELECT value\n FROM archive_revisions\n WHERE key = 'chapters'\n `,\n undefined,\n (row) => getNumber(row, \"value\"),\n )) ?? 0\n );\n }\n\n public async getMaxId(): Promise<number> {\n const maxId = await this.#database.queryOne(\n `\n SELECT COALESCE(MAX(id), 0) AS max_id\n FROM serials\n `,\n undefined,\n (row) => getNumber(row, \"max_id\"),\n );\n\n return maxId ?? 0;\n }\n\n public async setTopologyReady(\n serialId: number,\n ready = true,\n parameterHash?: string,\n ): Promise<void> {\n await this.ensure(serialId);\n\n if (ready) {\n await this.#database.run(\n `\n UPDATE serial_states\n SET\n topology_ready = ?,\n topology_parameter_hash = COALESCE(?, topology_parameter_hash)\n WHERE serial_id = ?\n `,\n [1, parameterHash ?? null, serialId],\n );\n return;\n }\n\n await this.#database.run(\n `\n UPDATE serial_states\n SET\n topology_ready = ?,\n topology_parameter_hash = NULL\n WHERE serial_id = ?\n `,\n [0, serialId],\n );\n }\n\n public async setKnowledgeGraphReady(\n serialId: number,\n ready = true,\n parameterHash?: string,\n ): Promise<void> {\n await this.ensure(serialId);\n\n if (ready) {\n await this.#database.run(\n `\n UPDATE serial_states\n SET\n knowledge_graph_ready = ?,\n knowledge_graph_parameter_hash = COALESCE(?, knowledge_graph_parameter_hash)\n WHERE serial_id = ?\n `,\n [1, parameterHash ?? null, serialId],\n );\n return;\n }\n\n await this.#database.run(\n `\n UPDATE serial_states\n SET\n knowledge_graph_ready = ?,\n knowledge_graph_parameter_hash = NULL\n WHERE serial_id = ?\n `,\n [0, serialId],\n );\n }\n\n public async listIds(): Promise<number[]> {\n return await this.#database.queryAll(\n `\n SELECT id\n FROM serials\n ORDER BY id\n `,\n undefined,\n (row) => getNumber(row, \"id\"),\n );\n }\n\n public async listDocumentOrders(): Promise<ReadonlyMap<number, number>> {\n const rows = await this.#database.queryAll(\n `\n SELECT id, document_order\n FROM serials\n ORDER BY id\n `,\n undefined,\n (row) =>\n [getNumber(row, \"id\"), getNumber(row, \"document_order\")] as const,\n );\n\n return new Map(rows);\n }\n\n public async setDocumentOrders(\n entries: readonly {\n readonly documentOrder: number;\n readonly serialId: number;\n }[],\n ): Promise<void> {\n await this.#database.transaction(async () => {\n for (const entry of entries) {\n await this.#database.run(\n `\n INSERT OR IGNORE INTO serials (id)\n VALUES (?)\n `,\n [entry.serialId],\n );\n await this.#database.run(\n `\n INSERT OR IGNORE INTO serial_states (\n serial_id, revision, topology_ready, knowledge_graph_ready\n )\n VALUES (?, ?, ?, ?)\n `,\n [entry.serialId, 0, 0, 0],\n );\n await this.#database.run(\n `\n UPDATE serials\n SET document_order = ?\n WHERE id = ?\n `,\n [entry.documentOrder, entry.serialId],\n );\n }\n });\n }\n}\n\nfunction mapSerialRow(row: SqlRow): SerialRecord {\n const topologyParameterHash = getOptionalString(\n row,\n \"topology_parameter_hash\",\n );\n const knowledgeGraphParameterHash = getOptionalString(\n row,\n \"knowledge_graph_parameter_hash\",\n );\n\n return {\n documentOrder: getNumber(row, \"document_order\"),\n id: getNumber(row, \"id\"),\n knowledgeGraphReady: getNumber(row, \"knowledge_graph_ready\") !== 0,\n ...(knowledgeGraphParameterHash === undefined\n ? {}\n : { knowledgeGraphParameterHash }),\n revision: getNumber(row, \"revision\"),\n topologyReady: getNumber(row, \"topology_ready\") !== 0,\n ...(topologyParameterHash === undefined ? {} : { topologyParameterHash }),\n };\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nexport class GraphBuildParameterStore implements ReadonlyGraphBuildParameterStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(input: {\n readonly language?: string;\n readonly prompt: string;\n }): Promise<GraphBuildParameterRecord> {\n const hash = createHash({\n language: input.language ?? null,\n prompt: input.prompt,\n });\n const createdAt = new Date().toISOString();\n\n await this.#database.run(\n `\n INSERT OR IGNORE INTO graph_build_parameters (\n hash, prompt, language, created_at\n )\n VALUES (?, ?, ?, ?)\n `,\n [hash, input.prompt, input.language ?? null, createdAt],\n );\n\n return (await this.getByHash(hash))!;\n }\n\n public async getByHash(\n hash: string,\n ): Promise<GraphBuildParameterRecord | undefined> {\n return await this.#database.queryOne(\n `\n SELECT hash, prompt, language, created_at\n FROM graph_build_parameters\n WHERE hash = ?\n `,\n [hash],\n (row) => {\n const language = getOptionalString(row, \"language\");\n\n return {\n createdAt: getString(row, \"created_at\"),\n hash: getString(row, \"hash\"),\n ...(language === undefined ? {} : { language }),\n prompt: getString(row, \"prompt\"),\n };\n },\n );\n }\n\n public async deleteUnreferenced(): Promise<void> {\n await this.#database.run(`\n DELETE FROM graph_build_parameters\n WHERE hash NOT IN (\n SELECT topology_parameter_hash\n FROM serial_states\n WHERE topology_parameter_hash IS NOT NULL\n UNION\n SELECT knowledge_graph_parameter_hash\n FROM serial_states\n WHERE knowledge_graph_parameter_hash IS NOT NULL\n )\n `);\n }\n}\n\nfunction isSqliteConstraintError(\n error: unknown,\n): error is { readonly code: string } {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n typeof error.code === \"string\" &&\n error.code.startsWith(\"SQLITE_CONSTRAINT\")\n );\n}\n\nexport class ChunkStore implements ReadonlyChunkStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async countAll(): Promise<number> {\n return (\n (await this.#database.queryOne(\n `\n SELECT COUNT(*) AS count\n FROM chunks\n `,\n undefined,\n (row) => getNumber(row, \"count\"),\n )) ?? 0\n );\n }\n\n public async create(record: CreateChunkRecord): Promise<ChunkRecord> {\n return await this.#database.transaction(async () => {\n await this.#database.run(\n `\n INSERT INTO chunks (\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n record.generation,\n record.sentenceId[0],\n record.sentenceId[1],\n record.label,\n record.content,\n record.retention ?? null,\n record.importance ?? null,\n record.wordsCount,\n record.weight,\n ],\n );\n\n const id = await this.#database.getLastInsertRowId();\n const createdRecord = {\n ...record,\n id,\n };\n\n await this.#replaceChunkSentences(createdRecord);\n\n return createdRecord;\n });\n }\n\n public async save(record: ChunkRecord): Promise<void> {\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO chunks (\n id,\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n record.id,\n record.generation,\n record.sentenceId[0],\n record.sentenceId[1],\n record.label,\n record.content,\n record.retention ?? null,\n record.importance ?? null,\n record.wordsCount,\n record.weight,\n ],\n );\n\n await this.#replaceChunkSentences(record);\n });\n }\n\n public async getById(chunkId: number): Promise<ChunkRecord | undefined> {\n const row = await this.#database.queryOne(\n `\n SELECT\n id,\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n FROM chunks\n WHERE id = ?\n `,\n [chunkId],\n (value) => value,\n );\n\n if (row === undefined) {\n return undefined;\n }\n\n return await this.#mapChunkRow(row);\n }\n\n public async listAll(): Promise<ChunkRecord[]> {\n const rows = await this.#database.queryAll(\n `\n SELECT\n id,\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n FROM chunks\n ORDER BY id\n `,\n undefined,\n (row) => row,\n );\n\n return await Promise.all(\n rows.map(async (row) => await this.#mapChunkRow(row)),\n );\n }\n\n public async listBySentenceStartIndexes(\n serialId: number,\n sentenceStartIndexes: readonly number[],\n ): Promise<ChunkRecord[]> {\n if (sentenceStartIndexes.length === 0) {\n return [];\n }\n\n const results = await Promise.all(\n sentenceStartIndexes.map(\n async (sentenceStartIndex) =>\n await this.listBySentenceRange(\n serialId,\n sentenceStartIndex,\n sentenceStartIndex,\n ),\n ),\n );\n\n return deduplicateById(results.flat());\n }\n\n public async listBySentenceRange(\n serialId: number,\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<ChunkRecord[]> {\n const rows = await this.#database.queryAll(\n `\n SELECT\n id,\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n FROM chunks\n WHERE serial_id = ?\n AND sentence_index BETWEEN ? AND ?\n ORDER BY id\n `,\n [serialId, startSentenceIndex, endSentenceIndex],\n (row) => row,\n );\n\n return await Promise.all(\n rows.map(async (row) => await this.#mapChunkRow(row)),\n );\n }\n\n public async listBySerial(serialId: number): Promise<ChunkRecord[]> {\n const rows = await this.#database.queryAll(\n `\n SELECT\n id,\n generation,\n serial_id,\n sentence_index,\n label,\n content,\n retention,\n importance,\n wordsCount,\n weight\n FROM chunks\n WHERE serial_id = ?\n ORDER BY id\n `,\n [serialId],\n (row) => row,\n );\n\n return await Promise.all(\n rows.map(async (row) => await this.#mapChunkRow(row)),\n );\n }\n\n public async getMaxId(): Promise<number> {\n return (\n (await this.#database.queryOne(\n `\n SELECT MAX(id) AS id\n FROM chunks\n `,\n undefined,\n (row) => {\n const value = row.id;\n\n return typeof value === \"number\" ? value : 0;\n },\n )) ?? 0\n );\n }\n\n public async listFragmentPairs(): Promise<\n ReadonlyArray<readonly [number, number]>\n > {\n return await this.#database.queryAll(\n `\n SELECT DISTINCT serial_id, sentence_index\n FROM chunks\n ORDER BY serial_id, sentence_index\n `,\n undefined,\n (row) =>\n [\n getNumber(row, \"serial_id\"),\n getNumber(row, \"sentence_index\"),\n ] as const,\n );\n }\n\n async #getSentenceIds(chunkId: number): Promise<SentenceId[]> {\n return await this.#database.queryAll(\n `\n SELECT serial_id, sentence_index\n FROM chunk_sentences\n WHERE chunk_id = ?\n ORDER BY serial_id, sentence_index\n `,\n [chunkId],\n (row) =>\n [\n getNumber(row, \"serial_id\"),\n getNumber(row, \"sentence_index\"),\n ] as const,\n );\n }\n\n async #replaceChunkSentences(record: ChunkRecord): Promise<void> {\n await this.#database.run(\n `\n DELETE FROM chunk_sentences\n WHERE chunk_id = ?\n `,\n [record.id],\n );\n\n for (const sentenceId of record.sentenceIds) {\n await this.#database.run(\n `\n INSERT INTO chunk_sentences (\n chunk_id,\n serial_id,\n sentence_index\n )\n VALUES (?, ?, ?)\n `,\n [record.id, sentenceId[0], sentenceId[1]],\n );\n }\n }\n\n async #mapChunkRow(row: SqlRow): Promise<ChunkRecord> {\n const chunkId = getNumber(row, \"id\");\n const importance = parseChunkImportance(\n getOptionalString(row, \"importance\"),\n );\n const retention = parseChunkRetention(getOptionalString(row, \"retention\"));\n\n return {\n content: getString(row, \"content\"),\n generation: getNumber(row, \"generation\"),\n id: chunkId,\n label: getString(row, \"label\"),\n sentenceId: [\n getNumber(row, \"serial_id\"),\n getNumber(row, \"sentence_index\"),\n ] as const,\n sentenceIds: await this.#getSentenceIds(chunkId),\n wordsCount: getNumber(row, \"wordsCount\"),\n weight: getNumber(row, \"weight\"),\n ...(importance === undefined ? {} : { importance }),\n ...(retention === undefined ? {} : { retention }),\n };\n }\n}\n\nexport class ReadingEdgeStore implements ReadonlyReadingEdgeStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async countAll(): Promise<number> {\n return (\n (await this.#database.queryOne(\n `\n SELECT COUNT(*) AS count\n FROM reading_edges\n `,\n undefined,\n (row) => getNumber(row, \"count\"),\n )) ?? 0\n );\n }\n\n public async save(record: ReadingEdgeRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO reading_edges (from_id, to_id, strength, weight)\n VALUES (?, ?, ?, ?)\n `,\n [record.fromId, record.toId, record.strength ?? null, record.weight],\n );\n }\n\n public async listAll(): Promise<ReadingEdgeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT from_id, to_id, strength, weight\n FROM reading_edges\n ORDER BY from_id, to_id\n `,\n undefined,\n (row) => mapReadingEdgeRow(row),\n );\n }\n\n public async listBySerial(serialId: number): Promise<ReadingEdgeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n reading_edges.from_id AS from_id,\n reading_edges.to_id AS to_id,\n reading_edges.strength AS strength,\n reading_edges.weight AS weight\n FROM reading_edges\n INNER JOIN chunks AS from_chunks\n ON from_chunks.id = reading_edges.from_id\n INNER JOIN chunks AS to_chunks\n ON to_chunks.id = reading_edges.to_id\n WHERE from_chunks.serial_id = ? AND to_chunks.serial_id = ?\n ORDER BY reading_edges.from_id, reading_edges.to_id\n `,\n [serialId, serialId],\n (row) => mapReadingEdgeRow(row),\n );\n }\n\n public async listIncoming(chunkId: number): Promise<ReadingEdgeRecord[]> {\n return await this.#listByDirection(\"to_id\", chunkId);\n }\n\n public async listOutgoing(chunkId: number): Promise<ReadingEdgeRecord[]> {\n return await this.#listByDirection(\"from_id\", chunkId);\n }\n\n async #listByDirection(\n column: \"from_id\" | \"to_id\",\n chunkId: number,\n ): Promise<ReadingEdgeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT from_id, to_id, strength, weight\n FROM reading_edges\n WHERE ${column} = ?\n ORDER BY from_id, to_id\n `,\n [chunkId],\n (row) => mapReadingEdgeRow(row),\n );\n }\n}\n\nexport class SnakeStore implements ReadonlySnakeStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async create(record: CreateSnakeRecord): Promise<number> {\n await this.#database.run(\n `\n INSERT INTO snakes (\n serial_id,\n group_id,\n local_snake_id,\n size,\n first_label,\n last_label,\n wordsCount,\n weight\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n record.serialId,\n record.groupId,\n record.localSnakeId,\n record.size,\n record.firstLabel,\n record.lastLabel,\n record.wordsCount ?? 0,\n record.weight ?? 0,\n ],\n );\n\n return await this.#database.getLastInsertRowId();\n }\n\n public async getById(snakeId: number): Promise<SnakeRecord | undefined> {\n return await this.#database.queryOne(\n `\n SELECT\n id,\n serial_id,\n group_id,\n local_snake_id,\n size,\n first_label,\n last_label,\n wordsCount,\n weight\n FROM snakes\n WHERE id = ?\n `,\n [snakeId],\n (row) => ({\n serialId: getNumber(row, \"serial_id\"),\n firstLabel: getString(row, \"first_label\"),\n groupId: getNumber(row, \"group_id\"),\n id: getNumber(row, \"id\"),\n lastLabel: getString(row, \"last_label\"),\n localSnakeId: getNumber(row, \"local_snake_id\"),\n size: getNumber(row, \"size\"),\n wordsCount: getNumber(row, \"wordsCount\"),\n weight: getNumber(row, \"weight\"),\n }),\n );\n }\n\n public async listIdsByGroup(\n serialId: number,\n groupId: number,\n ): Promise<number[]> {\n return await this.#database.queryAll(\n `\n SELECT id\n FROM snakes\n WHERE serial_id = ? AND group_id = ?\n ORDER BY id\n `,\n [serialId, groupId],\n (row) => getNumber(row, \"id\"),\n );\n }\n\n public async listBySerial(serialId: number): Promise<SnakeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n id,\n serial_id,\n group_id,\n local_snake_id,\n size,\n first_label,\n last_label,\n wordsCount,\n weight\n FROM snakes\n WHERE serial_id = ?\n ORDER BY group_id, id\n `,\n [serialId],\n (row) => ({\n serialId: getNumber(row, \"serial_id\"),\n firstLabel: getString(row, \"first_label\"),\n groupId: getNumber(row, \"group_id\"),\n id: getNumber(row, \"id\"),\n lastLabel: getString(row, \"last_label\"),\n localSnakeId: getNumber(row, \"local_snake_id\"),\n size: getNumber(row, \"size\"),\n wordsCount: getNumber(row, \"wordsCount\"),\n weight: getNumber(row, \"weight\"),\n }),\n );\n }\n}\n\nexport class MentionStore implements ReadonlyMentionStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(record: MentionRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO mentions (\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n record.id,\n record.chapterId,\n record.sentenceIndex ?? null,\n record.rangeStart,\n record.rangeEnd,\n record.surface,\n record.qid,\n record.confidence ?? null,\n record.note ?? null,\n ],\n );\n }\n\n public async saveMany(records: readonly MentionRecord[]): Promise<void> {\n await this.#database.transaction(async () => {\n for (const record of records) {\n await this.save(record);\n }\n });\n }\n\n public async getById(mentionId: string): Promise<MentionRecord | undefined> {\n return await this.#database.queryOne(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n WHERE id = ?\n `,\n [mentionId],\n mapMentionRow,\n );\n }\n\n public async listAll(): Promise<MentionRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n ORDER BY chapter_id, sentence_index, range_start, range_end, id\n `,\n undefined,\n mapMentionRow,\n );\n }\n\n public async listByQid(qid: string): Promise<MentionRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n WHERE qid = ?\n ORDER BY chapter_id, sentence_index, range_start, range_end, id\n `,\n [qid],\n mapMentionRow,\n );\n }\n\n public async listBySurfaces(\n surfaces: readonly string[],\n ): Promise<MentionRecord[]> {\n const normalizedSurfaces = [\n ...new Set(surfaces.map((surface) => surface.trim())),\n ].filter((surface) => surface !== \"\");\n\n if (normalizedSurfaces.length === 0) {\n return [];\n }\n\n return await this.#database.queryAll(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n WHERE surface IN (${normalizedSurfaces.map(() => \"?\").join(\", \")})\n ORDER BY chapter_id, sentence_index, range_start, range_end, id\n `,\n normalizedSurfaces,\n mapMentionRow,\n );\n }\n\n public async listBySurfaceTerms(\n terms: readonly string[],\n ): Promise<MentionRecord[]> {\n const normalizedTerms = [\n ...new Set(terms.map((term) => term.trim().toLowerCase())),\n ].filter((term) => term !== \"\");\n\n if (normalizedTerms.length === 0) {\n return [];\n }\n\n const filters = normalizedTerms\n .map(() => \"lower(surface) LIKE ? ESCAPE '\\\\'\")\n .join(\" OR \");\n\n return await this.#database.queryAll(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n WHERE ${filters}\n ORDER BY chapter_id, sentence_index, range_start, range_end, id\n `,\n normalizedTerms.map((term) => `%${escapeLikePattern(term)}%`),\n mapMentionRow,\n );\n }\n\n public async listByChapter(chapterId: number): Promise<MentionRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n id,\n chapter_id,\n sentence_index,\n range_start,\n range_end,\n surface,\n qid,\n confidence,\n note\n FROM mentions\n WHERE chapter_id = ?\n ORDER BY sentence_index, range_start, range_end, id\n `,\n [chapterId],\n mapMentionRow,\n );\n }\n\n public async deleteByChapter(chapterId: number): Promise<void> {\n await this.#database.run(\n `\n DELETE FROM mentions\n WHERE chapter_id = ?\n `,\n [chapterId],\n );\n }\n}\n\nexport class MentionLinkStore implements ReadonlyMentionLinkStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(record: MentionLinkRecord): Promise<void> {\n await this.#database.transaction(async () => {\n await this.#saveRecord(record);\n });\n }\n\n async #saveRecord(record: MentionLinkRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO mention_links (\n id,\n source_mention_id,\n target_mention_id,\n predicate,\n confidence,\n note\n )\n VALUES (?, ?, ?, ?, ?, ?)\n `,\n [\n record.id,\n record.sourceMentionId,\n record.targetMentionId,\n record.predicate,\n record.confidence ?? null,\n record.note ?? null,\n ],\n );\n await this.#database.run(\n `\n DELETE FROM mention_link_evidence_sentences\n WHERE link_id = ?\n `,\n [record.id],\n );\n\n for (const [chapterId, sentenceIndex] of record.evidenceSentenceIds) {\n await this.#database.run(\n `\n INSERT INTO mention_link_evidence_sentences (\n link_id,\n chapter_id,\n sentence_index\n )\n VALUES (?, ?, ?)\n `,\n [record.id, chapterId, sentenceIndex],\n );\n }\n }\n\n public async saveMany(records: readonly MentionLinkRecord[]): Promise<void> {\n await this.#database.transaction(async () => {\n for (const record of records) {\n await this.#saveRecord(record);\n }\n });\n }\n\n public async getById(linkId: string): Promise<MentionLinkRecord | undefined> {\n const row = await this.#database.queryOne(\n `\n SELECT\n id,\n source_mention_id,\n target_mention_id,\n predicate,\n confidence,\n note\n FROM mention_links\n WHERE id = ?\n `,\n [linkId],\n mapMentionLinkRow,\n );\n\n return row === undefined ? undefined : await this.#hydrateEvidence(row);\n }\n\n public async listByTriple(input: {\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n }): Promise<MentionLinkRecord[]> {\n const rows = await this.#database.queryAll(\n `\n SELECT\n mention_links.id AS id,\n mention_links.source_mention_id AS source_mention_id,\n mention_links.target_mention_id AS target_mention_id,\n mention_links.predicate AS predicate,\n mention_links.confidence AS confidence,\n mention_links.note AS note\n FROM mention_links\n INNER JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n INNER JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.qid = ?\n AND mention_links.predicate = ?\n AND target_mentions.qid = ?\n ORDER BY\n source_mentions.chapter_id,\n source_mentions.sentence_index,\n mention_links.id\n `,\n [input.subjectQid, input.predicate, input.objectQid],\n mapMentionLinkRow,\n );\n\n return await this.#hydrateEvidenceMany(rows);\n }\n\n public async listByChapter(chapterId: number): Promise<MentionLinkRecord[]> {\n const rows = await this.#database.queryAll(\n `\n SELECT\n mention_links.id AS id,\n mention_links.source_mention_id AS source_mention_id,\n mention_links.target_mention_id AS target_mention_id,\n mention_links.predicate AS predicate,\n mention_links.confidence AS confidence,\n mention_links.note AS note\n FROM mention_links\n INNER JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n INNER JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.chapter_id = ?\n OR target_mentions.chapter_id = ?\n ORDER BY mention_links.id\n `,\n [chapterId, chapterId],\n mapMentionLinkRow,\n );\n\n return await this.#hydrateEvidenceMany(rows);\n }\n\n public async deleteByChapter(chapterId: number): Promise<void> {\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n DELETE FROM mention_link_evidence_sentences\n WHERE link_id IN (\n SELECT mention_links.id\n FROM mention_links\n INNER JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n INNER JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.chapter_id = ?\n OR target_mentions.chapter_id = ?\n )\n `,\n [chapterId, chapterId],\n );\n await this.#database.run(\n `\n DELETE FROM mention_links\n WHERE source_mention_id IN (\n SELECT id\n FROM mentions\n WHERE chapter_id = ?\n )\n OR target_mention_id IN (\n SELECT id\n FROM mentions\n WHERE chapter_id = ?\n )\n `,\n [chapterId, chapterId],\n );\n });\n }\n\n async #hydrateEvidenceMany(\n records: readonly MentionLinkRecord[],\n ): Promise<MentionLinkRecord[]> {\n if (records.length === 0) {\n return [];\n }\n\n const sentenceIdsByLinkId = new Map<string, SentenceId[]>();\n const linkIds = [...new Set(records.map((record) => record.id))];\n\n for (const linkIdBatch of chunkArray(linkIds, MAX_SQL_BIND_PARAMS)) {\n const placeholders = linkIdBatch.map(() => \"?\").join(\", \");\n const rows = await this.#database.queryAll(\n `\n SELECT link_id, chapter_id, sentence_index\n FROM mention_link_evidence_sentences\n WHERE link_id IN (${placeholders})\n ORDER BY link_id, chapter_id, sentence_index\n `,\n linkIdBatch,\n (row) => ({\n linkId: getString(row, \"link_id\"),\n sentenceId: [\n getNumber(row, \"chapter_id\"),\n getNumber(row, \"sentence_index\"),\n ] as SentenceId,\n }),\n );\n\n for (const row of rows) {\n const sentenceIds = sentenceIdsByLinkId.get(row.linkId) ?? [];\n\n sentenceIds.push(row.sentenceId);\n sentenceIdsByLinkId.set(row.linkId, sentenceIds);\n }\n }\n\n return records.map((record) => ({\n ...record,\n evidenceSentenceIds: sentenceIdsByLinkId.get(record.id) ?? [],\n }));\n }\n\n async #hydrateEvidence(\n record: MentionLinkRecord,\n ): Promise<MentionLinkRecord> {\n const evidenceSentenceIds = await this.#database.queryAll(\n `\n SELECT chapter_id, sentence_index\n FROM mention_link_evidence_sentences\n WHERE link_id = ?\n ORDER BY chapter_id, sentence_index\n `,\n [record.id],\n (row): SentenceId => [\n getNumber(row, \"chapter_id\"),\n getNumber(row, \"sentence_index\"),\n ],\n );\n\n return {\n ...record,\n evidenceSentenceIds,\n };\n }\n}\n\nfunction chunkArray<T>(items: readonly T[], size: number): T[][] {\n const chunks: T[][] = [];\n\n for (let index = 0; index < items.length; index += size) {\n chunks.push(items.slice(index, index + size));\n }\n\n return chunks;\n}\n\nexport class SnakeChunkStore implements ReadonlySnakeChunkStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(record: SnakeChunkRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO snake_chunks (snake_id, chunk_id, position)\n VALUES (?, ?, ?)\n `,\n [record.snakeId, record.chunkId, record.position],\n );\n }\n\n public async listChunkIds(snakeId: number): Promise<number[]> {\n return await this.#database.queryAll(\n `\n SELECT chunk_id\n FROM snake_chunks\n WHERE snake_id = ?\n ORDER BY position\n `,\n [snakeId],\n (row) => getNumber(row, \"chunk_id\"),\n );\n }\n\n public async listBySnake(snakeId: number): Promise<SnakeChunkRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT snake_id, chunk_id, position\n FROM snake_chunks\n WHERE snake_id = ?\n ORDER BY position\n `,\n [snakeId],\n (row) => ({\n chunkId: getNumber(row, \"chunk_id\"),\n position: getNumber(row, \"position\"),\n snakeId: getNumber(row, \"snake_id\"),\n }),\n );\n }\n}\n\nexport class SnakeEdgeStore implements ReadonlySnakeEdgeStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(record: SnakeEdgeRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO snake_edges (from_snake_id, to_snake_id, weight)\n VALUES (?, ?, ?)\n `,\n [record.fromSnakeId, record.toSnakeId, record.weight],\n );\n }\n\n public async listIncoming(snakeId: number): Promise<SnakeEdgeRecord[]> {\n return await this.#listByDirection(\"to_snake_id\", snakeId);\n }\n\n public async listOutgoing(snakeId: number): Promise<SnakeEdgeRecord[]> {\n return await this.#listByDirection(\"from_snake_id\", snakeId);\n }\n\n public async listWithin(\n snakeIds: readonly number[],\n ): Promise<SnakeEdgeRecord[]> {\n if (snakeIds.length === 0) {\n return [];\n }\n\n const placeholders = snakeIds.map(() => \"?\").join(\", \");\n\n return await this.#database.queryAll(\n `\n SELECT from_snake_id, to_snake_id, weight\n FROM snake_edges\n WHERE from_snake_id IN (${placeholders})\n AND to_snake_id IN (${placeholders})\n ORDER BY from_snake_id, to_snake_id\n `,\n [...snakeIds, ...snakeIds],\n (row) => ({\n fromSnakeId: getNumber(row, \"from_snake_id\"),\n toSnakeId: getNumber(row, \"to_snake_id\"),\n weight: getNumber(row, \"weight\"),\n }),\n );\n }\n\n public async listBySerial(serialId: number): Promise<SnakeEdgeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT\n snake_edges.from_snake_id AS from_snake_id,\n snake_edges.to_snake_id AS to_snake_id,\n snake_edges.weight AS weight\n FROM snake_edges\n INNER JOIN snakes AS from_snakes\n ON from_snakes.id = snake_edges.from_snake_id\n INNER JOIN snakes AS to_snakes\n ON to_snakes.id = snake_edges.to_snake_id\n WHERE from_snakes.serial_id = ? AND to_snakes.serial_id = ?\n ORDER BY snake_edges.from_snake_id, snake_edges.to_snake_id\n `,\n [serialId, serialId],\n (row) => ({\n fromSnakeId: getNumber(row, \"from_snake_id\"),\n toSnakeId: getNumber(row, \"to_snake_id\"),\n weight: getNumber(row, \"weight\"),\n }),\n );\n }\n\n async #listByDirection(\n column: \"from_snake_id\" | \"to_snake_id\",\n snakeId: number,\n ): Promise<SnakeEdgeRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT from_snake_id, to_snake_id, weight\n FROM snake_edges\n WHERE ${column} = ?\n ORDER BY from_snake_id, to_snake_id\n `,\n [snakeId],\n (row) => ({\n fromSnakeId: getNumber(row, \"from_snake_id\"),\n toSnakeId: getNumber(row, \"to_snake_id\"),\n weight: getNumber(row, \"weight\"),\n }),\n );\n }\n}\n\nexport class FragmentGroupStore implements ReadonlyFragmentGroupStore {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public async save(record: SentenceGroupRecord): Promise<void> {\n await this.#database.run(\n `\n INSERT OR REPLACE INTO sentence_groups (\n serial_id,\n group_id,\n start_sentence_index,\n end_sentence_index\n )\n VALUES (?, ?, ?, ?)\n `,\n [\n record.serialId,\n record.groupId,\n record.startSentenceIndex,\n record.endSentenceIndex,\n ],\n );\n }\n\n public async saveMany(\n records: readonly SentenceGroupRecord[],\n ): Promise<void> {\n await this.#database.transaction(async () => {\n for (const record of records) {\n await this.save(record);\n }\n });\n }\n\n public async listBySerial(serialId: number): Promise<SentenceGroupRecord[]> {\n return await this.#database.queryAll(\n `\n SELECT serial_id, group_id, start_sentence_index, end_sentence_index\n FROM sentence_groups\n WHERE serial_id = ?\n ORDER BY group_id, start_sentence_index\n `,\n [serialId],\n (row) => ({\n serialId: getNumber(row, \"serial_id\"),\n groupId: getNumber(row, \"group_id\"),\n startSentenceIndex: getNumber(row, \"start_sentence_index\"),\n endSentenceIndex: getNumber(row, \"end_sentence_index\"),\n }),\n );\n }\n\n public async listSerialIds(): Promise<number[]> {\n return await this.#database.queryAll(\n `\n SELECT DISTINCT serial_id\n FROM sentence_groups\n ORDER BY serial_id\n `,\n undefined,\n (row) => getNumber(row, \"serial_id\"),\n );\n }\n\n public async listGroupIdsForSerial(serialId: number): Promise<number[]> {\n return await this.#database.queryAll(\n `\n SELECT DISTINCT group_id\n FROM sentence_groups\n WHERE serial_id = ?\n ORDER BY group_id\n `,\n [serialId],\n (row) => getNumber(row, \"group_id\"),\n );\n }\n}\n\nfunction mapReadingEdgeRow(row: SqlRow): ReadingEdgeRecord {\n const strength = getOptionalString(row, \"strength\");\n\n return {\n fromId: getNumber(row, \"from_id\"),\n toId: getNumber(row, \"to_id\"),\n weight: getNumber(row, \"weight\"),\n ...(strength === undefined ? {} : { strength }),\n };\n}\n\nfunction mapMentionRow(row: SqlRow): MentionRecord {\n const sentenceIndex = getOptionalNumber(row, \"sentence_index\");\n const confidence = getOptionalNumber(row, \"confidence\");\n const note = getOptionalString(row, \"note\");\n\n return {\n chapterId: getNumber(row, \"chapter_id\"),\n ...(confidence === undefined ? {} : { confidence }),\n id: getString(row, \"id\"),\n ...(note === undefined ? {} : { note }),\n qid: getString(row, \"qid\"),\n rangeEnd: getNumber(row, \"range_end\"),\n rangeStart: getNumber(row, \"range_start\"),\n ...(sentenceIndex === undefined ? {} : { sentenceIndex }),\n surface: getString(row, \"surface\"),\n };\n}\n\nfunction deduplicateById<T extends { readonly id: number }>(\n records: readonly T[],\n): T[] {\n const seen = new Set<number>();\n const result: T[] = [];\n\n for (const record of records) {\n if (seen.has(record.id)) {\n continue;\n }\n\n seen.add(record.id);\n result.push(record);\n }\n\n return result;\n}\n\nfunction mapMentionLinkRow(row: SqlRow): MentionLinkRecord {\n const confidence = getOptionalNumber(row, \"confidence\");\n const note = getOptionalString(row, \"note\");\n\n return {\n ...(confidence === undefined ? {} : { confidence }),\n evidenceSentenceIds: [],\n id: getString(row, \"id\"),\n ...(note === undefined ? {} : { note }),\n predicate: getString(row, \"predicate\"),\n sourceMentionId: getString(row, \"source_mention_id\"),\n targetMentionId: getString(row, \"target_mention_id\"),\n };\n}\n\nfunction parseMetadataValue(valueJson: string): unknown {\n try {\n return JSON.parse(valueJson);\n } catch (error) {\n throw new Error(\n `Invalid object metadata JSON: ${formatUnknownError(error)}`,\n );\n }\n}\n\nfunction getOptionalNumber(row: SqlRow, key: string): number | undefined {\n const value = row[key];\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value !== \"number\") {\n throw new TypeError(`Expected ${key} to be a number`);\n }\n\n return value;\n}\n\nfunction formatUnknownError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction escapeLikePattern(value: string): string {\n return value.replace(/[\\\\%_]/gu, (character) => `\\\\${character}`);\n}\n\nfunction parseChunkImportance(\n value: string | undefined,\n): ChunkImportance | undefined {\n return value !== undefined && isChunkImportance(value) ? value : undefined;\n}\n\nfunction parseChunkRetention(\n value: string | undefined,\n): ChunkRetention | undefined {\n return value !== undefined && isChunkRetention(value) ? value : undefined;\n}\n","import { AsyncLocalStorage } from \"async_hooks\";\nimport { mkdir, readFile, readdir, rm, unlink, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport { z } from \"zod\";\n\nimport { bookMetaSchema, type BookMeta } from \"../source/meta.js\";\nimport { tocFileSchema, type TocFile, type TocItem } from \"../source/toc.js\";\nimport type { SourceAsset } from \"../source/types.js\";\nimport { isNodeError } from \"../utils/node-error.js\";\nimport { Database } from \"./database.js\";\nimport type { Database as DocumentDatabase } from \"./database.js\";\nimport {\n TextStreams,\n type ReadonlySerialTextStream,\n type SerialTextStream,\n} from \"./text-streams.js\";\nimport {\n initializeDocumentSchema,\n SCHEMA_SQL,\n SEARCH_INDEX_SCHEMA_SQL,\n} from \"./schema.js\";\nimport {\n ChunkStore,\n FragmentGroupStore,\n GraphBuildParameterStore,\n ReadingEdgeStore,\n MentionLinkStore,\n MentionStore,\n ObjectMetadataStore,\n type ReadonlyChunkStore,\n type ReadonlyFragmentGroupStore,\n type ReadonlyGraphBuildParameterStore,\n type ReadonlyReadingEdgeStore,\n type ReadonlyMentionLinkStore,\n type ReadonlyMentionStore,\n type ReadonlyObjectMetadataStore,\n type ReadonlySerialStore,\n type ReadonlySnakeChunkStore,\n type ReadonlySnakeEdgeStore,\n type ReadonlySnakeStore,\n SerialStore,\n SnakeChunkStore,\n SnakeEdgeStore,\n SnakeStore,\n} from \"./stores.js\";\nimport { ObjectMetadataKind, type SentenceId } from \"./types.js\";\n\nexport interface DocumentFileStore {\n close(): Promise<void>;\n deleteFile(path: string): Promise<void>;\n deleteTree(path: string): Promise<void>;\n ensureDirectory(path: string): Promise<void>;\n initializeDatabaseSchema(): boolean;\n markDatabaseDirty?(): void;\n markSearchIndexDatabaseDirty?(): void;\n openDatabaseReadonly(): boolean;\n listFileContents?(path: string): Promise<ReadonlyMap<string, Uint8Array>>;\n listFiles(path: string): Promise<readonly string[]>;\n readFile(path: string): Promise<Uint8Array | undefined>;\n resolveDatabasePath(documentPath: string): Promise<string>;\n resolveSearchIndexDatabasePath?(documentPath: string): Promise<string>;\n writeFile(\n path: string,\n content: string | Uint8Array,\n options: { readonly overwrite?: boolean },\n ): Promise<void>;\n}\n\nconst LOCAL_DOCUMENT_FILE_STORE: DocumentFileStore = {\n close: () => Promise.resolve(),\n deleteFile: async (path) => {\n await unlink(path);\n },\n deleteTree: async (path) => {\n await rm(path, { force: true, recursive: true });\n },\n ensureDirectory: async (path) => {\n await mkdir(path, { recursive: true });\n },\n initializeDatabaseSchema: () => true,\n markDatabaseDirty: () => undefined,\n markSearchIndexDatabaseDirty: () => undefined,\n openDatabaseReadonly: () => false,\n listFiles: async (path) =>\n (await readdir(path, { withFileTypes: true }))\n .filter((entry) => entry.isFile())\n .map((entry) => entry.name),\n readFile: async (path) => {\n try {\n return await readFile(path);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n }\n },\n resolveDatabasePath: (documentPath) =>\n Promise.resolve(join(documentPath, \"database.db\")),\n resolveSearchIndexDatabasePath: (documentPath) =>\n Promise.resolve(join(documentPath, \"fts.db\")),\n writeFile: async (path, content, options) => {\n if (typeof content === \"string\") {\n await writeFile(path, content, {\n encoding: \"utf8\",\n flag: options.overwrite === true ? \"w\" : \"wx\",\n });\n } else {\n await writeFile(path, content, {\n flag: options.overwrite === true ? \"w\" : \"wx\",\n });\n }\n },\n};\n\nconst coverFileSchema = z.object({\n mediaType: z.string().min(1),\n path: z.string(),\n});\n\nexport interface ReadonlyDocument {\n readonly chunks: ReadonlyChunkStore;\n readonly fragmentGroups: ReadonlyFragmentGroupStore;\n readonly graphBuildParameters: ReadonlyGraphBuildParameterStore;\n readonly readingEdges: ReadonlyReadingEdgeStore;\n readonly mentionLinks: ReadonlyMentionLinkStore;\n readonly mentions: ReadonlyMentionStore;\n readonly metadata: ReadonlyObjectMetadataStore;\n readonly serials: ReadonlySerialStore;\n readonly snakeChunks: ReadonlySnakeChunkStore;\n readonly snakeEdges: ReadonlySnakeEdgeStore;\n readonly snakes: ReadonlySnakeStore;\n\n getSentence(sentenceId: SentenceId): Promise<string>;\n getSerialFragments(serialId: number): ReadonlySerialTextStream;\n getSummaryFragments(serialId: number): ReadonlySerialTextStream;\n openSession<T>(\n operation: (document: ReadonlyDocument) => Promise<T> | T,\n ): Promise<T>;\n readDatabase<T>(\n operation: (database: DocumentDatabase) => Promise<T> | T,\n ): Promise<T>;\n readSearchIndexDatabase<T>(\n operation: (database: DocumentDatabase) => Promise<T> | T,\n ): Promise<T>;\n readBookMeta(): Promise<BookMeta | undefined>;\n readCover(): Promise<SourceAsset | undefined>;\n readSummary(serialId: number): Promise<string | undefined>;\n readToc(): Promise<TocFile | undefined>;\n release(): Promise<void>;\n}\n\nexport interface DocumentContext {\n complete(): void;\n dispose(): Promise<void>;\n ownSerial(serialId: number): void;\n run<T>(operation: () => Promise<T> | T): Promise<T>;\n}\n\nexport interface Document extends ReadonlyDocument {\n readonly chunks: ChunkStore;\n readonly fragmentGroups: FragmentGroupStore;\n readonly graphBuildParameters: GraphBuildParameterStore;\n readonly readingEdges: ReadingEdgeStore;\n readonly mentionLinks: MentionLinkStore;\n readonly mentions: MentionStore;\n readonly metadata: ObjectMetadataStore;\n readonly serials: SerialStore;\n readonly snakeChunks: SnakeChunkStore;\n readonly snakeEdges: SnakeEdgeStore;\n readonly snakes: SnakeStore;\n\n createContext(): DocumentContext;\n getSerialFragments(serialId: number): SerialTextStream;\n getSummaryFragments(serialId: number): SerialTextStream;\n createSerial(): Promise<number>;\n clearSerialGraph(serialId: number): Promise<void>;\n clearSerialSource(serialId: number): Promise<void>;\n deleteSerial(serialId: number): Promise<void>;\n deleteSearchIndexDatabase(): Promise<void>;\n deleteSummary(serialId: number): Promise<void>;\n flush(): Promise<void>;\n openSession<T>(operation: (document: Document) => Promise<T> | T): Promise<T>;\n peekNextSerialId(): Promise<number>;\n replaceBookMeta(meta: BookMeta): Promise<void>;\n replaceToc(toc: TocFile): Promise<void>;\n writeBookMeta(meta: BookMeta): Promise<void>;\n writeCover(cover: SourceAsset): Promise<void>;\n writeSummary(serialId: number, summary: string): Promise<void>;\n writeToc(toc: TocFile): Promise<void>;\n writeSearchIndexDatabase<T>(\n operation: (database: DocumentDatabase) => Promise<T> | T,\n ): Promise<T>;\n}\n\nexport class DirectoryDocument implements Document {\n public readonly chunks: ChunkStore;\n public readonly fragmentGroups: FragmentGroupStore;\n public readonly graphBuildParameters: GraphBuildParameterStore;\n public readonly readingEdges: ReadingEdgeStore;\n public readonly mentionLinks: MentionLinkStore;\n public readonly mentions: MentionStore;\n public readonly metadata: ObjectMetadataStore;\n public readonly path: string;\n public readonly serials: SerialStore;\n public readonly snakeChunks: SnakeChunkStore;\n public readonly snakeEdges: SnakeEdgeStore;\n public readonly snakes: SnakeStore;\n\n readonly #database: Database;\n readonly #fileStore: DocumentFileStore;\n readonly #textStreams: TextStreams;\n readonly #contextScope = new AsyncLocalStorage<DirectoryDocumentContext>();\n\n public constructor(\n database: Database,\n textStreams: TextStreams,\n path: string,\n fileStore: DocumentFileStore = LOCAL_DOCUMENT_FILE_STORE,\n ) {\n this.#database = database;\n this.#fileStore = fileStore;\n this.#textStreams = textStreams;\n this.chunks = new ChunkStore(database);\n this.fragmentGroups = new FragmentGroupStore(database);\n this.graphBuildParameters = new GraphBuildParameterStore(database);\n this.readingEdges = new ReadingEdgeStore(database);\n this.mentionLinks = new MentionLinkStore(database);\n this.mentions = new MentionStore(database);\n this.metadata = new ObjectMetadataStore(database);\n this.path = path;\n this.serials = new SerialStore(database);\n this.snakeChunks = new SnakeChunkStore(database);\n this.snakeEdges = new SnakeEdgeStore(database);\n this.snakes = new SnakeStore(database);\n }\n\n public static async open(\n documentPath: string,\n options: { readonly fileStore?: DocumentFileStore } = {},\n ): Promise<DirectoryDocument> {\n const resolvedDocumentPath = resolve(documentPath);\n const fileStore = options.fileStore ?? LOCAL_DOCUMENT_FILE_STORE;\n try {\n const databasePath =\n await fileStore.resolveDatabasePath(resolvedDocumentPath);\n await fileStore.ensureDirectory(resolvedDocumentPath);\n\n const shouldInitializeDatabaseSchema =\n fileStore.initializeDatabaseSchema();\n const database = await Database.open(\n databasePath,\n shouldInitializeDatabaseSchema ? SCHEMA_SQL : \"\",\n {\n onWrite: () => {\n fileStore.markDatabaseDirty?.();\n },\n readonly: fileStore.openDatabaseReadonly(),\n },\n );\n if (shouldInitializeDatabaseSchema) {\n await initializeDocumentSchema(database);\n }\n const textStreams = new TextStreams(resolvedDocumentPath, database, {\n deleteTree: async (path) => {\n await fileStore.deleteTree(path);\n },\n ensureDirectory: async (path) => {\n await fileStore.ensureDirectory(path);\n },\n listFiles: async (path) => await fileStore.listFiles(path),\n readFile: async (path) => await fileStore.readFile(path),\n writeFile: async (path, content, options) => {\n await fileStore.writeFile(path, content, options);\n },\n });\n await textStreams.ensureCreated();\n\n const document = new DirectoryDocument(\n database,\n textStreams,\n resolvedDocumentPath,\n fileStore,\n );\n\n return document;\n } catch (error) {\n await fileStore.close();\n throw error;\n }\n }\n\n public static async openSession<T>(\n documentPath: string,\n operation: (document: DirectoryDocument) => Promise<T> | T,\n ): Promise<T> {\n const document = await DirectoryDocument.open(documentPath);\n\n try {\n return await document.openSession(async () => await operation(document));\n } finally {\n await document.release();\n }\n }\n\n public getSerialFragments(serialId: number): SerialTextStream {\n return this.#textStreams.getSerial(serialId);\n }\n\n public getSummaryFragments(serialId: number): SerialTextStream {\n return this.#textStreams.getSummarySerial(serialId);\n }\n\n public createContext(): DocumentContext {\n return new DirectoryDocumentContext(this);\n }\n\n public async createSerial(): Promise<number> {\n const serialId = await this.serials.create();\n\n this.#contextScope.getStore()?.ownSerial(serialId);\n return serialId;\n }\n\n public async clearSerialGraph(serialId: number): Promise<void> {\n await this.deleteSummary(serialId);\n await this.#deleteSerialGraphRecords(serialId);\n await this.serials.setTopologyReady(serialId, false);\n await this.serials.setKnowledgeGraphReady(serialId, false);\n await this.serials.bumpRevision(serialId);\n await this.graphBuildParameters.deleteUnreferenced();\n }\n\n public async clearSerialSource(serialId: number): Promise<void> {\n await this.clearSerialGraph(serialId);\n await this.#textStreams.getSerial(serialId).delete();\n await this.serials.bumpRevision(serialId);\n }\n\n public async deleteSerial(serialId: number): Promise<void> {\n await this.#deleteSerialResources(serialId);\n }\n\n public async deleteSummary(serialId: number): Promise<void> {\n await this.#textStreams.getSummarySerial(serialId).delete();\n }\n\n public async getSentence(sentenceId: SentenceId): Promise<string> {\n return await this.#textStreams.getSentence(sentenceId);\n }\n\n public async openSession<T>(\n operation: (document: Document) => Promise<T> | T,\n ): Promise<T> {\n const activeContext = this.#contextScope.getStore();\n\n if (activeContext !== undefined) {\n return await operation(this);\n }\n\n const context = new DirectoryDocumentContext(this);\n\n try {\n const result = await this.#database.transaction(\n async () => await context.run(async () => await operation(this)),\n );\n\n context.complete();\n return result;\n } finally {\n await context.dispose();\n }\n }\n\n public async readDatabase<T>(\n operation: (database: Database) => Promise<T> | T,\n ): Promise<T> {\n return await operation(this.#database);\n }\n\n public async readSearchIndexDatabase<T>(\n operation: (database: Database) => Promise<T> | T,\n ): Promise<T> {\n return await this.#openSearchIndexDatabase(true, operation);\n }\n\n public async writeSearchIndexDatabase<T>(\n operation: (database: Database) => Promise<T> | T,\n ): Promise<T> {\n return await this.#openSearchIndexDatabase(false, operation);\n }\n\n public async deleteSearchIndexDatabase(): Promise<void> {\n await this.#fileStore.deleteFile(join(this.path, \"fts.db\"));\n }\n\n public async peekNextSerialId(): Promise<number> {\n return (await this.serials.getMaxId()) + 1;\n }\n\n public async readBookMeta(): Promise<BookMeta | undefined> {\n const map = await this.metadata.getMap(\"\");\n\n return Object.keys(map).length === 0\n ? undefined\n : bookMetaSchema.parse(map);\n }\n\n public async readCover(): Promise<SourceAsset | undefined> {\n const coverFile = await this.#readJsonFile(\n this.#getCoverInfoPath(),\n (value) => coverFileSchema.parse(value),\n );\n\n if (coverFile === undefined) {\n return undefined;\n }\n\n const data = await this.#readOptionalFile(this.#getCoverDataPath());\n\n if (data === undefined) {\n throw new Error(\"Cover data is missing\");\n }\n\n return {\n data,\n mediaType: coverFile.mediaType,\n path: coverFile.path,\n };\n }\n\n public async readSummary(serialId: number): Promise<string | undefined> {\n return await this.#textStreams.getSummarySerial(serialId).readText();\n }\n\n public async readToc(): Promise<TocFile | undefined> {\n return await this.#readJsonFile(this.#getTocPath(), (value) =>\n tocFileSchema.parse(value),\n );\n }\n\n public async writeBookMeta(meta: BookMeta): Promise<void> {\n if (Object.keys(await this.metadata.getMap(\"\")).length > 0) {\n throw new Error(\"Archive metadata already exists.\");\n }\n\n await this.replaceBookMeta(meta);\n }\n\n public async replaceBookMeta(meta: BookMeta): Promise<void> {\n await this.metadata.replaceMap(\n {\n kind: ObjectMetadataKind.Archive,\n objectPath: \"\",\n },\n {\n ...meta,\n },\n );\n }\n\n public async writeCover(cover: SourceAsset): Promise<void> {\n await this.#fileStore.ensureDirectory(this.#getCoverDirectoryPath());\n await this.#writeJsonFile(this.#getCoverInfoPath(), {\n mediaType: cover.mediaType,\n path: cover.path,\n });\n await this.#writeNewFile(this.#getCoverDataPath(), cover.data);\n }\n\n public async writeSummary(serialId: number, summary: string): Promise<void> {\n await this.deleteSummary(serialId);\n await this.#textStreams.getSummarySerial(serialId).writeTextStream(summary);\n await this.serials.bumpRevision(serialId);\n }\n\n public async writeToc(toc: TocFile): Promise<void> {\n await this.#writeJsonFile(this.#getTocPath(), toc);\n await this.#replaceDocumentOrder(toc);\n await this.serials.bumpChaptersRevision();\n }\n\n public async replaceToc(toc: TocFile): Promise<void> {\n await this.#writeJsonFile(this.#getTocPath(), toc, { overwrite: true });\n await this.#replaceDocumentOrder(toc);\n await this.serials.bumpChaptersRevision();\n }\n\n public async flush(): Promise<void> {\n await this.#database.flush();\n }\n\n public async release(): Promise<void> {\n try {\n await this.flush();\n await this.#database.close();\n } finally {\n await this.#fileStore.close();\n }\n }\n\n public async close(): Promise<void> {\n await this.release();\n }\n\n async #openSearchIndexDatabase<T>(\n readonly: boolean,\n operation: (database: Database) => Promise<T> | T,\n ): Promise<T> {\n const databasePath =\n this.#fileStore.resolveSearchIndexDatabasePath === undefined\n ? join(this.path, \"fts.db\")\n : await this.#fileStore.resolveSearchIndexDatabasePath(this.path);\n const database = await Database.open(\n databasePath,\n readonly ? \"\" : SEARCH_INDEX_SCHEMA_SQL,\n {\n onWrite: () => {\n this.#fileStore.markSearchIndexDatabaseDirty?.();\n },\n readonly,\n },\n );\n\n try {\n return await operation(database);\n } finally {\n await database.close();\n }\n }\n\n async #rollbackContext(context: DirectoryDocumentContext): Promise<void> {\n await this.#rollbackOwnedSerials(context.listOwnedSerialIds());\n await this.#rollbackCreatedFiles(context.listCreatedFilePaths());\n }\n\n async #rollbackCreatedFiles(\n createdFilePaths: readonly string[],\n ): Promise<void> {\n for (const path of [...createdFilePaths].reverse()) {\n try {\n await this.#fileStore.deleteFile(path);\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n continue;\n }\n\n throw error;\n }\n }\n }\n\n async #rollbackOwnedSerials(serialIds: readonly number[]): Promise<void> {\n for (const serialId of [...serialIds].sort(compareNumberDescending)) {\n await this.#deleteSerialResources(serialId);\n }\n }\n\n async #deleteSerialResources(serialId: number): Promise<void> {\n await this.metadata.deleteChapterSubtree(serialId);\n await this.#deleteSerialGraphRecords(serialId);\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n DELETE FROM serial_states\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM serials\n WHERE id = ?\n `,\n [serialId],\n );\n });\n\n await this.#textStreams.getSerial(serialId).delete();\n await this.deleteSummary(serialId);\n await this.serials.bumpChaptersRevision();\n await this.graphBuildParameters.deleteUnreferenced();\n }\n\n async #deleteSerialGraphRecords(serialId: number): Promise<void> {\n await this.#database.transaction(async () => {\n await this.#database.run(\n `\n DELETE FROM mention_link_evidence_sentences\n WHERE link_id IN (\n SELECT mention_links.id\n FROM mention_links\n INNER JOIN mentions AS source_mentions\n ON source_mentions.id = mention_links.source_mention_id\n INNER JOIN mentions AS target_mentions\n ON target_mentions.id = mention_links.target_mention_id\n WHERE source_mentions.chapter_id = ?\n OR target_mentions.chapter_id = ?\n )\n `,\n [serialId, serialId],\n );\n await this.#database.run(\n `\n DELETE FROM mention_links\n WHERE source_mention_id IN (\n SELECT id\n FROM mentions\n WHERE chapter_id = ?\n ) OR target_mention_id IN (\n SELECT id\n FROM mentions\n WHERE chapter_id = ?\n )\n `,\n [serialId, serialId],\n );\n await this.#database.run(\n `\n DELETE FROM mentions\n WHERE chapter_id = ?\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM snake_edges\n WHERE from_snake_id IN (\n SELECT id\n FROM snakes\n WHERE serial_id = ?\n ) OR to_snake_id IN (\n SELECT id\n FROM snakes\n WHERE serial_id = ?\n )\n `,\n [serialId, serialId],\n );\n await this.#database.run(\n `\n DELETE FROM snake_chunks\n WHERE snake_id IN (\n SELECT id\n FROM snakes\n WHERE serial_id = ?\n )\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM snakes\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM sentence_groups\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM reading_edges\n WHERE from_id IN (\n SELECT id\n FROM chunks\n WHERE serial_id = ?\n ) OR to_id IN (\n SELECT id\n FROM chunks\n WHERE serial_id = ?\n )\n `,\n [serialId, serialId],\n );\n await this.#database.run(\n `\n DELETE FROM chunk_sentences\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.#database.run(\n `\n DELETE FROM chunks\n WHERE serial_id = ?\n `,\n [serialId],\n );\n await this.metadata.deleteDeletedChunks();\n await this.metadata.deleteDeletedEntitiesAndTriples();\n });\n }\n\n async #readJsonFile<T>(\n path: string,\n parse: (value: unknown) => T,\n ): Promise<T | undefined> {\n const content = await this.#readOptionalTextFile(path);\n\n if (content === undefined) {\n return undefined;\n }\n\n return parse(JSON.parse(content));\n }\n\n async #readOptionalFile(path: string): Promise<Uint8Array | undefined> {\n return await this.#fileStore.readFile(path);\n }\n\n async #replaceDocumentOrder(toc: TocFile): Promise<void> {\n await this.serials.setDocumentOrders(\n listTocSerialIds(toc.items).map((serialId, index) => ({\n documentOrder: index,\n serialId,\n })),\n );\n }\n\n async #readOptionalTextFile(path: string): Promise<string | undefined> {\n const content = await this.#fileStore.readFile(path);\n\n return content === undefined\n ? undefined\n : Buffer.from(content).toString(\"utf8\");\n }\n\n async #writeJsonFile(\n path: string,\n value: unknown,\n options: { readonly overwrite?: boolean } = {},\n ): Promise<void> {\n await this.#writeFile(path, `${JSON.stringify(value, null, 2)}\\n`, options);\n }\n\n async #writeNewFile(\n path: string,\n content: string | Uint8Array,\n ): Promise<void> {\n await this.#writeFile(path, content, { overwrite: false });\n }\n\n async #writeFile(\n path: string,\n content: string | Uint8Array,\n options: { readonly overwrite?: boolean },\n ): Promise<void> {\n try {\n await this.#fileStore.writeFile(path, content, options);\n } catch (error) {\n if (isNodeError(error) && error.code === \"EEXIST\") {\n throw new Error(`File already exists: ${path}`);\n }\n\n throw error;\n }\n\n if (options.overwrite !== true) {\n this.#contextScope.getStore()?.registerCreatedFile(path);\n }\n }\n\n #getCoverDataPath(): string {\n return join(this.#getCoverDirectoryPath(), \"data.bin\");\n }\n\n #getCoverDirectoryPath(): string {\n return join(this.path, \"cover\");\n }\n\n #getCoverInfoPath(): string {\n return join(this.#getCoverDirectoryPath(), \"info.json\");\n }\n\n #getTocPath(): string {\n return join(this.path, \"toc.json\");\n }\n\n public async runWithContext<T>(\n context: DirectoryDocumentContext,\n operation: () => Promise<T> | T,\n ): Promise<T> {\n return await this.#contextScope.run(context, operation);\n }\n\n public async rollbackContext(\n context: DirectoryDocumentContext,\n ): Promise<void> {\n await this.#rollbackContext(context);\n }\n}\n\nfunction listTocSerialIds(items: readonly TocItem[]): number[] {\n const serialIds: number[] = [];\n\n for (const item of items) {\n if (item.serialId !== undefined) {\n serialIds.push(item.serialId);\n }\n\n serialIds.push(...listTocSerialIds(item.children));\n }\n\n return serialIds;\n}\n\nclass DirectoryDocumentContext implements DocumentContext {\n readonly #createdFilePaths: string[] = [];\n readonly #document: DirectoryDocument;\n readonly #ownedSerialIds = new Set<number>();\n #completed = false;\n #disposed = false;\n\n public constructor(document: DirectoryDocument) {\n this.#document = document;\n }\n\n public complete(): void {\n this.#assertActive();\n this.#completed = true;\n }\n\n public async dispose(): Promise<void> {\n if (this.#disposed) {\n return;\n }\n\n this.#disposed = true;\n\n if (this.#completed) {\n return;\n }\n\n await this.#document.rollbackContext(this);\n }\n\n public ownSerial(serialId: number): void {\n this.#assertActive();\n this.#ownedSerialIds.add(serialId);\n }\n\n public async run<T>(operation: () => Promise<T> | T): Promise<T> {\n this.#assertActive();\n return await this.#document.runWithContext(this, operation);\n }\n\n public listCreatedFilePaths(): readonly string[] {\n return [...this.#createdFilePaths];\n }\n\n public listOwnedSerialIds(): readonly number[] {\n return [...this.#ownedSerialIds];\n }\n\n public registerCreatedFile(path: string): void {\n this.#createdFilePaths.push(path);\n }\n\n #assertActive(): void {\n if (this.#disposed) {\n throw new Error(\"DocumentContext is already disposed\");\n }\n }\n}\n\nfunction compareNumberDescending(left: number, right: number): number {\n return right - left;\n}\n","export { Database } from \"./database.js\";\nexport {\n ensureSharedStateDatabaseInitialized,\n openSharedStateDatabase,\n} from \"./shared-state-database.js\";\nexport { TextStreams, SerialTextStream } from \"./text-streams.js\";\nexport { FragmentDraft, Fragments, SerialFragments } from \"./fragments.js\";\nexport type {\n ReadonlySerialTextStream,\n ReadonlySerialTextStream as ReadonlySerialFragments,\n ReadonlyTextStreams,\n ReadonlyTextStreams as ReadonlyFragments,\n} from \"./text-streams.js\";\nexport { DirectoryDocument } from \"./document.js\";\nexport type {\n Document,\n DocumentContext,\n ReadonlyDocument,\n} from \"./document.js\";\nexport { SCHEMA_SQL } from \"./schema.js\";\nexport {\n ChunkStore,\n FragmentGroupStore,\n GraphBuildParameterStore,\n ReadingEdgeStore,\n MentionLinkStore,\n MentionStore,\n ObjectMetadataStore,\n SerialStore,\n SnakeChunkStore,\n SnakeEdgeStore,\n SnakeStore,\n} from \"./stores.js\";\nexport type {\n ReadonlyChunkStore,\n ReadonlyFragmentGroupStore,\n ReadonlyGraphBuildParameterStore,\n ReadonlyReadingEdgeStore,\n ReadonlyMentionLinkStore,\n ReadonlyMentionStore,\n ReadonlyObjectMetadataStore,\n ReadonlySerialStore,\n ReadonlySnakeChunkStore,\n ReadonlySnakeEdgeStore,\n ReadonlySnakeStore,\n} from \"./stores.js\";\nexport {\n ChunkImportance,\n ChunkRetention,\n expectChunkImportance,\n expectChunkRetention,\n isChunkImportance,\n isChunkRetention,\n ObjectMetadataKind,\n} from \"./types.js\";\nexport type {\n ChunkRecord,\n CreateSnakeRecord,\n FragmentRecord,\n GraphBuildParameterRecord,\n ReadingEdgeRecord,\n MentionLinkRecord,\n MentionRecord,\n ObjectMetadataTarget,\n SerialRecord,\n SentenceId,\n SentenceGroupRecord,\n SentenceRecord,\n SnakeChunkRecord,\n SnakeEdgeRecord,\n SnakeRecord,\n} from \"./types.js\";\n","import { stat } from \"fs/promises\";\n\nimport { resolveWikiGraphCacheDatabasePath } from \"../common/wiki-graph-dir.js\";\nimport { getNumber } from \"../document/database.js\";\nimport { openSharedStateDatabase } from \"../document/index.js\";\nimport type { Database } from \"../document/index.js\";\nimport type { GcContext, GcJobResult } from \"../gc/index.js\";\nimport { isNodeError } from \"../utils/node-error.js\";\n\nimport type {\n CachedDisambiguationRecord,\n CachedPageRecord,\n CachedQidRecord,\n DisambiguationLinkedQid,\n DisambiguationPageText,\n DisambiguationProfile,\n DisambiguationProfileMeaning,\n} from \"./types.js\";\n\ntype SqlRow = Record<string, unknown>;\n\nconst CREATE_QID_CACHE_SQL = `\nCREATE TABLE IF NOT EXISTS qid_cache (\n qid TEXT NOT NULL,\n language TEXT NOT NULL,\n label TEXT,\n description TEXT,\n pages_json TEXT NOT NULL,\n checked_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (qid, language)\n);\n`;\n\nconst CREATE_DISAMBIGUATION_CACHE_SQL = `\nCREATE TABLE IF NOT EXISTS disambiguation_cache (\n qid TEXT NOT NULL,\n wiki TEXT NOT NULL,\n pages_json TEXT NOT NULL,\n profile_json TEXT,\n checked_at TEXT NOT NULL,\n PRIMARY KEY (qid, wiki)\n);\n`;\n\nconst WIKIPAGE_CACHE_SCHEMA_SQL = `\n${CREATE_QID_CACHE_SQL}\n\nCREATE INDEX IF NOT EXISTS idx_qid_cache_checked_at\nON qid_cache(checked_at);\n\n${CREATE_DISAMBIGUATION_CACHE_SQL}\n\nCREATE INDEX IF NOT EXISTS idx_disambiguation_cache_checked_at\nON disambiguation_cache(checked_at);\n`;\nconst WIKIPAGE_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport class WikipageCache {\n readonly #database: Database;\n\n public constructor(database: Database) {\n this.#database = database;\n }\n\n public static async open(path?: string): Promise<WikipageCache> {\n const databasePath = path ?? resolveWikiGraphCacheDatabasePath();\n const database = await openSharedStateDatabase(\n databasePath,\n WIKIPAGE_CACHE_SCHEMA_SQL,\n );\n\n try {\n await migrateWikipageCacheSchema(database);\n } catch (error) {\n await database.close().catch(() => undefined);\n throw error;\n }\n\n return new WikipageCache(database);\n }\n\n public async close(): Promise<void> {\n await this.#database.close();\n }\n\n public async getQids(\n qids: readonly string[],\n language: string,\n ): Promise<ReadonlyMap<string, CachedQidRecord>> {\n if (qids.length === 0) {\n return new Map();\n }\n\n const results = new Map<string, CachedQidRecord>();\n\n for (const qid of qids) {\n const record = await this.#database.queryOne(\n `\nSELECT *\nFROM qid_cache\nWHERE qid = ? AND language = ?\n`,\n [qid, language],\n mapQidRecord,\n );\n\n if (record !== undefined) {\n results.set(qid, record);\n }\n }\n\n return results;\n }\n\n public async putQids(\n records: readonly CachedQidRecord[],\n language: string,\n ): Promise<void> {\n if (records.length === 0) {\n return;\n }\n\n await this.#database.transaction(async () => {\n for (const record of records) {\n await this.#database.run(\n `\nINSERT INTO qid_cache (\n qid, language, label, description, pages_json, checked_at, updated_at\n) VALUES (?, ?, ?, ?, ?, ?, ?)\nON CONFLICT(qid, language) DO UPDATE SET\n label = excluded.label,\n description = excluded.description,\n pages_json = excluded.pages_json,\n checked_at = excluded.checked_at,\n updated_at = excluded.updated_at\n`,\n [\n record.qid,\n language,\n record.label ?? null,\n record.description ?? null,\n JSON.stringify(record.sitelinks),\n record.checkedAt,\n record.updatedAt,\n ],\n );\n }\n });\n }\n\n public async getDisambiguations(\n qids: readonly string[],\n wiki: string,\n ): Promise<ReadonlyMap<string, CachedDisambiguationRecord>> {\n if (qids.length === 0) {\n return new Map();\n }\n\n const results = new Map<string, CachedDisambiguationRecord>();\n\n for (const qid of qids) {\n const record = await this.#database.queryOne(\n `\nSELECT *\nFROM disambiguation_cache\nWHERE qid = ? AND wiki = ?\n`,\n [qid, wiki],\n mapDisambiguationRecord,\n );\n\n if (record !== undefined) {\n results.set(qid, record);\n }\n }\n\n return results;\n }\n\n public async putDisambiguations(\n records: readonly CachedDisambiguationRecord[],\n wiki: string,\n ): Promise<void> {\n if (records.length === 0) {\n return;\n }\n\n await this.#database.transaction(async () => {\n for (const record of records) {\n await this.#database.run(\n `\nINSERT INTO disambiguation_cache (\n qid, wiki, pages_json, profile_json, checked_at\n) VALUES (?, ?, ?, ?, ?)\nON CONFLICT(qid, wiki) DO UPDATE SET\n pages_json = excluded.pages_json,\n profile_json = excluded.profile_json,\n checked_at = excluded.checked_at\n`,\n [\n record.disambiguationQid,\n wiki,\n JSON.stringify(record.pages),\n record.profile === undefined\n ? null\n : JSON.stringify(record.profile),\n record.checkedAt,\n ],\n );\n }\n });\n }\n}\n\nexport async function runWikipageCacheGc(\n context: GcContext,\n): Promise<GcJobResult> {\n const databasePath = resolveWikiGraphCacheDatabasePath();\n const beforeBytes = await readFileSize(databasePath);\n\n if (beforeBytes === undefined) {\n return {\n freedBytes: 0,\n removed: 0,\n scanned: 0,\n };\n }\n\n const database = await openSharedStateDatabase(\n databasePath,\n WIKIPAGE_CACHE_SCHEMA_SQL,\n );\n\n try {\n await migrateWikipageCacheSchema(database);\n\n const scanned = await countWikipageCacheRows(database);\n const cutoff = new Date(context.now - WIKIPAGE_CACHE_TTL_MS).toISOString();\n const expired = await countExpiredWikipageCacheRows(database, cutoff);\n\n if (!context.dryRun && expired > 0) {\n await database.transaction(async () => {\n await database.run(\"DELETE FROM qid_cache WHERE checked_at < ?\", [\n cutoff,\n ]);\n await database.run(\n \"DELETE FROM disambiguation_cache WHERE checked_at < ?\",\n [cutoff],\n );\n });\n await database.run(\"VACUUM\");\n }\n\n const afterBytes = await readFileSize(databasePath);\n\n return {\n freedBytes: context.dryRun\n ? estimateDryRunFreedBytes(beforeBytes, scanned, expired)\n : Math.max(0, beforeBytes - (afterBytes ?? 0)),\n removed: expired,\n scanned,\n };\n } finally {\n await database.close();\n }\n}\n\nfunction estimateDryRunFreedBytes(\n beforeBytes: number,\n scanned: number,\n expired: number,\n): number {\n if (scanned === 0 || expired === 0) {\n return 0;\n }\n\n return Math.round(beforeBytes * (expired / scanned));\n}\n\nasync function migrateWikipageCacheSchema(database: Database): Promise<void> {\n await migrateQidCacheSchema(database);\n await migrateDisambiguationCacheSchema(database);\n}\n\nasync function migrateQidCacheSchema(database: Database): Promise<void> {\n const columns = await listTableColumns(database, \"qid_cache\");\n\n if (columns.has(\"language\")) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(database, \"qid_cache\");\n\n if (transactionColumns.has(\"language\")) {\n return;\n }\n\n await database.run(\"ALTER TABLE qid_cache RENAME TO qid_cache_legacy\");\n await database.run(CREATE_QID_CACHE_SQL);\n await database.run(`\nINSERT OR IGNORE INTO qid_cache (\n qid, language, label, description, pages_json, checked_at, updated_at\n)\nSELECT qid, 'en', label, description, pages_json, checked_at, updated_at\nFROM qid_cache_legacy\n`);\n await database.run(\"DROP TABLE qid_cache_legacy\");\n });\n}\n\nasync function migrateDisambiguationCacheSchema(\n database: Database,\n): Promise<void> {\n const columns = await listTableColumns(database, \"disambiguation_cache\");\n\n if (columns.has(\"wiki\")) {\n return;\n }\n\n await database.transaction(async () => {\n const transactionColumns = await listTableColumns(\n database,\n \"disambiguation_cache\",\n );\n\n if (transactionColumns.has(\"wiki\")) {\n return;\n }\n\n await database.run(\n \"ALTER TABLE disambiguation_cache RENAME TO disambiguation_cache_legacy\",\n );\n await database.run(CREATE_DISAMBIGUATION_CACHE_SQL);\n await database.run(`\nINSERT OR IGNORE INTO disambiguation_cache (\n qid, wiki, pages_json, profile_json, checked_at\n)\nSELECT qid, 'enwiki', pages_json, profile_json, checked_at\nFROM disambiguation_cache_legacy\n`);\n await database.run(\"DROP TABLE disambiguation_cache_legacy\");\n });\n}\n\nasync function listTableColumns(\n database: Database,\n table: string,\n): Promise<ReadonlySet<string>> {\n const columns = await database.queryAll(\n `PRAGMA table_info(${table})`,\n undefined,\n (row) => getString(row.name, \"name\"),\n );\n\n return new Set(columns);\n}\n\nasync function countWikipageCacheRows(database: Database): Promise<number> {\n const qids = await countRows(database, \"qid_cache\");\n const disambiguations = await countRows(database, \"disambiguation_cache\");\n\n return qids + disambiguations;\n}\n\nasync function countExpiredWikipageCacheRows(\n database: Database,\n cutoff: string,\n): Promise<number> {\n const qids = await countRows(database, \"qid_cache\", cutoff);\n const disambiguations = await countRows(\n database,\n \"disambiguation_cache\",\n cutoff,\n );\n\n return qids + disambiguations;\n}\n\nasync function countRows(\n database: Database,\n table: \"disambiguation_cache\" | \"qid_cache\",\n checkedBefore?: string,\n): Promise<number> {\n return (\n (await database.queryOne(\n `\nSELECT COUNT(*) AS count\nFROM ${table}\n${checkedBefore === undefined ? \"\" : \"WHERE checked_at < ?\"}\n`,\n checkedBefore === undefined ? undefined : [checkedBefore],\n (row) => getNumber(row, \"count\"),\n )) ?? 0\n );\n}\n\nasync function readFileSize(path: string): Promise<number | undefined> {\n try {\n return (await stat(path)).size;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n }\n}\n\nfunction mapQidRecord(row: SqlRow): CachedQidRecord {\n const description = getOptionalString(row.description);\n const label = getOptionalString(row.label);\n\n return {\n checkedAt: getString(row.checked_at, \"checked_at\"),\n ...(description === undefined ? {} : { description }),\n ...(label === undefined ? {} : { label }),\n qid: getString(row.qid, \"qid\"),\n sitelinks: parsePageRecords(getString(row.pages_json, \"pages_json\")),\n updatedAt: getString(row.updated_at, \"updated_at\"),\n };\n}\n\nfunction mapDisambiguationRecord(row: SqlRow): CachedDisambiguationRecord {\n return {\n checkedAt: getString(row.checked_at, \"checked_at\"),\n disambiguationQid: getString(row.qid, \"qid\"),\n pages: parseDisambiguationPages(getString(row.pages_json, \"pages_json\")),\n ...(getOptionalString(row.profile_json) === undefined\n ? {}\n : {\n profile: parseDisambiguationProfile(\n getOptionalString(row.profile_json)!,\n ),\n }),\n };\n}\n\nfunction parsePageRecords(value: string): readonly CachedPageRecord[] {\n const parsed: unknown = JSON.parse(value);\n\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n return parsed.filter(isCachedPageRecord);\n}\n\nfunction parseDisambiguationPages(\n value: string,\n): readonly DisambiguationPageText[] {\n const parsed: unknown = JSON.parse(value);\n\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n return parsed.filter(isDisambiguationPageText);\n}\n\nfunction isCachedPageRecord(value: unknown): value is CachedPageRecord {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n typeof record.isDisambiguation === \"boolean\" &&\n typeof record.title === \"string\" &&\n record.title !== \"\" &&\n isSupportedWiki(record.wiki)\n );\n}\n\nfunction isDisambiguationPageText(\n value: unknown,\n): value is DisambiguationPageText {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n Array.isArray(record.linkedQids) &&\n record.linkedQids.every(isLinkedQid) &&\n typeof record.text === \"string\" &&\n record.text !== \"\" &&\n typeof record.title === \"string\" &&\n record.title !== \"\" &&\n isSupportedWiki(record.wiki)\n );\n}\n\nfunction parseDisambiguationProfile(value: string): DisambiguationProfile {\n const parsed: unknown = JSON.parse(value);\n\n if (!isDisambiguationProfile(parsed)) {\n throw new Error(\n \"Expected profile_json to contain a disambiguation profile.\",\n );\n }\n\n return parsed;\n}\n\nfunction isDisambiguationProfile(\n value: unknown,\n): value is DisambiguationProfile {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n Array.isArray(record.meanings) &&\n record.meanings.every(isDisambiguationProfileMeaning) &&\n typeof record.sourceQid === \"string\" &&\n /^Q[1-9]\\d*$/u.test(record.sourceQid) &&\n (!(\"surface\" in record) || typeof record.surface === \"string\")\n );\n}\n\nfunction isDisambiguationProfileMeaning(\n value: unknown,\n): value is DisambiguationProfileMeaning {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n (!(\"category\" in record) || typeof record.category === \"string\") &&\n typeof record.information === \"string\" &&\n typeof record.name === \"string\" &&\n record.name !== \"\" &&\n (record.priority === \"primary\" ||\n record.priority === \"secondary\" ||\n record.priority === \"other\") &&\n typeof record.qid === \"string\" &&\n /^Q[1-9]\\d*$/u.test(record.qid)\n );\n}\n\nfunction isLinkedQid(value: unknown): value is DisambiguationLinkedQid {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n typeof record.qid === \"string\" &&\n /^Q[1-9]\\d*$/u.test(record.qid) &&\n typeof record.title === \"string\" &&\n record.title !== \"\"\n );\n}\n\nfunction isSupportedWiki(value: unknown): value is \"enwiki\" | \"zhwiki\" {\n return value === \"enwiki\" || value === \"zhwiki\";\n}\n\nfunction getString(value: unknown, field: string): string {\n if (typeof value !== \"string\") {\n throw new Error(`Expected ${field} to be a string.`);\n }\n\n return value;\n}\n\nfunction getOptionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n","export class ParsedJsonError extends Error {\n public readonly issues: readonly string[];\n\n public constructor(issues: readonly string[]) {\n super(`Parse failed with ${issues.length} issue(s)`);\n this.name = \"ParsedJsonError\";\n this.issues = [...issues];\n }\n}\n\nexport abstract class GuaranteedRequestFailureError extends Error {\n public readonly attempts: number;\n public readonly maxRetries: number;\n\n protected constructor(message: string, attempts: number, maxRetries: number) {\n super(message);\n this.name = \"GuaranteedRequestFailureError\";\n this.attempts = attempts;\n this.maxRetries = maxRetries;\n }\n}\n\nexport class GuaranteedEmptyResponseError extends GuaranteedRequestFailureError {\n public constructor(attempts: number, maxRetries: number) {\n super(\n \"LLM returned empty response after all retries\",\n attempts,\n maxRetries,\n );\n this.name = \"GuaranteedEmptyResponseError\";\n }\n}\n\nexport class SuspectedModelRefusalError extends GuaranteedRequestFailureError {\n public readonly response: string;\n public readonly reason: string;\n\n public constructor(\n attempts: number,\n maxRetries: number,\n input: {\n response: string;\n reason: string;\n },\n ) {\n super(\n `Suspected model refusal after ${attempts} JSON syntax error attempt(s): ${input.reason}. Last response: ${JSON.stringify(input.response)}`,\n attempts,\n maxRetries,\n );\n this.name = \"SuspectedModelRefusalError\";\n this.response = input.response;\n this.reason = input.reason;\n }\n}\n\nexport class GuaranteedSchemaValidationError extends GuaranteedRequestFailureError {\n public readonly issues: readonly string[];\n public readonly response: string;\n\n public constructor(\n attempts: number,\n maxRetries: number,\n input: {\n issues: readonly string[];\n response: string;\n },\n cause: unknown,\n ) {\n super(\"Schema validation failed after all retries\", attempts, maxRetries);\n this.name = \"GuaranteedSchemaValidationError\";\n this.issues = [...input.issues];\n this.response = input.response;\n this.cause = cause;\n }\n}\n\nexport class GuaranteedParseValidationError extends GuaranteedRequestFailureError {\n public readonly issues: readonly string[];\n public readonly response: string;\n\n public constructor(\n attempts: number,\n maxRetries: number,\n input: {\n issues: readonly string[];\n response: string;\n },\n cause: unknown,\n ) {\n super(\"Parse validation failed after all retries\", attempts, maxRetries);\n this.name = \"GuaranteedParseValidationError\";\n this.issues = [...input.issues];\n this.response = input.response;\n this.cause = cause;\n }\n}\n","import type { LLMessage } from \"../llm/index.js\";\n\nexport const RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE =\n \"guaranteed/response_intent_classifier\";\n\nexport type GuaranteedResponseIntent =\n | \"ambiguous\"\n | \"malformed_json\"\n | \"natural_language\";\n\nconst NATURAL_LANGUAGE_PATTERNS = [\n /\\b(?:sorry|apologies)\\b/i,\n /\\b(?:i\\s+can(?:not|'t)|unable\\s+to|do\\s+not\\s+understand)\\b/i,\n /\\b(?:cannot\\s+answer|can't\\s+answer|can't\\s+help)\\b/i,\n /\\bno\\s+relevant\\s+result/i,\n /抱歉/,\n /无法回答/,\n /无法识别/,\n /无法给到/,\n /没有找到相关的结果/,\n];\n\nexport function buildResponseIntentClassificationMessages(\n systemPrompt: string,\n response: string,\n): readonly LLMessage[] {\n return [\n {\n role: \"system\",\n content: systemPrompt,\n },\n {\n role: \"user\",\n content: response,\n },\n ];\n}\n\nexport function classifyResponseIntentLocally(\n response: string,\n): GuaranteedResponseIntent {\n const trimmed = response.trim();\n\n if (trimmed === \"\") {\n return \"ambiguous\";\n }\n\n if (hasStrongMalformedJsonSignal(trimmed)) {\n return \"malformed_json\";\n }\n\n if (looksLikeNaturalLanguage(trimmed)) {\n return \"natural_language\";\n }\n\n if (hasWeakMalformedJsonSignal(trimmed)) {\n return \"ambiguous\";\n }\n\n if (looksLikePlainSentence(trimmed)) {\n return \"natural_language\";\n }\n\n return \"ambiguous\";\n}\n\nexport function parseResponseIntentClassification(\n response: string,\n): GuaranteedResponseIntent {\n const normalized = response.trim().toLowerCase();\n\n if (normalized.includes(\"malformed_json\")) {\n return \"malformed_json\";\n }\n\n if (normalized.includes(\"natural_language\")) {\n return \"natural_language\";\n }\n\n if (normalized.includes(\"ambiguous\")) {\n return \"ambiguous\";\n }\n\n return \"ambiguous\";\n}\n\nfunction hasStrongMalformedJsonSignal(text: string): boolean {\n return (\n /^(?:```json\\b|```|{|\\[)/i.test(text) ||\n /\"[^\"\\r\\n]+\"\\s*:/.test(text) ||\n (hasWeakMalformedJsonSignal(text) && endsWithLikelyTruncationToken(text))\n );\n}\n\nfunction hasWeakMalformedJsonSignal(text: string): boolean {\n const punctuationCount =\n (text.match(/[{}[\\]:,]/g) ?? []).length + (text.match(/\"/g) ?? []).length;\n\n return (\n /:\\s/.test(text) ||\n punctuationCount >= 3 ||\n hasUnbalancedBrackets(text) ||\n /[:,]\\s*$/.test(text)\n );\n}\n\nfunction hasUnbalancedBrackets(text: string): boolean {\n return count(text, \"{\") !== count(text, \"}\") ||\n count(text, \"[\") !== count(text, \"]\")\n ? true\n : false;\n}\n\nfunction endsWithLikelyTruncationToken(text: string): boolean {\n return /[:, \"{[]\\s*$/.test(text);\n}\n\nfunction looksLikeNaturalLanguage(text: string): boolean {\n if (NATURAL_LANGUAGE_PATTERNS.some((pattern) => pattern.test(text))) {\n return true;\n }\n\n return !hasWeakMalformedJsonSignal(text) && looksLikePlainSentence(text);\n}\n\nfunction looksLikePlainSentence(text: string): boolean {\n return (\n /[\\p{Script=Han}A-Za-z]/u.test(text) &&\n !/^[{[]/.test(text) &&\n !/\"[^\"\\r\\n]+\"\\s*:/.test(text)\n );\n}\n\nfunction count(text: string, character: string): number {\n return text.split(character).length - 1;\n}\n","import { jsonrepair } from \"jsonrepair\";\nimport type { ZodError, core } from \"zod\";\n\nexport function extractJsonText(response: string): string {\n const withoutCodeFence = response.replace(\n /```(?:json)?\\s*\\n(.*?)\\n```/gs,\n \"$1\",\n );\n const matchedArray = withoutCodeFence.match(/\\[[\\s\\S]*\\]/);\n const matchedObject = withoutCodeFence.match(/\\{[\\s\\S]*\\}/);\n\n if (\n matchedArray?.index !== undefined &&\n matchedArray[0] !== undefined &&\n (matchedObject?.index === undefined ||\n matchedArray.index < matchedObject.index)\n ) {\n return matchedArray[0];\n }\n\n if (matchedObject?.[0] !== undefined) {\n return matchedObject[0];\n }\n\n return withoutCodeFence.trim();\n}\n\nexport function repairJsonText(jsonText: string): string {\n try {\n return jsonrepair(jsonText);\n } catch {\n return jsonText;\n }\n}\n\nexport function buildSyntaxErrorMessage(error: SyntaxError): string {\n const lines = [\n \"Your JSON has a syntax error.\",\n `Error: ${error.message}`,\n \"\",\n \"Common issues:\",\n \"- Trailing commas in arrays or objects\",\n '- Unescaped quotes in strings, for example: \"He said \\\\\"hi\\\\\"\"',\n '- Single quotes instead of double quotes, for example: {\"key\": \"value\"}',\n \"- Missing commas between items\",\n \"\",\n \"Regenerate the complete and valid JSON directly. Do not add explanations or markdown fences.\",\n ];\n\n return lines.join(\"\\n\");\n}\n\nexport function buildMalformedJsonMessage(error: SyntaxError): string {\n const lines = [\n \"Your previous reply looked like malformed JSON.\",\n `Error: ${error.message}`,\n \"\",\n \"Return complete and valid JSON directly.\",\n \"Do not answer conversationally.\",\n \"Do not add explanations or markdown fences.\",\n ];\n\n return lines.join(\"\\n\");\n}\n\nexport function buildNaturalLanguageMessage(): string {\n const lines = [\n \"Your previous reply was plain natural language, not a JSON object or array.\",\n \"\",\n \"Do not apologize or explain.\",\n \"Do not answer conversationally.\",\n \"Return complete and valid JSON directly.\",\n ];\n\n return lines.join(\"\\n\");\n}\n\nexport function buildSchemaErrorMessage(error: ZodError): string {\n const issues = error.issues.map(formatZodIssue);\n const lines = [\n \"Your JSON has structural issues.\",\n \"\",\n ...issues,\n \"\",\n \"Regenerate the complete and valid JSON directly. Do not add explanations.\",\n ];\n\n return lines.join(\"\\n\");\n}\n\nexport function listSchemaIssues(error: ZodError): readonly string[] {\n return error.issues.map(formatZodIssue);\n}\n\nexport function buildBusinessErrorMessage(issues: readonly string[]): string {\n const lines = [\n \"Your JSON has the following issues.\",\n \"\",\n ...issues.map((issue) => `- ${issue}`),\n \"\",\n \"Regenerate the complete and valid JSON directly. Do not add explanations.\",\n ];\n\n return lines.join(\"\\n\");\n}\n\nfunction formatZodIssue(issue: core.$ZodIssue): string {\n const location = issue.path.length === 0 ? \"<root>\" : issue.path.join(\".\");\n\n return `- **${location}**: ${issue.message}`;\n}\n","import type { LLMessage } from \"../llm/index.js\";\nimport {\n buildResponseIntentClassificationMessages,\n classifyResponseIntentLocally,\n parseResponseIntentClassification,\n type GuaranteedResponseIntent,\n} from \"./classifier.js\";\nimport {\n GuaranteedEmptyResponseError,\n GuaranteedParseValidationError,\n GuaranteedSchemaValidationError,\n ParsedJsonError,\n SuspectedModelRefusalError,\n} from \"./errors.js\";\nimport {\n buildMalformedJsonMessage,\n buildNaturalLanguageMessage,\n buildBusinessErrorMessage,\n buildSchemaErrorMessage,\n buildSyntaxErrorMessage,\n extractJsonText,\n listSchemaIssues,\n repairJsonText,\n} from \"./response.js\";\nimport type { GuaranteedRequestOptions } from \"./types.js\";\n\nconst DEFAULT_MAX_RETRIES = 12;\nconst MAX_MALFORMED_JSON_REPAIR_HISTORY_ATTEMPTS = 3;\n\nexport async function requestGuaranteedJson<TData, TResult>(\n options: GuaranteedRequestOptions<TData, TResult>,\n): Promise<TResult> {\n const initialMessages = [...options.messages];\n let currentMessages = [...options.messages];\n let consecutiveProtocolDerailments = 0;\n let consecutiveMalformedJsonFailures = 0;\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\n for (let index = 0; index <= maxRetries; index += 1) {\n const response = await options.request(currentMessages, index, maxRetries);\n if (response === undefined || response.trim() === \"\") {\n if (index >= maxRetries) {\n throw new GuaranteedEmptyResponseError(index + 1, maxRetries);\n }\n continue;\n }\n let parsedData: unknown;\n\n try {\n const extractedJsonText = extractJsonText(response);\n const repairedJsonText = repairJsonText(extractedJsonText);\n\n parsedData = JSON.parse(repairedJsonText);\n } catch (error) {\n const intent = await classifyResponseIntent(options, response, index);\n\n if (intent === \"natural_language\") {\n consecutiveProtocolDerailments += 1;\n consecutiveMalformedJsonFailures = 0;\n\n if (consecutiveProtocolDerailments >= 2 || index >= maxRetries) {\n const reason =\n index >= maxRetries\n ? \"last retry still returned natural-language content\"\n : \"two consecutive retries returned natural-language content\";\n throw new SuspectedModelRefusalError(index + 1, maxRetries, {\n response,\n reason,\n });\n }\n\n currentMessages = buildRetryMessages(\n initialMessages,\n response,\n buildNaturalLanguageMessage(),\n false,\n );\n continue;\n }\n\n consecutiveProtocolDerailments = 0;\n consecutiveMalformedJsonFailures += 1;\n currentMessages = buildRetryMessages(\n initialMessages,\n response,\n intent === \"malformed_json\"\n ? buildMalformedJsonMessage(asSyntaxError(error))\n : buildSyntaxErrorMessage(asSyntaxError(error)),\n consecutiveMalformedJsonFailures <\n MAX_MALFORMED_JSON_REPAIR_HISTORY_ATTEMPTS,\n );\n continue;\n }\n consecutiveProtocolDerailments = 0;\n consecutiveMalformedJsonFailures = 0;\n\n const validation = await options.schema.safeParseAsync(parsedData);\n\n if (!validation.success) {\n if (\n shouldTreatSchemaFailureAsNaturalLanguage(validation.error, parsedData)\n ) {\n const intent = await classifyResponseIntent(options, response, index);\n\n if (intent === \"natural_language\") {\n consecutiveProtocolDerailments += 1;\n consecutiveMalformedJsonFailures = 0;\n\n if (consecutiveProtocolDerailments >= 2 || index >= maxRetries) {\n const reason =\n index >= maxRetries\n ? \"last retry still returned natural-language content\"\n : \"two consecutive retries returned natural-language content\";\n throw new SuspectedModelRefusalError(index + 1, maxRetries, {\n response,\n reason,\n });\n }\n\n currentMessages = buildRetryMessages(\n initialMessages,\n response,\n buildNaturalLanguageMessage(),\n false,\n );\n continue;\n }\n }\n\n consecutiveProtocolDerailments = 0;\n const feedback = buildSchemaErrorMessage(validation.error);\n\n if (index >= maxRetries) {\n throw new GuaranteedSchemaValidationError(\n index + 1,\n maxRetries,\n {\n issues: listSchemaIssues(validation.error),\n response,\n },\n validation.error,\n );\n }\n currentMessages = buildRetryMessages(\n initialMessages,\n response,\n feedback,\n true,\n );\n continue;\n }\n\n try {\n return await options.parse(validation.data, index, maxRetries);\n } catch (error) {\n if (!(error instanceof ParsedJsonError)) {\n throw error;\n }\n const feedback = buildBusinessErrorMessage(error.issues);\n\n if (index >= maxRetries) {\n throw new GuaranteedParseValidationError(\n index + 1,\n maxRetries,\n {\n issues: error.issues,\n response,\n },\n error,\n );\n }\n currentMessages = buildRetryMessages(\n initialMessages,\n response,\n feedback,\n true,\n );\n }\n }\n throw new Error(\"requestGuaranteedJson failed unexpectedly\");\n}\n\nfunction buildRetryMessages(\n initialMessages: readonly LLMessage[],\n response: string,\n feedback: string,\n includeAssistantResponse: boolean,\n): LLMessage[] {\n return [\n ...initialMessages,\n ...(includeAssistantResponse\n ? [\n {\n role: \"assistant\" as const,\n content: response,\n },\n ]\n : []),\n {\n role: \"user\",\n content: feedback,\n },\n ];\n}\n\nfunction asSyntaxError(error: unknown): SyntaxError {\n if (error instanceof SyntaxError) {\n return error;\n }\n\n return new SyntaxError(String(error));\n}\n\nasync function classifyResponseIntent<TData, TResult>(\n options: GuaranteedRequestOptions<TData, TResult>,\n response: string,\n index: number,\n): Promise<GuaranteedResponseIntent> {\n const localIntent = classifyResponseIntentLocally(response);\n\n if (localIntent !== \"ambiguous\") {\n return localIntent;\n }\n\n try {\n const classifierResponse = await options.request(\n buildResponseIntentClassificationMessages(\n options.responseIntentClassifierPrompt,\n response,\n ),\n index,\n options.maxRetries ?? DEFAULT_MAX_RETRIES,\n );\n\n if (classifierResponse === undefined || classifierResponse.trim() === \"\") {\n return \"natural_language\";\n }\n\n const classifierIntent =\n parseResponseIntentClassification(classifierResponse);\n\n return classifierIntent === \"ambiguous\"\n ? \"natural_language\"\n : classifierIntent;\n } catch {\n return \"natural_language\";\n }\n}\n\nfunction shouldTreatSchemaFailureAsNaturalLanguage(\n error: {\n issues: readonly {\n code?: string;\n path: readonly PropertyKey[];\n expected?: unknown;\n }[];\n },\n parsedData: unknown,\n): boolean {\n if (!isPrimitiveJsonValue(parsedData)) {\n return false;\n }\n\n return error.issues.some(\n (issue) =>\n issue.code === \"invalid_type\" &&\n issue.path.length === 0 &&\n (issue.expected === \"object\" || issue.expected === \"array\"),\n );\n}\n\nfunction isPrimitiveJsonValue(value: unknown): boolean {\n return (\n value === null || (typeof value !== \"object\" && typeof value !== \"function\")\n );\n}\n","export {\n GuaranteedEmptyResponseError,\n GuaranteedRequestFailureError,\n GuaranteedParseValidationError,\n GuaranteedSchemaValidationError,\n ParsedJsonError,\n SuspectedModelRefusalError,\n} from \"./errors.js\";\nexport { RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE } from \"./classifier.js\";\nexport { requestGuaranteedJson } from \"./request.js\";\nexport type {\n GuaranteedLazyRequest,\n GuaranteedParser,\n GuaranteedRequest,\n GuaranteedRequestController,\n GuaranteedRequestOptions,\n} from \"./types.js\";\n","import { z } from \"zod\";\n\nimport {\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n type GuaranteedRequest,\n} from \"../guaranteed/index.js\";\nimport type { LLMessage } from \"../llm/index.js\";\n\nimport type {\n DisambiguationLinkedQid,\n DisambiguationProfile,\n DisambiguationProfileMeaning,\n DisambiguationProfileNormalizer,\n DisambiguationProfileNormalizerInput,\n} from \"./types.js\";\n\nconst profileMeaningSchema = z\n .object({\n category: z.string().min(1).optional(),\n information: z.string(),\n name: z.string().min(1),\n priority: z.enum([\"primary\", \"secondary\", \"other\"]),\n qid: z.string().regex(/^Q[1-9]\\d*$/u),\n })\n .strict();\n\nconst profileSchema = z\n .object({\n meanings: z.array(profileMeaningSchema),\n sourceQid: z.string().regex(/^Q[1-9]\\d*$/u),\n surface: z.string().min(1).optional(),\n })\n .strict();\n\nexport interface CreateDisambiguationProfileNormalizerOptions {\n readonly maxMeanings?: number;\n readonly maxRetries?: number;\n readonly request: GuaranteedRequest;\n}\n\nexport function createDisambiguationProfileNormalizer(\n options: CreateDisambiguationProfileNormalizerOptions,\n): DisambiguationProfileNormalizer {\n return async (input) =>\n await requestGuaranteedJson({\n messages: buildNormalizerMessages(input, options.maxMeanings ?? 24),\n parse: (profile) =>\n parseDisambiguationProfile(input, normalizeProfile(profile)),\n request: options.request,\n responseIntentClassifierPrompt:\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n schema: profileSchema,\n ...(options.maxRetries === undefined\n ? {}\n : { maxRetries: options.maxRetries }),\n });\n}\n\nfunction normalizeProfile(\n profile: z.infer<typeof profileSchema>,\n): DisambiguationProfile {\n return {\n meanings: profile.meanings.map((meaning) => ({\n ...(meaning.category === undefined ? {} : { category: meaning.category }),\n information: meaning.information,\n name: meaning.name,\n priority: meaning.priority,\n qid: meaning.qid,\n })),\n sourceQid: profile.sourceQid,\n ...(profile.surface === undefined ? {} : { surface: profile.surface }),\n };\n}\n\nfunction parseDisambiguationProfile(\n input: DisambiguationProfileNormalizerInput,\n profile: DisambiguationProfile,\n): DisambiguationProfile {\n const pageQidLinks = new Set(input.pageQidLinks.map((item) => item.qid));\n const issues: string[] = [];\n const seen = new Set<string>();\n const meanings: DisambiguationProfileMeaning[] = [];\n\n if (profile.sourceQid !== input.sourceQid) {\n issues.push(\n `sourceQid must be ${input.sourceQid}, but got ${profile.sourceQid}.`,\n );\n }\n\n for (const meaning of profile.meanings) {\n if (!pageQidLinks.has(meaning.qid)) {\n issues.push(\n `Meaning \"${meaning.name}\" selected qid ${meaning.qid}, but it is not present in the input disambiguation page links.`,\n );\n continue;\n }\n if (seen.has(meaning.qid)) {\n issues.push(\n `Duplicate meaning qid ${meaning.qid}. Return each QID once.`,\n );\n continue;\n }\n\n seen.add(meaning.qid);\n meanings.push(meaning);\n }\n\n if (meanings.length === 0) {\n issues.push(\"Return at least one meaning from the pageQidLinks list.\");\n }\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n return {\n meanings,\n sourceQid: input.sourceQid,\n ...(profile.surface === undefined ? {} : { surface: profile.surface }),\n };\n}\n\nfunction buildNormalizerMessages(\n input: DisambiguationProfileNormalizerInput,\n maxMeanings: number,\n): LLMessage[] {\n return [\n {\n role: \"system\",\n content: [\n \"You normalize Wikipedia disambiguation pages for entity grounding.\",\n \"Return JSON only.\",\n \"Do not invent QIDs, pages, people, places, works, or meanings.\",\n \"Only use QIDs from the pageQidLinks list.\",\n \"The information field must only copy or summarize text present on the disambiguation page itself.\",\n \"Do not use Wikidata descriptions or external knowledge to fill information.\",\n \"Prefer common and context-independent meanings over exhaustive long-tail lists.\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: [\n `Source QID: ${input.sourceQid}`,\n input.surface === undefined ? \"\" : `Surface: ${input.surface}`,\n `Maximum meanings: ${maxMeanings}`,\n \"\",\n \"Page QID links:\",\n JSON.stringify(input.pageQidLinks.map(formatPageQidLink), null, 2),\n \"\",\n \"Disambiguation page text:\",\n input.pages\n .map(\n (page) =>\n `--- ${page.wiki}:${page.title} ---\\n${truncatePageText(page.text)}`,\n )\n .join(\"\\n\\n\"),\n \"\",\n \"Return this JSON shape:\",\n JSON.stringify(\n {\n meanings: [\n {\n category:\n \"person | place | organization | work | concept | event | other\",\n information:\n \"short information copied or summarized from the disambiguation page; empty string if no information is present\",\n name: \"display name\",\n priority: \"primary | secondary | other\",\n qid: \"QID from pageQidLinks\",\n },\n ],\n sourceQid: input.sourceQid,\n ...(input.surface === undefined ? {} : { surface: input.surface }),\n },\n null,\n 2,\n ),\n ]\n .filter((line) => line !== \"\")\n .join(\"\\n\"),\n },\n ];\n}\n\nfunction formatPageQidLink(item: DisambiguationLinkedQid): object {\n return {\n qid: item.qid,\n title: item.title,\n };\n}\n\nfunction truncatePageText(text: string): string {\n const maxLength = 16000;\n\n return text.length <= maxLength\n ? text\n : `${text.slice(0, maxLength)}\\n\\n[truncated]`;\n}\n","export interface RateLimiterOptions {\n readonly concurrency: number;\n readonly minRequestIntervalMs: number;\n}\n\nexport class RateLimiter {\n readonly #concurrency: number;\n readonly #minRequestIntervalMs: number;\n #active = 0;\n #blockedUntil = 0;\n #lastStartedAt = 0;\n readonly #queue: Array<() => void> = [];\n\n public constructor(options: RateLimiterOptions) {\n this.#concurrency = Math.max(1, Math.floor(options.concurrency));\n this.#minRequestIntervalMs = Math.max(\n 0,\n Math.floor(options.minRequestIntervalMs),\n );\n }\n\n public async use<T>(operation: () => Promise<T>): Promise<T> {\n await this.#acquire();\n\n try {\n return await operation();\n } finally {\n this.#active -= 1;\n this.#releaseNext();\n }\n }\n\n public blockFor(ms: number): void {\n if (!Number.isFinite(ms) || ms <= 0) {\n return;\n }\n\n this.#blockedUntil = Math.max(this.#blockedUntil, Date.now() + ms);\n }\n\n async #acquire(): Promise<void> {\n await new Promise<void>((resolve) => {\n this.#queue.push(resolve);\n this.#releaseNext();\n });\n\n await this.#waitForTurn();\n this.#active += 1;\n this.#lastStartedAt = Date.now();\n }\n\n async #waitForTurn(): Promise<void> {\n const blockedDelayMs = Math.max(0, this.#blockedUntil - Date.now());\n const intervalDelayMs = Math.max(\n 0,\n this.#lastStartedAt + this.#minRequestIntervalMs - Date.now(),\n );\n const delayMs = Math.max(blockedDelayMs, intervalDelayMs);\n\n if (delayMs > 0) {\n await delay(delayMs);\n }\n }\n\n #releaseNext(): void {\n if (this.#active >= this.#concurrency) {\n return;\n }\n\n const resolve = this.#queue.shift();\n\n if (resolve === undefined) {\n return;\n }\n\n resolve();\n }\n}\n\nasync function delay(ms: number): Promise<void> {\n await new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nexport function parseRetryAfterMs(value: string | null): number | undefined {\n if (value === null || value.trim() === \"\") {\n return undefined;\n }\n\n const seconds = Number(value);\n\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds * 1000);\n }\n\n const retryAt = Date.parse(value);\n\n if (Number.isNaN(retryAt)) {\n return undefined;\n }\n\n return Math.max(0, retryAt - Date.now());\n}\n","import { AsyncLocalStorage } from \"async_hooks\";\nimport { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync } from \"fs\";\nimport { dirname, join, resolve } from \"path\";\n\nimport pino, {\n multistream,\n type Logger as PinoLogger,\n type StreamEntry,\n} from \"pino\";\nimport pretty from \"pino-pretty\";\nimport type { PrettyOptions } from \"pino-pretty\";\n\ninterface LoggingContext {\n readonly artifactCounters: Map<string, number>;\n readonly artifactRootDirPath?: string;\n readonly logger: PinoLogger;\n readonly rootLogDirPath?: string;\n readonly runId: string;\n}\n\nconst loggingContext = new AsyncLocalStorage<LoggingContext>();\nconst artifactCounters = new Map<string, number>();\nconst silentLogger = pino({ enabled: false });\n\nexport async function withLoggingContext<T>(\n input: {\n readonly operation: string;\n readonly logDirPath?: string;\n readonly verbose?: boolean;\n },\n operation: () => Promise<T>,\n): Promise<T> {\n const rootLogDirPath =\n input.logDirPath === undefined ? undefined : resolve(input.logDirPath);\n const runId = createRunId();\n const runDirPath =\n rootLogDirPath === undefined ? undefined : join(rootLogDirPath, runId);\n const artifactRootDirPath =\n runDirPath === undefined ? undefined : join(runDirPath, \"artifacts\");\n\n if (runDirPath !== undefined) {\n mkdirSync(runDirPath, { recursive: true });\n }\n\n const logger = createLogger({\n operation: input.operation,\n runId,\n verbose: input.verbose ?? false,\n ...(runDirPath === undefined\n ? {}\n : { eventLogPath: join(runDirPath, \"run.log\") }),\n });\n\n return await loggingContext.run(\n {\n artifactCounters: new Map(),\n logger,\n runId,\n ...(artifactRootDirPath === undefined ? {} : { artifactRootDirPath }),\n ...(rootLogDirPath === undefined ? {} : { rootLogDirPath }),\n },\n operation,\n );\n}\n\nexport function getLogger(bindings?: Record<string, unknown>): PinoLogger {\n const logger = loggingContext.getStore()?.logger ?? silentLogger;\n\n return bindings === undefined ? logger : logger.child(bindings);\n}\n\nexport function resolveArtifactPath(input: {\n readonly category: string;\n readonly fileName: string;\n readonly logDirPath?: string;\n}): string | undefined {\n if (input.logDirPath === undefined) {\n return undefined;\n }\n\n const rootLogDirPath = resolve(input.logDirPath);\n const context = loggingContext.getStore();\n\n if (\n context?.rootLogDirPath === rootLogDirPath &&\n context.artifactRootDirPath !== undefined\n ) {\n const categoryDirPath = join(context.artifactRootDirPath, input.category);\n\n mkdirSync(categoryDirPath, { recursive: true });\n\n return join(categoryDirPath, input.fileName);\n }\n\n mkdirSync(rootLogDirPath, { recursive: true });\n\n return join(rootLogDirPath, input.fileName);\n}\n\nexport function allocateArtifactPath(input: {\n readonly alwaysNumbered?: boolean;\n readonly category: string;\n readonly extension?: string;\n readonly logDirPath?: string;\n readonly prefix: string;\n}): string | undefined {\n if (input.logDirPath === undefined) {\n return undefined;\n }\n\n const rootLogDirPath = resolve(input.logDirPath);\n const context = loggingContext.getStore();\n const extension = input.extension ?? \".log\";\n const counterStore = context?.artifactCounters ?? artifactCounters;\n const counterKey = `${rootLogDirPath}:${input.category}:${input.prefix}:${extension}`;\n const nextIndex = counterStore.get(counterKey);\n const startIndex = nextIndex === undefined ? 1 : nextIndex + 1;\n\n for (let index = startIndex; ; index += 1) {\n const fileName =\n input.alwaysNumbered === true\n ? `${input.prefix}-${index}${extension}`\n : index === 1\n ? `${input.prefix}${extension}`\n : `${input.prefix}-${index}${extension}`;\n const resolvedPath = resolveArtifactPath({\n category: input.category,\n fileName,\n logDirPath: input.logDirPath,\n });\n\n if (resolvedPath === undefined) {\n return undefined;\n }\n\n if (!existsSync(resolvedPath)) {\n counterStore.set(counterKey, index);\n return resolvedPath;\n }\n }\n}\n\nfunction createLogger(input: {\n readonly eventLogPath?: string;\n readonly operation: string;\n readonly runId: string;\n readonly verbose: boolean;\n}): PinoLogger {\n const streams: StreamEntry[] = [];\n\n if (input.eventLogPath !== undefined) {\n mkdirSync(dirname(input.eventLogPath), { recursive: true });\n streams.push({\n level: \"info\",\n stream: pretty(createPrettyOptions(input.eventLogPath)),\n });\n }\n\n if (input.verbose) {\n streams.push({\n level: \"info\",\n stream: pretty(createPrettyOptions(process.stderr)),\n });\n }\n\n if (streams.length === 0) {\n return silentLogger;\n }\n\n return pino(\n {\n base: null,\n level: \"info\",\n },\n multistream(streams),\n ).child({\n operation: input.operation,\n runId: input.runId,\n });\n}\n\nfunction createPrettyOptions(\n destination: string | NodeJS.WritableStream,\n): PrettyOptions {\n return {\n colorize: false,\n destination,\n ignore: \"pid,hostname,operation,runId,component,scope,sessionId\",\n mkdir: typeof destination === \"string\",\n singleLine: true,\n sync: true,\n translateTime: \"SYS:yyyy-mm-dd HH:MM:ss\",\n };\n}\n\nfunction createRunId(): string {\n const now = new Date();\n const year = String(now.getUTCFullYear());\n const month = pad(now.getUTCMonth() + 1);\n const day = pad(now.getUTCDate());\n const hours = pad(now.getUTCHours());\n const minutes = pad(now.getUTCMinutes());\n const seconds = pad(now.getUTCSeconds());\n\n return `${year}${month}${day}-${hours}${minutes}${seconds}-${randomUUID().slice(0, 8)}`;\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n","import { createHash } from \"crypto\";\nimport { appendFile } from \"fs/promises\";\n\nimport { getLogger, resolveArtifactPath } from \"../common/logging.js\";\nimport { formatError } from \"../utils/node-error.js\";\n\nconst MAX_RESPONSE_TEXT_LENGTH = 16_384;\n\nexport interface WikipageFetchLog {\n readonly filePath: string | undefined;\n append(entry: WikipageFetchLogEntry): Promise<void>;\n warnFailed(): void;\n}\n\ninterface WikipageFetchLogEntry {\n readonly attempt: number;\n readonly durationMs: number;\n readonly error?: unknown;\n readonly response?: Response;\n readonly responseText?: string;\n readonly startedAt: number;\n readonly url: URL;\n}\n\nclass SilentWikipageFetchLog implements WikipageFetchLog {\n public readonly filePath = undefined;\n\n public append(): Promise<void> {\n return Promise.resolve();\n }\n\n public warnFailed(): void {\n return undefined;\n }\n}\n\nclass FileWikipageFetchLog implements WikipageFetchLog {\n readonly #filePath: string;\n #failedWarned = false;\n\n public constructor(filePath: string) {\n this.#filePath = filePath;\n }\n\n public get filePath(): string {\n return this.#filePath;\n }\n\n public async append(entry: WikipageFetchLogEntry): Promise<void> {\n await appendFile(\n this.#filePath,\n `${JSON.stringify(formatEntry(entry))}\\n`,\n \"utf8\",\n );\n }\n\n public warnFailed(): void {\n if (this.#failedWarned) {\n return;\n }\n this.#failedWarned = true;\n\n getLogger({ component: \"wikipage\" }).warn(\n `\\n[Wikipage] Failed with fetch log: ${this.#filePath}`,\n );\n }\n}\n\nexport function createWikipageFetchLog(logDirPath?: string): WikipageFetchLog {\n if (logDirPath === undefined) {\n return new SilentWikipageFetchLog();\n }\n\n const filePath = resolveArtifactPath({\n category: \"wikipage\",\n fileName: \"wikipage-fetch.jsonl\",\n logDirPath,\n });\n\n return filePath === undefined\n ? new SilentWikipageFetchLog()\n : new FileWikipageFetchLog(filePath);\n}\n\nfunction formatEntry(entry: WikipageFetchLogEntry): Record<string, unknown> {\n return {\n action: entry.url.searchParams.get(\"action\") ?? null,\n attempt: entry.attempt,\n batch: summarizeBatch(entry.url),\n durationMs: entry.durationMs,\n endpoint: `${entry.url.origin}${entry.url.pathname}`,\n host: entry.url.host,\n ok: entry.response?.ok ?? false,\n startedAt: new Date(entry.startedAt).toISOString(),\n ...(entry.response === undefined\n ? {}\n : {\n retryAfter: entry.response.headers.get(\"retry-after\"),\n status: entry.response.status,\n }),\n ...(entry.error === undefined\n ? {}\n : { error: formatLogError(entry.error) }),\n ...(entry.responseText === undefined\n ? {}\n : { responseText: truncateResponseText(entry.responseText) }),\n };\n}\n\nfunction summarizeBatch(url: URL): Record<string, unknown> | undefined {\n const ids = url.searchParams.get(\"ids\");\n if (ids !== null) {\n return summarizeList(\"qid\", ids.split(\"|\"));\n }\n\n const titles = url.searchParams.get(\"titles\");\n if (titles !== null) {\n return summarizeList(\"title\", titles.split(\"|\"));\n }\n\n const page = url.searchParams.get(\"page\");\n if (page !== null) {\n return summarizeList(\"page\", [page]);\n }\n\n return undefined;\n}\n\nfunction summarizeList(\n kind: \"page\" | \"qid\" | \"title\",\n values: readonly string[],\n): Record<string, unknown> {\n const joined = values.join(\"|\");\n\n return {\n count: values.length,\n hash: createHash(\"sha256\").update(joined).digest(\"hex\").slice(0, 16),\n kind,\n sample: values.slice(0, 5),\n };\n}\n\nfunction formatLogError(error: unknown): Record<string, unknown> {\n return {\n message: formatError(error),\n name: error instanceof Error ? error.name : typeof error,\n ...formatErrorCause(error),\n };\n}\n\nfunction formatErrorCause(error: unknown): Record<string, unknown> {\n if (!(error instanceof Error) || error.cause === undefined) {\n return {};\n }\n\n const cause = error.cause;\n\n return {\n cause:\n cause instanceof Error\n ? {\n message: formatError(cause),\n name: cause.name,\n ...formatNodeErrorCode(cause),\n }\n : stringifyUnknown(cause),\n };\n}\n\nfunction stringifyUnknown(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n if (\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\" ||\n typeof value === \"symbol\"\n ) {\n return value.toString();\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return \"unserializable error cause\";\n }\n}\n\nfunction formatNodeErrorCode(error: Error): Record<string, unknown> {\n const code = (error as { readonly code?: unknown }).code;\n\n return typeof code === \"string\" ? { code } : {};\n}\n\nfunction truncateResponseText(text: string): string {\n if (text.length <= MAX_RESPONSE_TEXT_LENGTH) {\n return text;\n }\n\n return `${text.slice(0, MAX_RESPONSE_TEXT_LENGTH)}\\n[truncated ${text.length - MAX_RESPONSE_TEXT_LENGTH} chars]`;\n}\n","import { DomUtils, parseDocument } from \"htmlparser2\";\n\nimport { getLogger } from \"../common/logging.js\";\nimport { formatError } from \"../utils/node-error.js\";\nimport type { WikipageFetchLog } from \"./fetch-log.js\";\nimport { RateLimiter, parseRetryAfterMs } from \"./rate-limiter.js\";\n\nexport type SupportedWiki = \"enwiki\" | \"zhwiki\";\n\nexport interface WikimediaClientOptions {\n readonly concurrency: number;\n readonly fetch?: typeof fetch;\n readonly language: string;\n readonly minRequestIntervalMs: number;\n readonly requestLog: WikipageFetchLog;\n readonly retryBaseDelayMs: number;\n readonly retryTimes: number;\n readonly userAgent?: string;\n readonly wiki: string;\n}\n\nexport interface WikidataEntityInfo {\n readonly description?: string;\n readonly label?: string;\n readonly qid: string;\n readonly sitelinks: readonly WikidataSitelinkInfo[];\n}\n\nexport interface WikidataSitelinkInfo {\n readonly title: string;\n readonly wiki: SupportedWiki;\n}\n\nexport interface WikiPageInfo {\n readonly isDisambiguation: boolean;\n readonly pageId?: number;\n readonly title: string;\n readonly wiki: SupportedWiki;\n readonly wikibaseItem?: string;\n}\n\nexport interface ParsedDisambiguationPage {\n readonly linkedTitles: readonly string[];\n readonly pageId?: number;\n readonly text: string;\n readonly title: string;\n readonly wiki: SupportedWiki;\n}\n\ninterface MediaWikiPage {\n readonly ns?: unknown;\n readonly pageid?: unknown;\n readonly pageprops?: {\n readonly disambiguation?: unknown;\n readonly wikibase_item?: unknown;\n };\n readonly title?: unknown;\n}\n\nconst SUPPORTED_WIKIS: readonly SupportedWiki[] = [\"zhwiki\", \"enwiki\"];\nconst TITLE_URI_PREFIX = \"wikigraph-title://\";\n\ninterface HtmlElement {\n readonly attribs: Record<string, string | undefined>;\n readonly children?: readonly unknown[];\n readonly name: string;\n}\n\nexport class WikimediaClient {\n readonly #fetch: typeof fetch;\n readonly #language: string;\n readonly #limiter: RateLimiter;\n readonly #requestLog: WikipageFetchLog;\n readonly #retryBaseDelayMs: number;\n readonly #retryTimes: number;\n readonly #userAgent: string | undefined;\n readonly #wiki: string;\n\n public constructor(options: WikimediaClientOptions) {\n this.#fetch = options.fetch ?? fetch;\n this.#language = options.language;\n this.#requestLog = options.requestLog;\n this.#wiki = options.wiki;\n this.#userAgent = options.userAgent;\n this.#retryBaseDelayMs = Math.max(0, Math.floor(options.retryBaseDelayMs));\n this.#retryTimes = Math.max(0, Math.floor(options.retryTimes));\n this.#limiter = new RateLimiter({\n concurrency: options.concurrency,\n minRequestIntervalMs: options.minRequestIntervalMs,\n });\n }\n\n public async getEntities(\n qids: readonly string[],\n ): Promise<ReadonlyMap<string, WikidataEntityInfo>> {\n if (qids.length === 0) {\n return new Map();\n }\n\n const url = new URL(\"https://www.wikidata.org/w/api.php\");\n url.searchParams.set(\"action\", \"wbgetentities\");\n url.searchParams.set(\"ids\", qids.join(\"|\"));\n url.searchParams.set(\"props\", \"labels|descriptions|sitelinks\");\n url.searchParams.set(\"languages\", listWikidataLanguages(this.#language));\n url.searchParams.set(\"sitefilter\", SUPPORTED_WIKIS.join(\"|\"));\n url.searchParams.set(\"format\", \"json\");\n url.searchParams.set(\"formatversion\", \"2\");\n\n const json = await this.#fetchJson(url);\n const entities = asRecord(json.entities);\n const results = new Map<string, WikidataEntityInfo>();\n\n for (const qid of qids) {\n const entity = asRecord(entities[qid]);\n const labels = asRecord(entity.labels);\n const descriptions = asRecord(entity.descriptions);\n\n results.set(qid, {\n ...(pickLocalizedValue(labels, this.#language) === undefined\n ? {}\n : { label: pickLocalizedValue(labels, this.#language)! }),\n ...(pickLocalizedValue(descriptions, this.#language) === undefined\n ? {}\n : { description: pickLocalizedValue(descriptions, this.#language)! }),\n qid,\n sitelinks: SUPPORTED_WIKIS.flatMap((wiki) => {\n const title = getNestedString(entity, [\"sitelinks\", wiki, \"title\"]);\n\n return title === undefined ? [] : [{ title, wiki }];\n }),\n });\n }\n\n return results;\n }\n\n public async getPagesByTitles(\n titles: readonly string[],\n wiki: SupportedWiki = normalizeWiki(this.#wiki) ?? \"enwiki\",\n ): Promise<ReadonlyMap<string, WikiPageInfo>> {\n if (titles.length === 0) {\n return new Map();\n }\n\n const url = new URL(`${wikiApiBaseURL(wiki)}w/api.php`);\n url.searchParams.set(\"action\", \"query\");\n url.searchParams.set(\"titles\", titles.join(\"|\"));\n url.searchParams.set(\"prop\", \"pageprops\");\n url.searchParams.set(\"ppprop\", \"disambiguation|wikibase_item\");\n url.searchParams.set(\"format\", \"json\");\n url.searchParams.set(\"formatversion\", \"2\");\n\n const json = await this.#fetchJson(url);\n const pages = asArray(asRecord(asRecord(json.query).pages));\n const results = new Map<string, WikiPageInfo>();\n\n for (const pageValue of pages) {\n const page = asRecord(pageValue) as MediaWikiPage;\n const title = getString(page.title);\n\n if (title === undefined) {\n continue;\n }\n\n results.set(title, {\n isDisambiguation: asRecord(page.pageprops).disambiguation !== undefined,\n ...(getNumber(page.pageid) === undefined\n ? {}\n : { pageId: getNumber(page.pageid)! }),\n title,\n wiki,\n ...(getString(asRecord(page.pageprops).wikibase_item) === undefined\n ? {}\n : {\n wikibaseItem: getString(asRecord(page.pageprops).wikibase_item)!,\n }),\n });\n }\n\n return results;\n }\n\n public async parseDisambiguationPage(\n title: string,\n wiki: SupportedWiki,\n ): Promise<ParsedDisambiguationPage> {\n const url = new URL(`${wikiApiBaseURL(wiki)}w/api.php`);\n url.searchParams.set(\"action\", \"parse\");\n url.searchParams.set(\"page\", title);\n url.searchParams.set(\"prop\", \"text|properties\");\n url.searchParams.set(\"format\", \"json\");\n url.searchParams.set(\"formatversion\", \"2\");\n\n const json = await this.#fetchJson(url);\n const parse = asRecord(json.parse);\n const rendered = renderDisambiguationHtml(getNestedString(parse, [\"text\"]));\n\n return {\n linkedTitles: rendered.linkedTitles,\n ...(getNumber(parse.pageid) === undefined\n ? {}\n : { pageId: getNumber(parse.pageid)! }),\n text: rendered.text,\n title: getString(parse.title) ?? title,\n wiki,\n };\n }\n\n async #fetchJson(url: URL): Promise<Record<string, unknown>> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.#retryTimes; attempt += 1) {\n try {\n return await this.#limiter.use(\n async () => await this.#fetchJsonOnce(url, attempt + 1),\n );\n } catch (error) {\n lastError = error;\n\n if (!isRetryableError(error) || attempt >= this.#retryTimes) {\n this.#requestLog.warnFailed();\n throw error;\n }\n\n await delay(getRetryDelayMs(error, attempt, this.#retryBaseDelayMs));\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n }\n\n async #fetchJsonOnce(\n url: URL,\n attempt: number,\n ): Promise<Record<string, unknown>> {\n const startedAt = Date.now();\n let response: Response;\n\n try {\n response = await this.#fetch(\n url,\n this.#userAgent === undefined\n ? undefined\n : {\n headers: {\n \"User-Agent\": this.#userAgent,\n },\n },\n );\n } catch (error) {\n await this.#appendFetchLog({\n attempt,\n durationMs: Date.now() - startedAt,\n error,\n startedAt,\n url,\n });\n throw error;\n }\n\n const retryAfterMs = parseRetryAfterMs(response.headers.get(\"retry-after\"));\n\n if (retryAfterMs !== undefined) {\n this.#limiter.blockFor(retryAfterMs);\n }\n if (!response.ok) {\n const responseText = isTextResponse(response)\n ? await response.text()\n : undefined;\n\n await this.#appendFetchLog({\n attempt,\n durationMs: Date.now() - startedAt,\n response,\n ...(responseText === undefined ? {} : { responseText }),\n startedAt,\n url,\n });\n throw new WikimediaRequestError(url, response.status, retryAfterMs);\n }\n\n await this.#appendFetchLog({\n attempt,\n durationMs: Date.now() - startedAt,\n response,\n startedAt,\n url,\n });\n\n return asRecord(await response.json());\n }\n\n async #appendFetchLog(\n entry: Parameters<WikipageFetchLog[\"append\"]>[0],\n ): Promise<void> {\n try {\n await this.#requestLog.append(entry);\n } catch (error) {\n getLogger({ component: \"wikipage\" }).warn(\n `Failed to write wikipage fetch log entry: ${formatError(error)}`,\n );\n }\n }\n}\n\nfunction isTextResponse(response: Response): boolean {\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType === null) {\n return false;\n }\n\n return /(?:json|text|xml|javascript|html)/iu.test(contentType);\n}\n\nclass WikimediaRequestError extends Error {\n public readonly retryAfterMs: number | undefined;\n public readonly status: number;\n public readonly url: URL;\n\n public constructor(\n url: URL,\n status: number,\n retryAfterMs: number | undefined,\n ) {\n super(`Wikimedia request failed with ${status}: ${url.toString()}`);\n this.retryAfterMs = retryAfterMs;\n this.status = status;\n this.url = url;\n }\n}\n\nexport function replaceTitleUriWithQidUri(\n text: string,\n titleToQid: ReadonlyMap<string, string>,\n): string {\n return text.replace(\n /\\[\\[([^\\]|]+)\\|wikigraph-title:\\/\\/([^\\]\\s]+)\\]\\]/gu,\n (_match: string, label: string, encodedTitle: string) => {\n const title = decodeURIComponent(encodedTitle);\n const qid = titleToQid.get(title);\n\n return qid === undefined ? label : `[[${label}|wikg://qid=${qid}]]`;\n },\n );\n}\n\nfunction renderDisambiguationHtml(html: string | undefined): {\n readonly linkedTitles: readonly string[];\n readonly text: string;\n} {\n if (html === undefined || html.trim() === \"\") {\n return {\n linkedTitles: [],\n text: \"\",\n };\n }\n\n const linkedTitles = new Set<string>();\n const document = parseDocument(html);\n const text = renderNodes(DomUtils.getChildren(document), {\n linkedTitles,\n listDepth: 0,\n })\n .replace(/[ \\t]+\\n/gu, \"\\n\")\n .replace(/\\n{3,}/gu, \"\\n\\n\")\n .trim();\n\n return {\n linkedTitles: [...linkedTitles],\n text,\n };\n}\n\nfunction renderNodes(\n nodes: readonly unknown[],\n context: {\n readonly linkedTitles: Set<string>;\n readonly listDepth: number;\n },\n): string {\n return nodes.map((node) => renderNode(node, context)).join(\"\");\n}\n\nfunction renderNode(\n node: unknown,\n context: {\n readonly linkedTitles: Set<string>;\n readonly listDepth: number;\n },\n): string {\n const record = asRecord(node);\n const nodeType = getString(record.type);\n\n if (nodeType === \"text\") {\n return normalizeInlineText(getString(record.data) ?? \"\");\n }\n if (nodeType !== \"tag\" && nodeType !== \"script\" && nodeType !== \"style\") {\n return \"\";\n }\n\n const element = node as HtmlElement;\n if (shouldSkipElement(element)) {\n return \"\";\n }\n\n const name = element.name.toLowerCase();\n\n if (name === \"a\") {\n return renderLink(element, context);\n }\n if (/^h[1-6]$/u.test(name)) {\n const level = Math.min(6, Math.max(1, Number(name.slice(1))));\n const marker = \"=\".repeat(level);\n const heading = renderNodes(element.children ?? [], context).trim();\n\n return heading === \"\" ? \"\" : `\\n${marker} ${heading} ${marker}\\n`;\n }\n if (name === \"li\") {\n const indent = \" \".repeat(Math.max(0, context.listDepth - 1));\n const content = renderNodes(element.children ?? [], {\n ...context,\n listDepth: context.listDepth + 1,\n }).trim();\n\n return content === \"\" ? \"\" : `\\n${indent}* ${content}`;\n }\n if (name === \"ul\" || name === \"ol\") {\n return `${renderNodes(element.children ?? [], context)}\\n`;\n }\n if (isBlockElement(name)) {\n const content = renderNodes(element.children ?? [], context).trim();\n\n return content === \"\" ? \"\" : `\\n${content}\\n`;\n }\n\n return renderNodes(element.children ?? [], context);\n}\n\nfunction renderLink(\n node: HtmlElement,\n context: {\n readonly linkedTitles: Set<string>;\n readonly listDepth: number;\n },\n): string {\n const content = renderNodes(node.children ?? [], context).trim();\n const title = extractWikiLinkTitle(node);\n\n if (title === undefined) {\n return content;\n }\n\n context.linkedTitles.add(title);\n\n return `[[${content === \"\" ? title : content}|${TITLE_URI_PREFIX}${encodeURIComponent(title)}]]`;\n}\n\nfunction extractWikiLinkTitle(node: HtmlElement): string | undefined {\n const title = node.attribs.title;\n const href = node.attribs.href;\n\n if (title !== undefined && title !== \"\" && !title.includes(\":\")) {\n return title;\n }\n if (href === undefined || !href.startsWith(\"/wiki/\")) {\n return undefined;\n }\n\n const decoded = decodeURIComponent(href.slice(\"/wiki/\".length)).replaceAll(\n \"_\",\n \" \",\n );\n\n return decoded === \"\" || decoded.includes(\":\") ? undefined : decoded;\n}\n\nfunction shouldSkipElement(node: HtmlElement): boolean {\n const name = node.name.toLowerCase();\n\n if ([\"script\", \"style\", \"table\", \"sup\"].includes(name)) {\n return true;\n }\n\n const className = node.attribs.class ?? \"\";\n\n return [\n \"catlinks\",\n \"metadata\",\n \"mw-editsection\",\n \"mw-empty-elt\",\n \"navbox\",\n \"noprint\",\n \"reference\",\n \"reflist\",\n \"shortdescription\",\n \"toc\",\n ].some((item) => className.split(/\\s+/u).includes(item));\n}\n\nfunction isBlockElement(name: string): boolean {\n return [\"blockquote\", \"br\", \"dd\", \"div\", \"dl\", \"dt\", \"p\", \"section\"].includes(\n name,\n );\n}\n\nfunction normalizeInlineText(value: string): string {\n return value.replace(/\\s+/gu, \" \");\n}\n\nfunction listWikidataLanguages(language: string): string {\n return [...new Set([language, \"zh\", \"en\"])].join(\"|\");\n}\n\nfunction pickLocalizedValue(\n values: Record<string, unknown>,\n language: string,\n): string | undefined {\n for (const candidate of [language, \"zh\", \"en\"]) {\n const value = getNestedString(values, [candidate, \"value\"]);\n\n if (value !== undefined) {\n return value;\n }\n }\n\n return undefined;\n}\n\nfunction wikiApiBaseURL(wiki: SupportedWiki): string {\n return wiki === \"zhwiki\"\n ? \"https://zh.wikipedia.org/\"\n : \"https://en.wikipedia.org/\";\n}\n\nfunction normalizeWiki(value: string): SupportedWiki | undefined {\n return value === \"zhwiki\" || value === \"enwiki\" ? value : undefined;\n}\n\nfunction isRetryableError(error: unknown): boolean {\n if (error instanceof WikimediaRequestError) {\n return (\n error.status === 429 ||\n error.status === 500 ||\n error.status === 502 ||\n error.status === 503 ||\n error.status === 504\n );\n }\n\n return error instanceof TypeError;\n}\n\nfunction getRetryDelayMs(\n error: unknown,\n attempt: number,\n baseDelayMs: number,\n): number {\n if (\n error instanceof WikimediaRequestError &&\n error.retryAfterMs !== undefined\n ) {\n return error.retryAfterMs;\n }\n\n if (baseDelayMs <= 0) {\n return 0;\n }\n\n const exponentialDelayMs = baseDelayMs * 2 ** attempt;\n const jitterMs = Math.floor(Math.random() * Math.min(baseDelayMs, 250));\n\n return exponentialDelayMs + jitterMs;\n}\n\nasync function delay(ms: number): Promise<void> {\n if (ms <= 0) {\n return;\n }\n\n await new Promise<void>((resolveDelay) => {\n setTimeout(resolveDelay, ms);\n });\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction asArray(value: unknown): readonly unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\nfunction getNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value)\n ? value\n : undefined;\n}\n\nfunction getNestedString(\n value: Record<string, unknown>,\n path: readonly string[],\n): string | undefined {\n let current: unknown = value;\n\n for (const part of path) {\n current = asRecord(current)[part];\n }\n\n return getString(current);\n}\n","import { WikipageCache } from \"./cache.js\";\nimport { createWikipageFetchLog } from \"./fetch-log.js\";\nimport {\n WikimediaClient,\n replaceTitleUriWithQidUri,\n type SupportedWiki,\n type WikiPageInfo,\n type WikidataEntityInfo,\n} from \"./wikimedia-client.js\";\nimport type {\n CachedDisambiguationRecord,\n CachedPageRecord,\n CachedQidRecord,\n DisambiguationExpansion,\n DisambiguationLinkedQid,\n DisambiguationPageText,\n DisambiguationProfileNormalizer,\n QidResolution,\n WikipageResolveProgressReporter,\n WikipageResolverOptions,\n} from \"./types.js\";\n\nconst DEFAULT_CONCURRENCY = 3;\nconst DEFAULT_LANGUAGE = \"zh\";\nconst DEFAULT_MAX_BATCH_SIZE = 50;\nconst DEFAULT_MIN_REQUEST_INTERVAL_MS = 100;\nconst DEFAULT_RETRY_BASE_DELAY_MS = 1_000;\nconst DEFAULT_RETRY_TIMES = 3;\nconst DEFAULT_USER_AGENT =\n \"WikiGraph/0.3 (https://github.com/oomol-lab/wiki-graph)\";\n\nexport class WikipageResolver {\n readonly #cache: WikipageCache;\n readonly #client: WikimediaClient;\n readonly #maxBatchSize: number;\n readonly #language: string;\n readonly #normalizer: DisambiguationProfileNormalizer | undefined;\n readonly #ownsCache: boolean;\n readonly #progress: WikipageResolveProgressReporter | undefined;\n readonly #wiki: string;\n\n public constructor(input: {\n readonly cache: WikipageCache;\n readonly client: WikimediaClient;\n readonly language: string;\n readonly maxBatchSize: number;\n readonly normalizer: DisambiguationProfileNormalizer | undefined;\n readonly ownsCache: boolean;\n readonly progress: WikipageResolveProgressReporter | undefined;\n readonly wiki: string;\n }) {\n this.#cache = input.cache;\n this.#client = input.client;\n this.#language = input.language;\n this.#maxBatchSize = input.maxBatchSize;\n this.#normalizer = input.normalizer;\n this.#ownsCache = input.ownsCache;\n this.#progress = input.progress;\n this.#wiki = input.wiki;\n }\n\n public static async open(\n options: WikipageResolverOptions = {},\n ): Promise<WikipageResolver> {\n const language = normalizeLanguage(options.language);\n const wiki = options.wiki ?? `${language}wiki`;\n const cache = await WikipageCache.open(options.cacheDatabasePath);\n\n return new WikipageResolver({\n cache,\n client: new WikimediaClient({\n concurrency: options.concurrency ?? DEFAULT_CONCURRENCY,\n language,\n requestLog: createWikipageFetchLog(options.logDirPath),\n minRequestIntervalMs:\n options.minRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,\n retryBaseDelayMs:\n options.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS,\n retryTimes: options.retryTimes ?? DEFAULT_RETRY_TIMES,\n userAgent: options.userAgent ?? DEFAULT_USER_AGENT,\n wiki,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n }),\n language,\n maxBatchSize: normalizeBatchSize(options.maxBatchSize),\n normalizer: options.normalizer,\n ownsCache: true,\n progress: options.progress,\n wiki,\n });\n }\n\n public async close(): Promise<void> {\n if (this.#ownsCache) {\n await this.#cache.close();\n }\n }\n\n public async resolveQids(\n qids: readonly string[],\n ): Promise<readonly QidResolution[]> {\n const normalizedQids = normalizeQids(qids);\n const qidRecords = await this.#resolveQidRecords(normalizedQids);\n const disambiguationRecords = await this.#resolveDisambiguations(\n [...qidRecords.values()].filter(hasDisambiguationPage),\n );\n\n return normalizedQids.map((qid) => {\n const record = qidRecords.get(qid);\n\n if (record === undefined) {\n return {\n isDisambiguation: false,\n qid,\n };\n }\n\n const disambiguation = disambiguationRecords.get(qid);\n const disambiguationPages = disambiguation?.pages ?? [];\n const primarySitelink = pickPrimarySitelink(record.sitelinks, this.#wiki);\n\n return {\n ...(record.description === undefined\n ? {}\n : { description: record.description }),\n ...(disambiguation === undefined\n ? {}\n : { disambiguation: toDisambiguationExpansion(disambiguation) }),\n ...(disambiguationPages.length === 0 ? {} : { disambiguationPages }),\n isDisambiguation: disambiguationPages.length > 0,\n ...(record.label === undefined ? {} : { label: record.label }),\n qid,\n ...(primarySitelink === undefined ? {} : { sitelink: primarySitelink }),\n ...(record.sitelinks.length === 0\n ? {}\n : { sitelinks: record.sitelinks.map(toSitelink) }),\n };\n });\n }\n\n async #resolveQidRecords(\n qids: readonly string[],\n ): Promise<ReadonlyMap<string, CachedQidRecord>> {\n const cached = new Map(await this.#cache.getQids(qids, this.#language));\n const missing = qids.filter((qid) => !cached.has(qid));\n let resolvedQids = cached.size;\n let resolvedEntities = 0;\n\n await this.#reportProgress(\"qid\", resolvedQids, qids.length);\n\n for (const batch of chunk(missing, this.#maxBatchSize)) {\n const entityInfos = await this.#client.getEntities(batch);\n resolvedEntities += batch.length;\n await this.#reportProgress(\"entity\", resolvedEntities, missing.length);\n const pageInfos = await this.#fetchPageInfos(entityInfos);\n const now = new Date().toISOString();\n const records = batch.map((qid) =>\n createQidRecord(qid, entityInfos.get(qid), pageInfos, now),\n );\n\n await this.#cache.putQids(records, this.#language);\n for (const record of records) {\n cached.set(record.qid, record);\n }\n resolvedQids += batch.length;\n await this.#reportProgress(\"qid\", resolvedQids, qids.length);\n }\n\n return cached;\n }\n\n async #fetchPageInfos(\n entityInfos: ReadonlyMap<string, WikidataEntityInfo>,\n ): Promise<ReadonlyMap<string, WikiPageInfo>> {\n const results = new Map<string, WikiPageInfo>();\n const titlesByWiki = new Map<SupportedWiki, Set<string>>();\n\n for (const entity of entityInfos.values()) {\n for (const sitelink of entity.sitelinks) {\n const titles = titlesByWiki.get(sitelink.wiki) ?? new Set<string>();\n\n titles.add(sitelink.title);\n titlesByWiki.set(sitelink.wiki, titles);\n }\n }\n\n for (const [wiki, titles] of titlesByWiki) {\n for (const batch of chunk([...titles], this.#maxBatchSize)) {\n for (const [title, page] of await this.#client.getPagesByTitles(\n batch,\n wiki,\n )) {\n results.set(pageKey(wiki, title), page);\n }\n await this.#reportProgress(\n \"page\",\n results.size,\n countTitleSetItems(titlesByWiki),\n );\n }\n }\n\n return results;\n }\n\n async #resolveDisambiguations(\n records: readonly CachedQidRecord[],\n ): Promise<ReadonlyMap<string, CachedDisambiguationRecord>> {\n const qids = records.map((record) => record.qid);\n const cached = new Map(\n await this.#cache.getDisambiguations(qids, this.#wiki),\n );\n const missing = records.filter((record) => !cached.has(record.qid));\n let resolvedPages = cached.size;\n\n await this.#reportProgress(\n \"disambiguation-page\",\n resolvedPages,\n qids.length,\n );\n\n for (const record of missing) {\n const expansion = await this.#expandDisambiguation(record);\n\n await this.#cache.putDisambiguations([expansion], this.#wiki);\n cached.set(record.qid, expansion);\n resolvedPages += 1;\n await this.#reportProgress(\n \"disambiguation-page\",\n resolvedPages,\n qids.length,\n );\n }\n\n return cached;\n }\n\n async #expandDisambiguation(\n record: CachedQidRecord,\n ): Promise<CachedDisambiguationRecord> {\n const pages: DisambiguationPageText[] = [];\n\n for (const page of record.sitelinks.filter(\n (item) => item.isDisambiguation,\n )) {\n const parsedPage = await this.#client.parseDisambiguationPage(\n page.title,\n page.wiki,\n );\n const linkedPageInfos = new Map<string, WikiPageInfo>();\n\n for (const batch of chunk(parsedPage.linkedTitles, this.#maxBatchSize)) {\n for (const [title, linkedPage] of await this.#client.getPagesByTitles(\n batch,\n page.wiki,\n )) {\n linkedPageInfos.set(title, linkedPage);\n }\n await this.#reportProgress(\n \"linked-page\",\n linkedPageInfos.size,\n parsedPage.linkedTitles.length,\n );\n }\n\n const titleToQid = new Map<string, string>();\n const linkedQids: DisambiguationLinkedQid[] = [];\n\n for (const title of parsedPage.linkedTitles) {\n const qid = linkedPageInfos.get(title)?.wikibaseItem;\n\n if (qid === undefined) {\n continue;\n }\n\n titleToQid.set(title, qid);\n linkedQids.push({ qid, title });\n }\n\n pages.push({\n linkedQids,\n ...(parsedPage.pageId === undefined\n ? {}\n : { pageId: parsedPage.pageId }),\n text: replaceTitleUriWithQidUri(parsedPage.text, titleToQid),\n title: parsedPage.title,\n wiki: parsedPage.wiki,\n });\n }\n\n const linkedQids = mergeLinkedQids(pages);\n const profile =\n this.#normalizer === undefined || linkedQids.length === 0\n ? undefined\n : await this.#normalizer({\n pageQidLinks: linkedQids,\n pages,\n sourceQid: record.qid,\n ...(record.label === undefined ? {} : { surface: record.label }),\n });\n\n return {\n checkedAt: new Date().toISOString(),\n disambiguationQid: record.qid,\n pages,\n ...(profile === undefined ? {} : { profile }),\n };\n }\n\n async #reportProgress(\n detail: Parameters<WikipageResolveProgressReporter>[0][\"detail\"],\n done: number,\n total: number,\n ): Promise<void> {\n await this.#progress?.({\n detail,\n done: Math.min(Math.max(0, done), Math.max(0, total)),\n total: Math.max(0, total),\n });\n }\n}\n\nfunction createQidRecord(\n qid: string,\n entityInfo: WikidataEntityInfo | undefined,\n pageInfos: ReadonlyMap<string, WikiPageInfo>,\n now: string,\n): CachedQidRecord {\n return {\n checkedAt: now,\n ...(entityInfo?.description === undefined\n ? {}\n : { description: entityInfo.description }),\n ...(entityInfo?.label === undefined ? {} : { label: entityInfo.label }),\n qid,\n sitelinks:\n entityInfo?.sitelinks.flatMap((sitelink): CachedPageRecord[] => {\n const page = pageInfos.get(pageKey(sitelink.wiki, sitelink.title));\n\n if (page === undefined) {\n return [];\n }\n\n return [\n {\n isDisambiguation: page.isDisambiguation,\n ...(page.pageId === undefined ? {} : { pageId: page.pageId }),\n title: page.title,\n wiki: page.wiki,\n },\n ];\n }) ?? [],\n updatedAt: now,\n };\n}\n\nfunction toDisambiguationExpansion(\n record: CachedDisambiguationRecord,\n): DisambiguationExpansion {\n return {\n checkedAt: record.checkedAt,\n disambiguationQid: record.disambiguationQid,\n linkedQids: mergeLinkedQids(record.pages),\n pages: record.pages,\n ...(record.profile === undefined ? {} : { profile: record.profile }),\n };\n}\n\nfunction mergeLinkedQids(\n pages: readonly DisambiguationPageText[],\n): readonly DisambiguationLinkedQid[] {\n const results = new Map<string, DisambiguationLinkedQid>();\n\n for (const page of pages) {\n for (const item of page.linkedQids) {\n results.set(item.qid, item);\n }\n }\n\n return [...results.values()];\n}\n\nfunction countTitleSetItems(\n input: ReadonlyMap<SupportedWiki, Set<string>>,\n): number {\n let count = 0;\n\n for (const titles of input.values()) {\n count += titles.size;\n }\n\n return count;\n}\n\nfunction hasDisambiguationPage(record: CachedQidRecord): boolean {\n return record.sitelinks.some((page) => page.isDisambiguation);\n}\n\nfunction pickPrimarySitelink(\n pages: readonly CachedPageRecord[],\n wiki: string,\n): { readonly title: string; readonly wiki: string } | undefined {\n const preferred = pages.find((page) => page.wiki === wiki) ?? pages[0];\n\n return preferred === undefined ? undefined : toSitelink(preferred);\n}\n\nfunction toSitelink(page: CachedPageRecord): {\n readonly title: string;\n readonly wiki: string;\n} {\n return {\n title: page.title,\n wiki: page.wiki,\n };\n}\n\nfunction pageKey(wiki: SupportedWiki, title: string): string {\n return `${wiki}:${title}`;\n}\n\nfunction normalizeQids(qids: readonly string[]): readonly string[] {\n return [\n ...new Set(\n qids\n .map((qid) => qid.trim().toUpperCase())\n .filter((qid) => /^Q[1-9]\\d*$/u.test(qid)),\n ),\n ];\n}\n\nfunction normalizeLanguage(value: string | undefined): string {\n const normalized = value?.trim().toLowerCase();\n\n if (normalized === \"en\" || normalized === \"zh\") {\n return normalized;\n }\n\n return DEFAULT_LANGUAGE;\n}\n\nfunction normalizeBatchSize(value: number | undefined): number {\n if (value === undefined || !Number.isFinite(value)) {\n return DEFAULT_MAX_BATCH_SIZE;\n }\n\n return Math.max(1, Math.floor(value));\n}\n\nfunction chunk<T>(items: readonly T[], size: number): readonly T[][] {\n const batches: T[][] = [];\n\n for (let index = 0; index < items.length; index += size) {\n batches.push(items.slice(index, index + size));\n }\n\n return batches;\n}\n","export { runWikipageCacheGc, WikipageCache } from \"./cache.js\";\nexport {\n createDisambiguationProfileNormalizer,\n type CreateDisambiguationProfileNormalizerOptions,\n} from \"./normalizer.js\";\nexport { RateLimiter, parseRetryAfterMs } from \"./rate-limiter.js\";\nexport { WikipageResolver } from \"./resolver.js\";\nexport { WikimediaClient } from \"./wikimedia-client.js\";\nexport type {\n CachedDisambiguationRecord,\n CachedPageRecord,\n CachedQidRecord,\n DisambiguationExpansion,\n DisambiguationLinkedQid,\n DisambiguationPageText,\n DisambiguationMeaningPriority,\n DisambiguationProfile,\n DisambiguationProfileMeaning,\n DisambiguationProfileNormalizer,\n DisambiguationProfileNormalizerInput,\n QidResolution,\n WikipageResolveProgress,\n WikipageResolveProgressDetail,\n WikipageResolveProgressReporter,\n WikipageResolverOptions,\n WikipageSitelink,\n} from \"./types.js\";\n","import type {\n ChunkImportance,\n ChunkRecord,\n ChunkRetention,\n ReadingEdgeRecord,\n ReadonlyDocument,\n SentenceId,\n} from \"../document/index.js\";\n\nexport interface GraphNode {\n readonly content: string;\n readonly id: number;\n readonly importance?: ChunkImportance;\n readonly label: string;\n readonly retention?: ChunkRetention;\n readonly sentenceIds: readonly SentenceId[];\n readonly weight: number;\n readonly wordsCount: number;\n}\n\nexport interface GraphEdge {\n readonly fromId: number;\n readonly strength?: string;\n readonly toId: number;\n readonly weight: number;\n}\n\nexport interface GraphEvidenceLine {\n readonly sentenceId: SentenceId;\n readonly text: string;\n}\n\nexport interface GraphStatus {\n readonly chapterId: number;\n readonly edgeCount: number;\n readonly graphReady: boolean;\n readonly nodeCount: number;\n}\n\nexport interface GraphSearchHit {\n readonly node: GraphNode;\n readonly matchedFields: readonly GraphSearchField[];\n}\n\nexport type GraphSearchField = \"content\" | \"evidence\" | \"label\";\n\nexport interface GraphNeighbor {\n readonly direction: \"incoming\" | \"outgoing\";\n readonly edge: GraphEdge;\n readonly node: GraphNode;\n}\n\nexport interface GraphPathStep {\n readonly edge?: GraphEdge;\n readonly node: GraphNode;\n}\n\nexport async function getGraphStatus(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<GraphStatus> {\n await requireChapter(document, chapterId);\n const [serial, nodes, edges] = await Promise.all([\n document.serials.getById(chapterId),\n listGraphNodes(document, chapterId),\n document.readingEdges.listBySerial(chapterId),\n ]);\n\n return {\n chapterId,\n edgeCount: edges.length,\n graphReady: serial?.topologyReady === true,\n nodeCount: nodes.length,\n };\n}\n\nexport async function listGraphNodes(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<readonly GraphNode[]> {\n await requireChapter(document, chapterId);\n const chunks = await document.chunks.listBySerial(chapterId);\n\n return chunks.map(formatGraphNode).sort(compareNodeByWeightDescending);\n}\n\nexport async function getGraphNode(\n document: ReadonlyDocument,\n chapterId: number,\n nodeId: number,\n): Promise<GraphNode> {\n const node = formatGraphNode(\n await requireChapterNode(document, chapterId, nodeId),\n );\n\n return node;\n}\n\nexport async function searchGraphNodes(\n document: ReadonlyDocument,\n chapterId: number,\n pattern: string,\n): Promise<readonly GraphSearchHit[]> {\n const normalizedPattern = pattern.trim().toLowerCase();\n\n if (normalizedPattern === \"\") {\n return [];\n }\n\n const nodes = await listGraphNodes(document, chapterId);\n const hits: GraphSearchHit[] = [];\n\n for (const node of nodes) {\n const evidence = await getGraphEvidence(document, chapterId, node.id);\n const matchedFields: GraphSearchField[] = [];\n\n if (node.label.toLowerCase().includes(normalizedPattern)) {\n matchedFields.push(\"label\");\n }\n if (node.content.toLowerCase().includes(normalizedPattern)) {\n matchedFields.push(\"content\");\n }\n if (\n evidence.some((line) =>\n line.text.toLowerCase().includes(normalizedPattern),\n )\n ) {\n matchedFields.push(\"evidence\");\n }\n\n if (matchedFields.length > 0) {\n hits.push({ matchedFields, node });\n }\n }\n\n return hits;\n}\n\nexport async function listGraphNeighbors(\n document: ReadonlyDocument,\n chapterId: number,\n nodeId: number,\n): Promise<readonly GraphNeighbor[]> {\n await requireChapterNode(document, chapterId, nodeId);\n const [incoming, outgoing] = await Promise.all([\n document.readingEdges.listIncoming(nodeId),\n document.readingEdges.listOutgoing(nodeId),\n ]);\n const neighbors = await Promise.all([\n ...incoming.map(async (edge) => ({\n direction: \"incoming\" as const,\n edge: formatGraphEdge(edge),\n node: formatGraphNode(\n await requireChapterNode(document, chapterId, edge.fromId),\n ),\n })),\n ...outgoing.map(async (edge) => ({\n direction: \"outgoing\" as const,\n edge: formatGraphEdge(edge),\n node: formatGraphNode(\n await requireChapterNode(document, chapterId, edge.toId),\n ),\n })),\n ]);\n\n return neighbors.sort(compareNeighbor);\n}\n\nexport async function getGraphEvidence(\n document: ReadonlyDocument,\n chapterId: number,\n nodeId: number,\n): Promise<readonly GraphEvidenceLine[]> {\n const node = await requireChapterNode(document, chapterId, nodeId);\n const lines = await Promise.all(\n node.sentenceIds.map(async (sentenceId) => ({\n sentenceId,\n text: await document.getSentence(sentenceId),\n })),\n );\n\n return lines;\n}\n\nexport async function findGraphPath(\n document: ReadonlyDocument,\n chapterId: number,\n fromNodeId: number,\n toNodeId: number,\n): Promise<readonly GraphPathStep[]> {\n await requireChapterNode(document, chapterId, fromNodeId);\n await requireChapterNode(document, chapterId, toNodeId);\n\n if (fromNodeId === toNodeId) {\n return [{ node: await getGraphNode(document, chapterId, fromNodeId) }];\n }\n\n const edges = (await document.readingEdges.listBySerial(chapterId)).map(\n formatGraphEdge,\n );\n const outgoing = new Map<number, GraphEdge[]>();\n\n for (const edge of edges) {\n const existing = outgoing.get(edge.fromId) ?? [];\n\n existing.push(edge);\n outgoing.set(edge.fromId, existing);\n }\n\n const visited = new Set<number>([fromNodeId]);\n const queue: number[] = [fromNodeId];\n const previous = new Map<\n number,\n { readonly edge: GraphEdge; readonly id: number }\n >();\n\n while (queue.length > 0) {\n const currentId = queue.shift()!;\n\n for (const edge of outgoing.get(currentId) ?? []) {\n if (visited.has(edge.toId)) {\n continue;\n }\n\n visited.add(edge.toId);\n previous.set(edge.toId, { edge, id: currentId });\n\n if (edge.toId === toNodeId) {\n return await buildPathSteps(\n document,\n chapterId,\n fromNodeId,\n toNodeId,\n previous,\n );\n }\n\n queue.push(edge.toId);\n }\n }\n\n return [];\n}\n\nasync function buildPathSteps(\n document: ReadonlyDocument,\n chapterId: number,\n fromNodeId: number,\n toNodeId: number,\n previous: ReadonlyMap<\n number,\n { readonly edge: GraphEdge; readonly id: number }\n >,\n): Promise<readonly GraphPathStep[]> {\n const steps: Array<{ readonly edge?: GraphEdge; readonly nodeId: number }> = [\n { nodeId: toNodeId },\n ];\n let currentId = toNodeId;\n\n while (currentId !== fromNodeId) {\n const previousStep = previous.get(currentId);\n\n if (previousStep === undefined) {\n return [];\n }\n\n steps.push({ edge: previousStep.edge, nodeId: previousStep.id });\n currentId = previousStep.id;\n }\n\n steps.reverse();\n\n return await Promise.all(\n steps.map(async (step) => ({\n ...(step.edge === undefined ? {} : { edge: step.edge }),\n node: await getGraphNode(document, chapterId, step.nodeId),\n })),\n );\n}\n\nasync function requireChapter(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<void> {\n const serial = await document.serials.getById(chapterId);\n\n if (serial === undefined) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n}\n\nasync function requireChapterNode(\n document: ReadonlyDocument,\n chapterId: number,\n nodeId: number,\n): Promise<ChunkRecord> {\n const chunk = await document.chunks.getById(nodeId);\n\n if (chunk === undefined || chunk.sentenceId[0] !== chapterId) {\n throw new Error(\n `Graph node ${nodeId} does not exist in chapter ${chapterId}. Use \\`wg <archive-uri>/chapter/${chapterId}/chunk list\\` to discover chunk ids.`,\n );\n }\n\n return chunk;\n}\n\nfunction formatGraphNode(chunk: ChunkRecord): GraphNode {\n return {\n content: chunk.content,\n id: chunk.id,\n label: chunk.label,\n sentenceIds: chunk.sentenceIds,\n weight: chunk.weight,\n wordsCount: chunk.wordsCount,\n ...(chunk.importance === undefined ? {} : { importance: chunk.importance }),\n ...(chunk.retention === undefined ? {} : { retention: chunk.retention }),\n };\n}\n\nfunction formatGraphEdge(edge: ReadingEdgeRecord): GraphEdge {\n return {\n fromId: edge.fromId,\n toId: edge.toId,\n weight: edge.weight,\n ...(edge.strength === undefined ? {} : { strength: edge.strength }),\n };\n}\n\nfunction compareNodeByWeightDescending(\n left: GraphNode,\n right: GraphNode,\n): number {\n return right.weight - left.weight || left.id - right.id;\n}\n\nfunction compareNeighbor(left: GraphNeighbor, right: GraphNeighbor): number {\n if (left.direction !== right.direction) {\n return left.direction === \"incoming\" ? -1 : 1;\n }\n\n return right.edge.weight - left.edge.weight || left.node.id - right.node.id;\n}\n","export enum Language {\n Arabic = \"Arabic\",\n Danish = \"Danish\",\n Dutch = \"Dutch\",\n English = \"English\",\n Finnish = \"Finnish\",\n French = \"French\",\n German = \"German\",\n Hindi = \"Hindi\",\n Indonesian = \"Indonesian\",\n Italian = \"Italian\",\n Japanese = \"Japanese\",\n Korean = \"Korean\",\n Norwegian = \"Norwegian\",\n Polish = \"Polish\",\n Portuguese = \"Portuguese\",\n Russian = \"Russian\",\n SimplifiedChinese = \"Simplified Chinese\",\n Spanish = \"Spanish\",\n Swedish = \"Swedish\",\n Thai = \"Thai\",\n TraditionalChinese = \"Traditional Chinese\",\n Turkish = \"Turkish\",\n Vietnamese = \"Vietnamese\",\n}\n\nexport enum LanguageCode {\n Arabic = \"ar\",\n Chinese = \"zh\",\n Danish = \"da\",\n Dutch = \"nl\",\n English = \"en\",\n Finnish = \"fi\",\n French = \"fr\",\n German = \"de\",\n Hindi = \"hi\",\n Indonesian = \"id\",\n Italian = \"it\",\n Japanese = \"ja\",\n Korean = \"ko\",\n Norwegian = \"no\",\n Polish = \"pl\",\n Portuguese = \"pt\",\n Russian = \"ru\",\n Spanish = \"es\",\n Swedish = \"sv\",\n Thai = \"th\",\n Turkish = \"tr\",\n Vietnamese = \"vi\",\n}\n\nconst LANGUAGE_TO_CODE = {\n [Language.Arabic]: LanguageCode.Arabic,\n [Language.Danish]: LanguageCode.Danish,\n [Language.Dutch]: LanguageCode.Dutch,\n [Language.English]: LanguageCode.English,\n [Language.Finnish]: LanguageCode.Finnish,\n [Language.French]: LanguageCode.French,\n [Language.German]: LanguageCode.German,\n [Language.Hindi]: LanguageCode.Hindi,\n [Language.Indonesian]: LanguageCode.Indonesian,\n [Language.Italian]: LanguageCode.Italian,\n [Language.Japanese]: LanguageCode.Japanese,\n [Language.Korean]: LanguageCode.Korean,\n [Language.Norwegian]: LanguageCode.Norwegian,\n [Language.Polish]: LanguageCode.Polish,\n [Language.Portuguese]: LanguageCode.Portuguese,\n [Language.Russian]: LanguageCode.Russian,\n [Language.SimplifiedChinese]: LanguageCode.Chinese,\n [Language.Spanish]: LanguageCode.Spanish,\n [Language.Swedish]: LanguageCode.Swedish,\n [Language.Thai]: LanguageCode.Thai,\n [Language.TraditionalChinese]: LanguageCode.Chinese,\n [Language.Turkish]: LanguageCode.Turkish,\n [Language.Vietnamese]: LanguageCode.Vietnamese,\n} satisfies Record<Language, LanguageCode>;\n\nconst LANGUAGE_CODE_TO_PROMPT_LANGUAGE = {\n [LanguageCode.Arabic]: Language.Arabic,\n [LanguageCode.Chinese]: Language.SimplifiedChinese,\n [LanguageCode.Danish]: Language.Danish,\n [LanguageCode.Dutch]: Language.Dutch,\n [LanguageCode.English]: Language.English,\n [LanguageCode.Finnish]: Language.Finnish,\n [LanguageCode.French]: Language.French,\n [LanguageCode.German]: Language.German,\n [LanguageCode.Hindi]: Language.Hindi,\n [LanguageCode.Indonesian]: Language.Indonesian,\n [LanguageCode.Italian]: Language.Italian,\n [LanguageCode.Japanese]: Language.Japanese,\n [LanguageCode.Korean]: Language.Korean,\n [LanguageCode.Norwegian]: Language.Norwegian,\n [LanguageCode.Polish]: Language.Polish,\n [LanguageCode.Portuguese]: Language.Portuguese,\n [LanguageCode.Russian]: Language.Russian,\n [LanguageCode.Spanish]: Language.Spanish,\n [LanguageCode.Swedish]: Language.Swedish,\n [LanguageCode.Thai]: Language.Thai,\n [LanguageCode.Turkish]: Language.Turkish,\n [LanguageCode.Vietnamese]: Language.Vietnamese,\n} satisfies Record<LanguageCode, Language>;\n\nconst LANGUAGE_CODE_ALIASES = new Map<string, LanguageCode>([\n [\"ar\", LanguageCode.Arabic],\n [\"ara\", LanguageCode.Arabic],\n [\"arabic\", LanguageCode.Arabic],\n [\"da\", LanguageCode.Danish],\n [\"danish\", LanguageCode.Danish],\n [\"de\", LanguageCode.German],\n [\"deu\", LanguageCode.German],\n [\"dut\", LanguageCode.Dutch],\n [\"dutch\", LanguageCode.Dutch],\n [\"en\", LanguageCode.English],\n [\"eng\", LanguageCode.English],\n [\"english\", LanguageCode.English],\n [\"fi\", LanguageCode.Finnish],\n [\"fin\", LanguageCode.Finnish],\n [\"finnish\", LanguageCode.Finnish],\n [\"fr\", LanguageCode.French],\n [\"fra\", LanguageCode.French],\n [\"fre\", LanguageCode.French],\n [\"french\", LanguageCode.French],\n [\"ger\", LanguageCode.German],\n [\"german\", LanguageCode.German],\n [\"hi\", LanguageCode.Hindi],\n [\"hin\", LanguageCode.Hindi],\n [\"hindi\", LanguageCode.Hindi],\n [\"id\", LanguageCode.Indonesian],\n [\"ind\", LanguageCode.Indonesian],\n [\"indonesian\", LanguageCode.Indonesian],\n [\"it\", LanguageCode.Italian],\n [\"ita\", LanguageCode.Italian],\n [\"italian\", LanguageCode.Italian],\n [\"ja\", LanguageCode.Japanese],\n [\"japanese\", LanguageCode.Japanese],\n [\"jpn\", LanguageCode.Japanese],\n [\"ko\", LanguageCode.Korean],\n [\"kor\", LanguageCode.Korean],\n [\"korean\", LanguageCode.Korean],\n [\"nl\", LanguageCode.Dutch],\n [\"no\", LanguageCode.Norwegian],\n [\"nor\", LanguageCode.Norwegian],\n [\"norwegian\", LanguageCode.Norwegian],\n [\"pl\", LanguageCode.Polish],\n [\"pol\", LanguageCode.Polish],\n [\"polish\", LanguageCode.Polish],\n [\"por\", LanguageCode.Portuguese],\n [\"portuguese\", LanguageCode.Portuguese],\n [\"pt\", LanguageCode.Portuguese],\n [\"ru\", LanguageCode.Russian],\n [\"rus\", LanguageCode.Russian],\n [\"russian\", LanguageCode.Russian],\n [\"spa\", LanguageCode.Spanish],\n [\"spanish\", LanguageCode.Spanish],\n [\"es\", LanguageCode.Spanish],\n [\"sv\", LanguageCode.Swedish],\n [\"swe\", LanguageCode.Swedish],\n [\"swedish\", LanguageCode.Swedish],\n [\"th\", LanguageCode.Thai],\n [\"tha\", LanguageCode.Thai],\n [\"thai\", LanguageCode.Thai],\n [\"tr\", LanguageCode.Turkish],\n [\"tur\", LanguageCode.Turkish],\n [\"turkish\", LanguageCode.Turkish],\n [\"vi\", LanguageCode.Vietnamese],\n [\"vie\", LanguageCode.Vietnamese],\n [\"vietnamese\", LanguageCode.Vietnamese],\n [\"zh\", LanguageCode.Chinese],\n [\"zh-cn\", LanguageCode.Chinese],\n [\"zh-hans\", LanguageCode.Chinese],\n [\"zh-sg\", LanguageCode.Chinese],\n [\"zh-tw\", LanguageCode.Chinese],\n [\"zh-hant\", LanguageCode.Chinese],\n [\"zh-hk\", LanguageCode.Chinese],\n [\"cn\", LanguageCode.Chinese],\n [\"chi\", LanguageCode.Chinese],\n [\"chinese\", LanguageCode.Chinese],\n [\"simplified chinese\", LanguageCode.Chinese],\n [\"traditional chinese\", LanguageCode.Chinese],\n [\"zho\", LanguageCode.Chinese],\n]);\n\nexport function getLanguageCode(language: Language): LanguageCode {\n return LANGUAGE_TO_CODE[language];\n}\n\nexport function formatLanguageForPrompt(code: LanguageCode): Language {\n return LANGUAGE_CODE_TO_PROMPT_LANGUAGE[code];\n}\n\nexport function normalizeLanguageCode(\n language: Language | string | null | undefined,\n): LanguageCode | undefined {\n const normalized = language?.trim().toLowerCase().replaceAll(\"_\", \"-\");\n\n if (normalized === undefined || normalized === \"\") {\n return undefined;\n }\n\n return LANGUAGE_CODE_ALIASES.get(normalized);\n}\n\nexport function getWikipageLanguageCode(\n language: Language | string | null | undefined,\n): \"en\" | \"zh\" | undefined {\n const code = normalizeLanguageCode(language);\n\n if (code === LanguageCode.English || code === LanguageCode.Chinese) {\n return code;\n }\n\n return undefined;\n}\n","export class AsyncSemaphore {\n #activeCount = 0;\n readonly #concurrency: number;\n readonly #pendingResolvers: Array<() => void> = [];\n\n public constructor(concurrency: number) {\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new RangeError(\"concurrency must be a positive integer\");\n }\n\n this.#concurrency = concurrency;\n }\n\n public async use<T>(operation: () => Promise<T> | T): Promise<T> {\n await this.#acquire();\n\n try {\n return await operation();\n } finally {\n this.#release();\n }\n }\n\n async #acquire(): Promise<void> {\n if (this.#activeCount < this.#concurrency) {\n this.#activeCount += 1;\n return;\n }\n\n await new Promise<void>((resolve) => {\n this.#pendingResolvers.push(() => {\n this.#activeCount += 1;\n resolve();\n });\n });\n }\n\n #release(): void {\n this.#activeCount -= 1;\n\n const next = this.#pendingResolvers.shift();\n next?.();\n }\n}\n","export enum WikiGraphScope {\n EditorCompress = \"serial-generation/editor-compress\",\n EditorReview = \"serial-generation/editor-review\",\n EditorReviewGuide = \"serial-generation/editor-review-guide\",\n ReaderChoice = \"serial-generation/reader-choice\",\n ReaderExtraction = \"serial-generation/reader-extraction\",\n}\n\nexport const WIKI_GRAPH_SCOPES = Object.freeze([\n WikiGraphScope.EditorCompress,\n WikiGraphScope.EditorReview,\n WikiGraphScope.EditorReviewGuide,\n WikiGraphScope.ReaderChoice,\n WikiGraphScope.ReaderExtraction,\n] as const);\n\nexport const WIKI_GRAPH_EDITOR_SCOPES = Object.freeze({\n compress: WikiGraphScope.EditorCompress,\n review: WikiGraphScope.EditorReview,\n reviewGuide: WikiGraphScope.EditorReviewGuide,\n});\n\nexport const WIKI_GRAPH_READER_SCOPES = Object.freeze({\n choice: WikiGraphScope.ReaderChoice,\n extraction: WikiGraphScope.ReaderExtraction,\n});\n","import type { CognitiveChunk } from \"../chunk-batch/types.js\";\n\nexport class WaveReflection {\n readonly #generationDecayFactor: number;\n\n public constructor(generationDecayFactor: number) {\n this.#generationDecayFactor = generationDecayFactor;\n }\n\n public selectTopChunks(input: {\n allChunks: readonly CognitiveChunk[];\n capacity: number;\n getSuccessorChunkIds: (chunkId: number) => readonly number[];\n latestChunkIds: readonly number[];\n }): CognitiveChunk[] {\n if (input.allChunks.length === 0) {\n return [];\n }\n\n if (input.latestChunkIds.length === 0) {\n return [...input.allChunks].slice(0, input.capacity);\n }\n\n const latestChunkIdRecord = createMembershipRecord(input.latestChunkIds);\n const historicalChunks = input.allChunks.filter(\n (chunk) => !hasMembership(latestChunkIdRecord, chunk.id),\n );\n\n if (historicalChunks.length === 0) {\n return [];\n }\n\n const forwardScores = this.#forwardPropagation({\n getSuccessorChunkIds: input.getSuccessorChunkIds,\n latestChunkIds: input.latestChunkIds,\n });\n\n if (countOwnKeys(forwardScores) === 0) {\n return [...historicalChunks]\n .sort((left, right) => right.generation - left.generation)\n .slice(0, input.capacity);\n }\n\n const reflectionScores = this.#reflectionPropagation({\n allChunkIds: input.allChunks.map((chunk) => chunk.id),\n forwardScores,\n getSuccessorChunkIds: input.getSuccessorChunkIds,\n });\n\n return historicalChunks\n .map((chunk) => ({\n chunk,\n decayedScore:\n (reflectionScores[String(chunk.id)] ?? 0) *\n this.#generationDecayFactor ** chunk.generation,\n }))\n .sort((left, right) => {\n if (left.decayedScore !== right.decayedScore) {\n return right.decayedScore - left.decayedScore;\n }\n\n return left.chunk.id - right.chunk.id;\n })\n .slice(0, input.capacity)\n .map((item) => item.chunk);\n }\n\n #forwardPropagation(input: {\n getSuccessorChunkIds: (chunkId: number) => readonly number[];\n latestChunkIds: readonly number[];\n }): Record<string, number> {\n const latestChunkCount = input.latestChunkIds.length;\n\n if (latestChunkCount === 0) {\n return createEmptyRecord<number>();\n }\n\n const initialScore = 1 / latestChunkCount;\n const scores = createEmptyRecord<number>();\n const visited = createEmptyRecord<true>();\n const queue = [...input.latestChunkIds];\n let queueOffset = 0;\n\n for (const chunkId of input.latestChunkIds) {\n scores[String(chunkId)] = initialScore;\n visited[String(chunkId)] = true;\n }\n\n while (queueOffset < queue.length) {\n const currentId = queue[queueOffset];\n queueOffset += 1;\n\n if (currentId === undefined) {\n continue;\n }\n\n const currentScore = scores[String(currentId)];\n\n if (currentScore === undefined) {\n continue;\n }\n\n const successors = [...input.getSuccessorChunkIds(currentId)].sort(\n compareNumber,\n );\n\n if (successors.length === 0) {\n continue;\n }\n\n const scorePerSuccessor = currentScore / successors.length;\n\n for (const successorId of successors) {\n const successorKey = String(successorId);\n scores[successorKey] = (scores[successorKey] ?? 0) + scorePerSuccessor;\n\n if (hasMembership(visited, successorId)) {\n continue;\n }\n\n visited[successorKey] = true;\n queue.push(successorId);\n }\n }\n\n return scores;\n }\n\n #reflectionPropagation(input: {\n allChunkIds: readonly number[];\n forwardScores: Readonly<Record<string, number>>;\n getSuccessorChunkIds: (chunkId: number) => readonly number[];\n }): Record<string, number> {\n const reverseSuccessorIdsByChunkId = createEmptyRecord<number[]>();\n\n for (const chunkId of input.allChunkIds) {\n for (const successorId of input.getSuccessorChunkIds(chunkId)) {\n const successorKey = String(successorId);\n const reverseSuccessors =\n reverseSuccessorIdsByChunkId[successorKey] ?? [];\n\n reverseSuccessorIdsByChunkId[successorKey] = [\n ...reverseSuccessors,\n chunkId,\n ];\n }\n }\n\n const reflectionScores = { ...input.forwardScores };\n const queue = Object.keys(input.forwardScores).map(Number);\n const visited = createMembershipRecord(queue);\n let queueOffset = 0;\n\n while (queueOffset < queue.length) {\n const currentId = queue[queueOffset];\n queueOffset += 1;\n\n if (currentId === undefined) {\n continue;\n }\n\n const currentScore = reflectionScores[String(currentId)];\n\n if (currentScore === undefined) {\n continue;\n }\n\n const successors = [\n ...(reverseSuccessorIdsByChunkId[String(currentId)] ?? []),\n ].sort(compareNumber);\n\n if (successors.length === 0) {\n continue;\n }\n\n const scorePerSuccessor = currentScore / successors.length;\n\n for (const successorId of successors) {\n const successorKey = String(successorId);\n\n reflectionScores[successorKey] =\n (reflectionScores[successorKey] ?? 0) + scorePerSuccessor;\n\n if (hasMembership(visited, successorId)) {\n continue;\n }\n\n visited[successorKey] = true;\n queue.push(successorId);\n }\n }\n\n return reflectionScores;\n }\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction createMembershipRecord(\n values: readonly number[],\n): Record<string, true> {\n const record = createEmptyRecord<true>();\n\n for (const value of values) {\n record[String(value)] = true;\n }\n\n return record;\n}\n\nfunction hasMembership(\n record: Readonly<Record<string, true>>,\n value: number,\n): boolean {\n return Object.hasOwn(record, String(value));\n}\n\nfunction countOwnKeys(record: Readonly<Record<string, unknown>>): number {\n return Object.keys(record).length;\n}\n\nfunction createEmptyRecord<TValue>(): Record<string, TValue> {\n return Object.create(null) as Record<string, TValue>;\n}\n","import type { CognitiveChunk } from \"../chunk-batch/types.js\";\n\nexport class WorkingMemory {\n readonly #capacity: number;\n readonly #currentFragmentChunks: CognitiveChunk[] = [];\n readonly #previousFragmentChunks: CognitiveChunk[] = [];\n readonly #extraChunks: CognitiveChunk[] = [];\n #generation = 0;\n\n public constructor(capacity: number) {\n this.#capacity = capacity;\n }\n\n public get capacity(): number {\n return this.#capacity;\n }\n\n public get generation(): number {\n return this.#generation;\n }\n\n public addChunks(chunks: readonly CognitiveChunk[]): void {\n this.#currentFragmentChunks.push(...chunks);\n }\n\n public setRetainedChunks(input: {\n readonly extraChunks: readonly CognitiveChunk[];\n readonly previousFragmentChunks: readonly CognitiveChunk[];\n }): void {\n this.#previousFragmentChunks.splice(\n 0,\n this.#previousFragmentChunks.length,\n ...input.previousFragmentChunks,\n );\n this.#extraChunks.splice(0, this.#extraChunks.length, ...input.extraChunks);\n }\n\n public finalizeFragment(): CognitiveChunk[] {\n const finishedChunks = [...this.#currentFragmentChunks];\n\n this.#currentFragmentChunks.splice(0, this.#currentFragmentChunks.length);\n this.#generation += 1;\n\n return finishedChunks;\n }\n\n public getChunks(): CognitiveChunk[] {\n return [\n ...this.#currentFragmentChunks,\n ...this.#previousFragmentChunks,\n ...this.#extraChunks,\n ];\n }\n\n public getAllChunksForSaving(): CognitiveChunk[] {\n return [...this.#currentFragmentChunks];\n }\n\n public getChunksForPrompt(includeCurrentFragment = true): CognitiveChunk[] {\n return includeCurrentFragment\n ? this.getChunks()\n : [...this.#previousFragmentChunks, ...this.#extraChunks];\n }\n\n public formatForPrompt(includeCurrentFragment = true): string {\n const chunks = this.getChunksForPrompt(includeCurrentFragment);\n\n if (chunks.length === 0) {\n return \"(empty)\";\n }\n\n return chunks\n .map((chunk) => `${chunk.id}. [${chunk.label}] - ${chunk.content}`)\n .join(\"\\n\");\n }\n\n public clear(): void {\n this.#currentFragmentChunks.splice(0, this.#currentFragmentChunks.length);\n this.#previousFragmentChunks.splice(0, this.#previousFragmentChunks.length);\n this.#extraChunks.splice(0, this.#extraChunks.length);\n }\n}\n","import { WaveReflection } from \"./wave-reflection.js\";\nimport { WorkingMemory } from \"./working-memory.js\";\nimport type {\n ChunkBatch,\n ChunkImportanceAnnotation,\n CognitiveChunk,\n} from \"../chunk-batch/types.js\";\n\nexport interface ChunkBatchContext {\n readonly visibleChunkIds: readonly number[];\n readonly workingMemoryPrompt: string;\n}\n\nexport interface ChunkGraphEdge {\n readonly fromId: number;\n readonly strength?: string;\n readonly toId: number;\n}\n\nexport interface ChunkGraphDelta {\n readonly chunks: CognitiveChunk[];\n readonly edges: readonly ChunkGraphEdge[];\n readonly importanceAnnotations?: readonly ChunkImportanceAnnotation[];\n}\n\nexport class Attention {\n readonly #idGenerator: () => Promise<number>;\n readonly #waveReflection: WaveReflection;\n readonly #workingMemory: WorkingMemory;\n\n public constructor(\n capacity: number,\n generationDecayFactor: number,\n idGenerator: () => Promise<number>,\n ) {\n this.#idGenerator = idGenerator;\n this.#waveReflection = new WaveReflection(generationDecayFactor);\n this.#workingMemory = new WorkingMemory(capacity);\n }\n\n public get capacity(): number {\n return this.#workingMemory.capacity;\n }\n\n public createChunkBatchContext(input?: {\n includeCurrentFragment?: boolean;\n }): ChunkBatchContext {\n const chunks = this.#workingMemory.getChunksForPrompt(\n input?.includeCurrentFragment ?? true,\n );\n\n return {\n visibleChunkIds: chunks.map((chunk) => chunk.id),\n workingMemoryPrompt: this.#workingMemory.formatForPrompt(\n input?.includeCurrentFragment ?? true,\n ),\n };\n }\n\n public async acceptChunkBatch(\n chunkBatch: ChunkBatch,\n ): Promise<ChunkGraphDelta> {\n const delta = await assembleChunkBatch({\n chunkBatch,\n generation: this.#workingMemory.generation,\n idGenerator: this.#idGenerator,\n visibleChunks: this.#workingMemory.getChunks(),\n });\n\n this.#workingMemory.addChunks(delta.chunks);\n\n return delta;\n }\n\n public completeFragment(input: {\n allChunks: readonly CognitiveChunk[];\n getSuccessorChunkIds: (chunkId: number) => readonly number[];\n }): void {\n const previousFragmentChunks = this.#workingMemory.getAllChunksForSaving();\n const latestChunkIds = previousFragmentChunks.map((chunk) => chunk.id);\n const extraChunks = this.#waveReflection.selectTopChunks({\n allChunks: input.allChunks,\n capacity: this.#workingMemory.capacity,\n getSuccessorChunkIds: input.getSuccessorChunkIds,\n latestChunkIds,\n });\n\n this.#workingMemory.setRetainedChunks({\n extraChunks,\n previousFragmentChunks,\n });\n this.#workingMemory.finalizeFragment();\n }\n\n public clear(): void {\n this.#workingMemory.clear();\n }\n}\n\nasync function assembleChunkBatch(input: {\n chunkBatch: ChunkBatch;\n generation: number;\n idGenerator: () => Promise<number>;\n visibleChunks: readonly CognitiveChunk[];\n}): Promise<ChunkGraphDelta> {\n const tempIdRecord: Record<string, CognitiveChunk> = Object.create(\n null,\n ) as Record<string, CognitiveChunk>;\n\n for (const [index, chunk] of input.chunkBatch.chunks.entries()) {\n chunk.id = await input.idGenerator();\n chunk.generation = input.generation;\n\n const tempId = input.chunkBatch.tempIds[index];\n\n if (tempId === undefined || tempId === \"\") {\n continue;\n }\n\n tempIdRecord[tempId] = chunk;\n }\n\n const visibleChunks = [...input.visibleChunks, ...input.chunkBatch.chunks];\n const edges: ChunkGraphEdge[] = [];\n\n for (const link of input.chunkBatch.links) {\n const fromChunk = resolveChunkReference(\n link.from,\n tempIdRecord,\n visibleChunks,\n );\n const toChunk = resolveChunkReference(link.to, tempIdRecord, visibleChunks);\n\n if (fromChunk === undefined || toChunk === undefined) {\n continue;\n }\n\n const [edgeFromId, edgeToId] =\n fromChunk.id > toChunk.id\n ? ([fromChunk.id, toChunk.id] as const)\n : ([toChunk.id, fromChunk.id] as const);\n\n attachLink(visibleChunks, edgeToId, edgeFromId);\n if (link.strength === undefined) {\n edges.push({\n fromId: edgeFromId,\n toId: edgeToId,\n });\n continue;\n }\n\n edges.push({\n fromId: edgeFromId,\n strength: link.strength,\n toId: edgeToId,\n });\n }\n\n return {\n chunks: input.chunkBatch.chunks,\n edges,\n ...(input.chunkBatch.importanceAnnotations === undefined\n ? {}\n : {\n importanceAnnotations: input.chunkBatch.importanceAnnotations,\n }),\n };\n}\n\nfunction resolveChunkReference(\n reference: number | string,\n tempIdRecord: Readonly<Record<string, CognitiveChunk>>,\n visibleChunks: readonly CognitiveChunk[],\n): CognitiveChunk | undefined {\n if (typeof reference === \"string\") {\n return tempIdRecord[reference];\n }\n\n return visibleChunks.find((chunk) => chunk.id === reference);\n}\n\nfunction attachLink(\n visibleChunks: readonly CognitiveChunk[],\n targetChunkId: number,\n sourceChunkId: number,\n): void {\n for (const chunk of visibleChunks) {\n if (chunk.id !== targetChunkId) {\n continue;\n }\n\n if (!chunk.links.includes(sourceChunkId)) {\n chunk.links.push(sourceChunkId);\n }\n\n return;\n }\n}\n","const HYPHEN_FLAGS = new Set([\"‐\", \"‑\", \"‒\", \"–\", \"—\", \"―\"]);\nconst SENTENCE_SEGMENTER = new Intl.Segmenter(undefined, {\n granularity: \"sentence\",\n});\n\nexport function splitTextIntoSentences(text: string): string[] {\n const stripped = text.trim();\n\n if (stripped === \"\") {\n return [];\n }\n\n return Array.from(SENTENCE_SEGMENTER.segment(stripped))\n .map((segment) => segment.segment.trim())\n .filter((segment) => segment !== \"\");\n}\n\nexport function normalizeText(text: string): string {\n const collapsed = text.replace(/\\s+/gu, \" \").trim();\n const chars = processSpacesAndHyphens(collapsed);\n let normalized = \"\";\n\n for (const char of chars) {\n if (isPunctuation(char)) {\n continue;\n }\n\n if (!isLatinLetter(char)) {\n normalized += char;\n continue;\n }\n\n normalized += char.toLowerCase().normalize(\"NFD\").replace(/\\p{M}/gu, \"\");\n }\n\n return normalized;\n}\n\nfunction processSpacesAndHyphens(text: string): string[] {\n const chars: string[] = [];\n let index = 0;\n\n while (index < text.length) {\n const char = text[index];\n\n if (char === undefined) {\n break;\n }\n\n if (char !== \" \") {\n chars.push(char);\n index += 1;\n continue;\n }\n\n if (\n chars.length >= 2 &&\n HYPHEN_FLAGS.has(chars[chars.length - 1] ?? \"\") &&\n isLatinLetter(chars[chars.length - 2] ?? \"\") &&\n index < text.length - 1 &&\n isLatinLetter(text[index + 1] ?? \"\")\n ) {\n chars.pop();\n index += 1;\n continue;\n }\n\n const previousChar = chars[chars.length - 1] ?? \"\";\n const nextChar = text[index + 1] ?? \"\";\n const keepSpace =\n previousChar !== \"\" &&\n nextChar !== \"\" &&\n isLatinLetter(previousChar) &&\n isLatinLetter(nextChar);\n\n if (keepSpace) {\n chars.push(char);\n }\n\n index += 1;\n }\n\n return chars;\n}\n\nfunction isLatinLetter(char: string): boolean {\n return char !== \"\" && /\\p{Script=Latin}/u.test(char) && /\\p{L}/u.test(char);\n}\n\nfunction isPunctuation(char: string): boolean {\n return /[\\p{P}\\p{S}]/u.test(char);\n}\n","import type { SentenceId } from \"../document/index.js\";\nimport type {\n EvidenceResolutionFailure,\n EvidenceResolutionResult,\n RankedSentenceCandidate,\n} from \"./anchor-types.js\";\nimport { normalizeText, splitTextIntoSentences } from \"./text.js\";\n\nconst MIN_AUTO_RESOLVE_GAP = 0.07;\nconst MIN_AUTO_RESOLVE_SCORE = 0.9;\nconst MIN_BOUNDARY_BONUS_LENGTH = 12;\nconst MIN_CANDIDATE_SCORE = 0.55;\nconst VERY_HIGH_CONFIDENCE_SCORE = 0.97;\nconst MAX_BOUNDARY_BONUS = 0.08;\nconst MAX_CANDIDATE_DISPLAY = 3;\n\ninterface AnchorSpec {\n readonly mode: \"full\" | \"head_tail\";\n readonly text?: string;\n readonly head?: string;\n readonly tail?: string;\n}\n\ninterface TextMatchScore {\n readonly score: number;\n readonly exactRaw: boolean;\n readonly exactNormalized: boolean;\n readonly exactSubstring: boolean;\n readonly matchStart: number;\n readonly matchEnd: number;\n}\n\nexport class EvidenceResolver {\n public resolve(\n evidence: Record<string, unknown>,\n candidateSentenceIds: readonly SentenceId[],\n candidateTexts: readonly string[],\n ): readonly [\n result: EvidenceResolutionResult | undefined,\n failure: EvidenceResolutionFailure | undefined,\n ] {\n const startValue = evidence.start_anchor ?? evidence.start;\n const endValue = evidence.end_anchor ?? evidence.end;\n\n const [startAnchor, startFailure] = this.parseAnchor(\n startValue,\n \"start_anchor\",\n );\n\n if (startFailure !== undefined) {\n return [undefined, startFailure];\n }\n\n let endAnchor: AnchorSpec | undefined;\n\n if (endValue !== undefined) {\n const [parsedEndAnchor, endFailure] = this.parseAnchor(\n endValue,\n \"end_anchor\",\n );\n\n if (endFailure !== undefined) {\n return [undefined, endFailure];\n }\n\n endAnchor = parsedEndAnchor;\n }\n\n const [startCandidate, startStrategy, startResolveFailure] =\n this.resolveAnchor({\n anchor: startAnchor,\n candidateSentenceIds,\n candidateTexts,\n label: \"start_anchor\",\n });\n\n if (startResolveFailure !== undefined) {\n return [undefined, startResolveFailure];\n }\n\n if (startCandidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n fieldName: \"start_anchor\",\n message: \"start_anchor could not be matched: no candidate selected\",\n },\n ];\n }\n\n if (endAnchor === undefined) {\n return [\n {\n confidence: startCandidate.score,\n sentenceIds: [startCandidate.sentenceId],\n strategy: startStrategy,\n },\n undefined,\n ];\n }\n\n const [endCandidate, endStrategy, endResolveFailure] = this.resolveAnchor({\n anchor: endAnchor,\n candidateSentenceIds,\n candidateTexts,\n label: \"end_anchor\",\n minIndex: startCandidate.index,\n });\n\n if (endResolveFailure !== undefined) {\n return [undefined, endResolveFailure];\n }\n\n if (endCandidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n fieldName: \"end_anchor\",\n message: \"end_anchor could not be matched: no candidate selected\",\n },\n ];\n }\n\n if (endCandidate.index < startCandidate.index) {\n return [\n undefined,\n {\n candidates: [],\n code: \"invalid_range\",\n fieldName: \"end_anchor\",\n message:\n \"Invalid evidence range: end_anchor resolved before start_anchor \" +\n `(${startCandidate.occurrenceId} -> ${endCandidate.occurrenceId}).`,\n },\n ];\n }\n\n return [\n {\n confidence: Math.min(startCandidate.score, endCandidate.score),\n sentenceIds: candidateSentenceIds.slice(\n startCandidate.index,\n endCandidate.index + 1,\n ),\n strategy: `${startStrategy}+${endStrategy}`,\n },\n undefined,\n ];\n }\n\n public resolveWithOverrides(input: {\n evidence: Record<string, unknown>;\n candidateSentenceIds: readonly SentenceId[];\n candidateTexts: readonly string[];\n overrides: Partial<\n Readonly<Record<\"start_anchor\" | \"end_anchor\", RankedSentenceCandidate>>\n >;\n }): readonly [\n result: EvidenceResolutionResult | undefined,\n failure: EvidenceResolutionFailure | undefined,\n ] {\n const startValue = input.evidence.start_anchor ?? input.evidence.start;\n const endValue = input.evidence.end_anchor ?? input.evidence.end;\n\n const [startAnchor, startFailure] = this.parseAnchor(\n startValue,\n \"start_anchor\",\n );\n\n if (startFailure !== undefined) {\n return [undefined, startFailure];\n }\n\n const startCandidateOverride = input.overrides.start_anchor;\n const [startCandidate, startStrategy, startResolveFailure] =\n startCandidateOverride === undefined\n ? this.resolveAnchor({\n anchor: startAnchor,\n candidateSentenceIds: input.candidateSentenceIds,\n candidateTexts: input.candidateTexts,\n label: \"start_anchor\",\n })\n : [startCandidateOverride, \"choice_final\", undefined];\n\n if (startResolveFailure !== undefined) {\n return [undefined, startResolveFailure];\n }\n\n if (startCandidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n fieldName: \"start_anchor\",\n message: \"start_anchor could not be matched: no candidate selected\",\n },\n ];\n }\n\n if (endValue === undefined) {\n return [\n {\n confidence: startCandidate.score,\n sentenceIds: [startCandidate.sentenceId],\n strategy: startStrategy,\n },\n undefined,\n ];\n }\n\n const [endAnchor, endFailure] = this.parseAnchor(endValue, \"end_anchor\");\n\n if (endFailure !== undefined) {\n return [undefined, endFailure];\n }\n\n const endCandidateOverride = input.overrides.end_anchor;\n const [endCandidate, endStrategy, endResolveFailure] =\n endCandidateOverride === undefined\n ? this.resolveAnchor({\n anchor: endAnchor,\n candidateSentenceIds: input.candidateSentenceIds,\n candidateTexts: input.candidateTexts,\n label: \"end_anchor\",\n minIndex: startCandidate.index,\n })\n : [endCandidateOverride, \"choice_final\", undefined];\n\n if (endResolveFailure !== undefined) {\n return [undefined, endResolveFailure];\n }\n\n if (endCandidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n fieldName: \"end_anchor\",\n message: \"end_anchor could not be matched: no candidate selected\",\n },\n ];\n }\n\n if (endCandidate.index < startCandidate.index) {\n return [\n undefined,\n {\n candidates: [],\n code: \"invalid_range\",\n fieldName: \"end_anchor\",\n message:\n \"Invalid evidence range: end_anchor resolved before start_anchor \" +\n `(${startCandidate.occurrenceId} -> ${endCandidate.occurrenceId}).`,\n },\n ];\n }\n\n return [\n {\n confidence: Math.min(startCandidate.score, endCandidate.score),\n sentenceIds: input.candidateSentenceIds.slice(\n startCandidate.index,\n endCandidate.index + 1,\n ),\n strategy: `${startStrategy}+${endStrategy}`,\n },\n undefined,\n ];\n }\n\n public parseAnchor(\n value: unknown,\n fieldName: string,\n ): readonly [\n anchor: AnchorSpec | undefined,\n failure: EvidenceResolutionFailure | undefined,\n ] {\n const [anchor, error] = this.#parseAnchor(value);\n\n if (error !== undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"invalid_anchor\",\n fieldName,\n message: `Invalid evidence.${fieldName}: ${error}`,\n },\n ];\n }\n\n return [this.#normalizeAnchor(anchor, fieldName), undefined];\n }\n\n public resolveAnchor(input: {\n anchor: AnchorSpec | undefined;\n candidateSentenceIds: readonly SentenceId[];\n candidateTexts: readonly string[];\n label: string;\n minIndex?: number;\n }): readonly [\n candidate: RankedSentenceCandidate | undefined,\n strategy: string,\n failure: EvidenceResolutionFailure | undefined,\n ] {\n if (input.anchor === undefined) {\n return [\n undefined,\n \"invalid\",\n {\n candidates: [],\n code: \"invalid\",\n fieldName: input.label,\n message: `${input.label} is missing`,\n },\n ];\n }\n\n let candidates = this.rankAnchor(\n input.anchor,\n input.candidateSentenceIds,\n input.candidateTexts,\n input.label,\n );\n\n const minIndex = input.minIndex;\n\n if (minIndex !== undefined) {\n candidates = candidates.filter(\n (candidate) => candidate.index >= minIndex,\n );\n }\n\n if (candidates.length === 0) {\n return [\n undefined,\n \"none\",\n {\n candidates: [],\n code: \"none\",\n fieldName: input.label,\n message: `${input.label} could not be matched: no candidates available`,\n },\n ];\n }\n\n const exactRaw = candidates.filter((candidate) => candidate.exactRaw);\n\n if (exactRaw.length === 1) {\n return [exactRaw[0], \"exact_raw\", undefined];\n }\n\n if (exactRaw.length > 1) {\n return [\n undefined,\n \"ambiguous_exact_raw\",\n this.#buildAmbiguousFailure(\n input.label,\n \"ambiguous_exact_raw\",\n exactRaw,\n ),\n ];\n }\n\n const exactSubstring = candidates.filter(\n (candidate) => candidate.exactSubstring,\n );\n\n if (exactSubstring.length === 1 && this.#anchorLength(input.anchor) >= 8) {\n return [exactSubstring[0], \"exact_substring\", undefined];\n }\n\n if (exactSubstring.length > 1 && this.#anchorLength(input.anchor) >= 8) {\n const selected = this.#tryAutoSelectScoredDuplicates(exactSubstring);\n\n if (selected !== undefined) {\n return [selected, \"exact_substring_scored\", undefined];\n }\n\n return [\n undefined,\n \"ambiguous_exact_substring\",\n this.#buildAmbiguousFailure(\n input.label,\n \"ambiguous_exact_substring\",\n exactSubstring,\n ),\n ];\n }\n\n const exactNormalized = candidates.filter(\n (candidate) => candidate.exactNormalized,\n );\n\n if (exactNormalized.length === 1) {\n return [exactNormalized[0], \"exact_normalized\", undefined];\n }\n\n if (exactNormalized.length > 1) {\n const selected = this.#tryAutoSelectScoredDuplicates(exactNormalized);\n\n if (selected !== undefined) {\n return [selected, \"exact_normalized_scored\", undefined];\n }\n\n return [\n undefined,\n \"ambiguous_exact_normalized\",\n this.#buildAmbiguousFailure(\n input.label,\n \"ambiguous_exact_normalized\",\n exactNormalized,\n ),\n ];\n }\n\n const topCandidate = candidates[0];\n\n if (topCandidate === undefined) {\n return [\n undefined,\n \"none\",\n {\n candidates: [],\n code: \"none\",\n fieldName: input.label,\n message: `${input.label} could not be matched: no candidates available`,\n },\n ];\n }\n\n const secondScore = candidates[1]?.score ?? 0;\n const gap = topCandidate.score - secondScore;\n\n if (topCandidate.score < MIN_CANDIDATE_SCORE) {\n return [\n undefined,\n \"low_confidence\",\n {\n candidates: candidates.slice(0, MAX_CANDIDATE_DISPLAY),\n code: \"low_confidence\",\n fieldName: input.label,\n message:\n `${input.label} could not be matched confidently. ` +\n `Best candidate score=${topCandidate.score.toFixed(3)}.`,\n },\n ];\n }\n\n if (candidates.length === 1) {\n return [topCandidate, \"auto_top1_single_candidate\", undefined];\n }\n\n if (topCandidate.score >= VERY_HIGH_CONFIDENCE_SCORE) {\n return [topCandidate, \"auto_top1_very_high\", undefined];\n }\n\n if (\n topCandidate.score >= MIN_AUTO_RESOLVE_SCORE &&\n gap >= MIN_AUTO_RESOLVE_GAP\n ) {\n return [topCandidate, \"auto_top1\", undefined];\n }\n\n return [\n undefined,\n \"ambiguous_ranked\",\n this.#buildAmbiguousFailure(input.label, \"ambiguous_ranked\", candidates),\n ];\n }\n\n public rankAnchor(\n anchor: AnchorSpec,\n candidateSentenceIds: readonly SentenceId[],\n candidateTexts: readonly string[],\n label: string,\n ): RankedSentenceCandidate[] {\n const ranked: RankedSentenceCandidate[] = [];\n\n for (const [index, sentenceId] of candidateSentenceIds.entries()) {\n const text = candidateTexts[index] ?? \"\";\n const prevText = index > 0 ? (candidateTexts[index - 1] ?? \"\") : \"\";\n const nextText =\n index < candidateTexts.length - 1\n ? (candidateTexts[index + 1] ?? \"\")\n : \"\";\n const match = this.#scoreAnchor(anchor, text, label);\n\n ranked.push({\n exactNormalized: match.exactNormalized,\n exactRaw: match.exactRaw,\n exactSubstring: match.exactSubstring,\n index,\n nextText,\n occurrenceId: `S${index + 1}`,\n prevText,\n score: match.score,\n sentenceId,\n text,\n });\n }\n\n ranked.sort((left, right) => {\n if (left.score !== right.score) {\n return right.score - left.score;\n }\n\n if (left.exactRaw !== right.exactRaw) {\n return left.exactRaw ? -1 : 1;\n }\n\n if (left.exactSubstring !== right.exactSubstring) {\n return left.exactSubstring ? -1 : 1;\n }\n\n if (left.exactNormalized !== right.exactNormalized) {\n return left.exactNormalized ? -1 : 1;\n }\n\n return left.index - right.index;\n });\n\n return ranked;\n }\n\n #scoreAnchor(\n anchor: AnchorSpec,\n candidateText: string,\n label: string,\n ): TextMatchScore {\n if (anchor.mode === \"head_tail\") {\n return this.#scoreHeadTail(anchor, candidateText);\n }\n\n return this.#scoreTextQuery(anchor.text ?? \"\", candidateText, label);\n }\n\n #scoreHeadTail(anchor: AnchorSpec, candidateText: string): TextMatchScore {\n const head = anchor.head ?? \"\";\n const tail = anchor.tail ?? \"\";\n const candidateNormalized = normalizeText(candidateText);\n const headNormalized = normalizeText(head);\n const tailNormalized = normalizeText(tail);\n\n if (headNormalized === \"\" || tailNormalized === \"\") {\n return createEmptyTextMatchScore();\n }\n\n const headPosition = candidateNormalized.indexOf(headNormalized);\n const tailPosition = candidateNormalized.lastIndexOf(tailNormalized);\n\n if (\n headPosition !== -1 &&\n tailPosition !== -1 &&\n headPosition <= tailPosition\n ) {\n const coveredChars = headNormalized.length + tailNormalized.length;\n const coverage = Math.min(\n 1,\n coveredChars / Math.max(1, candidateNormalized.length),\n );\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: true,\n matchEnd: tailPosition + tailNormalized.length,\n matchStart: headPosition,\n score: 0.82 + 0.18 * coverage,\n };\n }\n\n const headMatch = this.#scoreTextQuery(head, candidateText, \"start_anchor\");\n const tailMatch = this.#scoreTextQuery(tail, candidateText, \"end_anchor\");\n const ordered = headMatch.matchStart <= tailMatch.matchStart ? 1 : 0.85;\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: Math.max(headMatch.matchEnd, tailMatch.matchEnd),\n matchStart: Math.min(headMatch.matchStart, tailMatch.matchStart),\n score: ((headMatch.score + tailMatch.score) / 2) * ordered,\n };\n }\n\n #scoreTextQuery(\n queryText: string,\n candidateText: string,\n label: string,\n ): TextMatchScore {\n const queryRaw = queryText.trim();\n const candidateRaw = candidateText.trim();\n const queryNormalized = normalizeText(queryRaw);\n const candidateNormalized = normalizeText(candidateRaw);\n\n if (queryNormalized === \"\" || candidateNormalized === \"\") {\n return createEmptyTextMatchScore();\n }\n\n if (queryRaw === candidateRaw) {\n return {\n exactNormalized: true,\n exactRaw: true,\n exactSubstring: true,\n matchEnd: candidateNormalized.length,\n matchStart: 0,\n score: 1,\n };\n }\n\n if (queryNormalized === candidateNormalized) {\n return {\n exactNormalized: true,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: candidateNormalized.length,\n matchStart: 0,\n score: 0.995,\n };\n }\n\n if (candidateNormalized.includes(queryNormalized)) {\n const matchStart = candidateNormalized.indexOf(queryNormalized);\n const coverage =\n queryNormalized.length / Math.max(1, candidateNormalized.length);\n const boundaryBonus = this.#boundaryBonus({\n candidateNormalized,\n label,\n matchEnd: matchStart + queryNormalized.length,\n matchStart,\n queryNormalized,\n });\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: true,\n matchEnd: matchStart + queryNormalized.length,\n matchStart,\n score: Math.min(1, 0.75 + 0.25 * coverage + boundaryBonus),\n };\n }\n\n if (queryNormalized.includes(candidateNormalized)) {\n const coverage =\n candidateNormalized.length / Math.max(1, queryNormalized.length);\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: candidateNormalized.length,\n matchStart: 0,\n score: Math.min(0.92, 0.78 + 0.14 * coverage),\n };\n }\n\n let bestScore = 0;\n let bestStart = -1;\n let bestEnd = -1;\n const maxWindow = Math.min(\n candidateNormalized.length,\n Math.max(\n queryNormalized.length,\n Math.floor(queryNormalized.length * 1.2),\n ),\n );\n const minWindow = Math.min(\n maxWindow,\n Math.max(1, Math.floor(queryNormalized.length * 0.8)),\n );\n const queryChars = new Set(queryNormalized);\n\n for (let windowSize = minWindow; windowSize <= maxWindow; windowSize += 1) {\n for (\n let start = 0;\n start <= candidateNormalized.length - windowSize;\n start += 1\n ) {\n const window = candidateNormalized.slice(start, start + windowSize);\n const windowChars = new Set(window);\n const unionSize = new Set([...queryChars, ...windowChars]).size;\n const overlapSize = [...queryChars].filter((char) =>\n windowChars.has(char),\n ).length;\n const jaccard = unionSize === 0 ? 0 : overlapSize / unionSize;\n\n if (jaccard < 0.35) {\n continue;\n }\n\n const totalScore =\n 0.35 * charNgramScore(queryNormalized, window) +\n 0.35 * sequenceSimilarity(queryNormalized, window) +\n 0.2 * levenshteinSimilarity(queryNormalized, window) +\n 0.1 * lengthPenalty(queryNormalized, window) +\n this.#boundaryBonus({\n candidateNormalized,\n label,\n matchEnd: start + windowSize,\n matchStart: start,\n queryNormalized,\n });\n\n if (totalScore > bestScore) {\n bestScore = Math.min(1, totalScore);\n bestStart = start;\n bestEnd = start + windowSize;\n }\n }\n }\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: bestEnd,\n matchStart: bestStart,\n score: bestScore,\n };\n }\n\n #parseAnchor(\n value: unknown,\n ): readonly [anchor: AnchorSpec | undefined, error: string | undefined] {\n if (value === undefined || value === null) {\n return [undefined, \"anchor is required\"];\n }\n\n if (typeof value === \"string\") {\n const stripped = value.trim();\n\n if (stripped === \"\") {\n return [undefined, \"anchor string is empty\"];\n }\n\n if (stripped.includes(\"...\")) {\n const parts = stripped\n .split(\"...\")\n .map((part) => part.trim())\n .filter((part) => part !== \"\");\n\n if (parts.length >= 2) {\n const head = parts[0];\n const tail = parts[parts.length - 1];\n\n if (head === undefined || tail === undefined) {\n return [undefined, \"anchor string is empty\"];\n }\n\n return [\n {\n head,\n mode: \"head_tail\",\n tail,\n },\n undefined,\n ];\n }\n }\n\n return [{ mode: \"full\", text: stripped }, undefined];\n }\n\n if (typeof value !== \"object\") {\n return [undefined, `expected string or object, got ${typeof value}`];\n }\n\n const rawValue = value as Record<string, unknown>;\n const mode =\n rawValue.mode === \"head_tail\" || rawValue.head !== undefined\n ? \"head_tail\"\n : \"full\";\n\n if (mode === \"full\") {\n const text =\n typeof rawValue.text === \"string\" ? rawValue.text.trim() : \"\";\n\n if (text === \"\") {\n return [undefined, \"full anchor requires non-empty 'text'\"];\n }\n\n return [{ mode: \"full\", text }, undefined];\n }\n\n const head = typeof rawValue.head === \"string\" ? rawValue.head.trim() : \"\";\n const tail = typeof rawValue.tail === \"string\" ? rawValue.tail.trim() : \"\";\n\n if (head !== \"\" && tail === \"\") {\n return [{ mode: \"full\", text: head }, undefined];\n }\n\n if (head === \"\" && tail !== \"\") {\n return [{ mode: \"full\", text: tail }, undefined];\n }\n\n if (head === \"\" || tail === \"\") {\n return [\n undefined,\n \"head_tail anchor requires non-empty 'head' and 'tail'\",\n ];\n }\n\n return [{ head, mode: \"head_tail\", tail }, undefined];\n }\n\n #normalizeAnchor(\n anchor: AnchorSpec | undefined,\n fieldName: string,\n ): AnchorSpec | undefined {\n if (anchor === undefined) {\n return undefined;\n }\n\n if (anchor.mode === \"head_tail\") {\n const boundaryText =\n fieldName === \"end_anchor\"\n ? this.#selectBoundarySentence(anchor.tail ?? \"\", \"end_anchor\")\n : this.#selectBoundarySentence(anchor.head ?? \"\", \"start_anchor\");\n\n return boundaryText === \"\"\n ? undefined\n : {\n mode: \"full\",\n text: boundaryText,\n };\n }\n\n const normalizedText = this.#selectBoundarySentence(\n anchor.text ?? \"\",\n fieldName,\n );\n\n return {\n mode: \"full\",\n text: normalizedText,\n };\n }\n\n #selectBoundarySentence(text: string, fieldName: string): string {\n const sentences = splitTextIntoSentences(text);\n\n if (sentences.length <= 1) {\n return text.trim();\n }\n\n return fieldName === \"end_anchor\"\n ? (sentences[sentences.length - 1] ?? text.trim())\n : (sentences[0] ?? text.trim());\n }\n\n #boundaryBonus(input: {\n label: string;\n queryNormalized: string;\n candidateNormalized: string;\n matchStart: number;\n matchEnd: number;\n }): number {\n if (input.matchStart < 0 || input.matchEnd < 0) {\n return 0;\n }\n\n if (input.queryNormalized.length < MIN_BOUNDARY_BONUS_LENGTH) {\n return 0;\n }\n\n if (input.label === \"start_anchor\" && input.matchStart !== 0) {\n return 0;\n }\n\n if (\n input.label === \"end_anchor\" &&\n input.matchEnd !== input.candidateNormalized.length\n ) {\n return 0;\n }\n\n const coverage =\n input.queryNormalized.length /\n Math.max(1, input.candidateNormalized.length);\n\n return MAX_BOUNDARY_BONUS * (0.5 + 0.5 * Math.min(1, coverage));\n }\n\n #tryAutoSelectScoredDuplicates(\n candidates: readonly RankedSentenceCandidate[],\n ): RankedSentenceCandidate | undefined {\n if (candidates.length === 0) {\n return undefined;\n }\n\n if (candidates.length === 1) {\n return candidates[0];\n }\n\n const topCandidate = candidates[0];\n\n if (topCandidate === undefined) {\n return undefined;\n }\n\n const secondScore = candidates[1]?.score ?? 0;\n\n if (\n topCandidate.score >= MIN_AUTO_RESOLVE_SCORE &&\n topCandidate.score - secondScore >= MIN_AUTO_RESOLVE_GAP\n ) {\n return topCandidate;\n }\n\n return undefined;\n }\n\n #buildAmbiguousFailure(\n label: string,\n code: string,\n candidates: readonly RankedSentenceCandidate[],\n ): EvidenceResolutionFailure {\n const topCandidates = candidates.slice(0, MAX_CANDIDATE_DISPLAY);\n const formatted = topCandidates\n .map((candidate) => formatCandidate(candidate))\n .join(\"\\n\");\n\n return {\n candidates: topCandidates,\n code,\n fieldName: label,\n message:\n `${label} is ambiguous. Top candidates:\\n${formatted}\\n` +\n \"Please provide a more specific anchor or surrounding context.\",\n };\n }\n\n #anchorLength(anchor: AnchorSpec): number {\n if (anchor.mode === \"head_tail\") {\n return (\n normalizeText(anchor.head ?? \"\").length +\n normalizeText(anchor.tail ?? \"\").length\n );\n }\n\n return normalizeText(anchor.text ?? \"\").length;\n }\n}\n\nfunction createEmptyTextMatchScore(): TextMatchScore {\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: -1,\n matchStart: -1,\n score: 0,\n };\n}\n\nfunction charNgramScore(left: string, right: string): number {\n const leftBigrams = charNgrams(left, 2);\n const rightBigrams = charNgrams(right, 2);\n const leftTrigrams = charNgrams(left, 3);\n const rightTrigrams = charNgrams(right, 3);\n\n return (\n (diceCoefficient(leftBigrams, rightBigrams) +\n diceCoefficient(leftTrigrams, rightTrigrams)) /\n 2\n );\n}\n\nfunction charNgrams(text: string, size: number): Set<string> {\n if (text.length <= size) {\n return text === \"\" ? new Set() : new Set([text]);\n }\n\n const result = new Set<string>();\n\n for (let index = 0; index <= text.length - size; index += 1) {\n result.add(text.slice(index, index + size));\n }\n\n return result;\n}\n\nfunction diceCoefficient(\n left: ReadonlySet<string>,\n right: ReadonlySet<string>,\n): number {\n if (left.size === 0 || right.size === 0) {\n return 0;\n }\n\n let overlap = 0;\n\n for (const value of left) {\n if (right.has(value)) {\n overlap += 1;\n }\n }\n\n return (2 * overlap) / (left.size + right.size);\n}\n\nfunction levenshteinSimilarity(left: string, right: string): number {\n if (left === right) {\n return 1;\n }\n\n if (left === \"\" || right === \"\") {\n return 0;\n }\n\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let current = new Array<number>(right.length + 1).fill(0);\n\n for (const [leftIndex, leftChar] of [...left].entries()) {\n current[0] = leftIndex + 1;\n\n for (const [rightIndex, rightChar] of [...right].entries()) {\n const insertCost = (current[rightIndex] ?? 0) + 1;\n const deleteCost = (previous[rightIndex + 1] ?? 0) + 1;\n const replaceCost =\n (previous[rightIndex] ?? 0) + (leftChar === rightChar ? 0 : 1);\n\n current[rightIndex + 1] = Math.min(insertCost, deleteCost, replaceCost);\n }\n\n [previous, current] = [current, previous];\n }\n\n const distance =\n previous[right.length] ?? Math.max(left.length, right.length);\n\n return 1 - distance / Math.max(left.length, right.length);\n}\n\nfunction lengthPenalty(left: string, right: string): number {\n return (\n 1 -\n Math.abs(left.length - right.length) / Math.max(left.length, right.length)\n );\n}\n\nfunction sequenceSimilarity(left: string, right: string): number {\n if (left === right) {\n return 1;\n }\n\n const leftLength = left.length;\n const rightLength = right.length;\n\n if (leftLength === 0 || rightLength === 0) {\n return 0;\n }\n\n const matrix = Array.from({ length: leftLength + 1 }, () =>\n new Array<number>(rightLength + 1).fill(0),\n );\n let longest = 0;\n\n for (let leftIndex = 1; leftIndex <= leftLength; leftIndex += 1) {\n for (let rightIndex = 1; rightIndex <= rightLength; rightIndex += 1) {\n if (left[leftIndex - 1] !== right[rightIndex - 1]) {\n continue;\n }\n\n const currentRow = matrix[leftIndex];\n\n if (currentRow === undefined) {\n continue;\n }\n\n currentRow[rightIndex] =\n (matrix[leftIndex - 1]?.[rightIndex - 1] ?? 0) + 1;\n longest = Math.max(longest, currentRow[rightIndex] ?? 0);\n }\n }\n\n return (2 * longest) / (leftLength + rightLength);\n}\n\nfunction formatCandidate(candidate: RankedSentenceCandidate): string {\n return (\n ` - ${candidate.occurrenceId} score=${candidate.score.toFixed(3)}\\n` +\n ` prev: ${truncate(candidate.prevText)}\\n` +\n ` text: ${truncate(candidate.text, 120)}\\n` +\n ` next: ${truncate(candidate.nextText)}`\n );\n}\n\nfunction truncate(text: string, limit = 80): string {\n const stripped = text.replace(/\\s+/gu, \" \").trim();\n\n if (stripped === \"\") {\n return \"(none)\";\n }\n\n if (stripped.length <= limit) {\n return stripped;\n }\n\n return `${stripped.slice(0, limit - 3)}...`;\n}\n","export const EVIDENCE_SELECTION_PROMPT_FRAGMENT = [\n \"Evidence selection:\",\n '- Return evidence as [{\"sentence_id\":\"S1\",\"quote\":\"exact short source quote\"}].',\n \"- evidence.quote is a locator for the source sentence, not a proof excerpt.\",\n \"- Choose the shortest exact quote that still uniquely identifies the intended sentence.\",\n \"- If the quote is too short, it may match multiple sentences; if it is too long, it may include unnecessary text or cross sentence boundaries.\",\n \"- Do not use a whole sentence by default.\",\n \"- evidence is an array. Use multiple items when the claim needs multiple source sentences.\",\n \"- A source sentence means exactly one displayed source line labeled S1, S2, S3, etc.\",\n \"- Each evidence item must point to exactly one source sentence.\",\n \"- Each evidence item must quote one continuous span from exactly one labeled source line, for example only from S2.\",\n \"- Never merge text from two labeled source lines into one evidence quote.\",\n \"- sentence_id must be one of the sentence IDs shown in the source context.\",\n \"- quote must be copied from the untagged original source sentence.\",\n \"- If the source context contains XML-like tags, do not copy the tags into quote; copy only the visible text.\",\n].join(\"\\n\");\n\nexport const EVIDENCE_SELECTION_JSON_SHAPE = [\n {\n quote: \"exact short source quote copied from the selected sentence\",\n sentence_id: \"sentence id from the source context, such as S1\",\n },\n] as const;\n\nexport function formatEvidenceSelectionChoicePrompt(input: {\n readonly evidenceLabel: string;\n readonly generatedEvidence: unknown;\n readonly candidates: readonly {\n readonly nextText: string;\n readonly occurrenceId: string;\n readonly prevText: string;\n readonly score: number;\n readonly text: string;\n }[];\n}): string {\n return [\n `Resolve only this evidence selection: ${input.evidenceLabel}.`,\n \"The generated evidence did not match its sentence_id and quote confidently.\",\n \"Choose exactly one candidate occurrence ID from the list below.\",\n 'Return JSON only: {\"choice\":\"S1\"}',\n \"\",\n \"Generated evidence:\",\n JSON.stringify(input.generatedEvidence, null, 2),\n \"\",\n \"Candidates:\",\n input.candidates.map(formatChoiceCandidate).join(\"\\n\"),\n ].join(\"\\n\");\n}\n\nfunction formatChoiceCandidate(input: {\n readonly nextText: string;\n readonly occurrenceId: string;\n readonly prevText: string;\n readonly score: number;\n readonly text: string;\n}): string {\n return [\n `${input.occurrenceId} score=${input.score.toFixed(3)}`,\n `prev: ${formatChoiceText(input.prevText)}`,\n `text: ${formatChoiceText(input.text)}`,\n `next: ${formatChoiceText(input.nextText)}`,\n ].join(\"\\n\");\n}\n\nfunction formatChoiceText(text: string): string {\n const collapsed = text.replace(/\\s+/gu, \" \").trim();\n\n return collapsed === \"\" ? \"(none)\" : collapsed;\n}\n","const EXACT_SCORE = 1;\nconst NORMALIZED_EXACT_SCORE = 0.995;\nconst SUBSTRING_BASE_SCORE = 0.95;\nconst SUBSTRING_COVERAGE_BONUS = 0.04;\nconst FUZZY_MIN_WINDOW_RATIO = 0.72;\nconst FUZZY_MAX_WINDOW_RATIO = 2.25;\nconst UNORDERED_PENALTY_THRESHOLD = 0.75;\nconst LOW_ORDERED_PENALTY_THRESHOLD = 0.45;\n\nconst CASE_FOLDING_OVERRIDES = new Map<string, string>([\n [\"ß\", \"ss\"],\n [\"ẞ\", \"ss\"],\n [\"ς\", \"σ\"],\n]);\n\nexport type EvidenceQuoteMatchStrategy =\n | \"empty\"\n | \"exact\"\n | \"normalized_exact\"\n | \"normalized_substring\"\n | \"fuzzy_window\";\n\nexport interface EvidenceQuoteScore {\n readonly exactRaw: boolean;\n readonly exactNormalized: boolean;\n readonly exactSubstring: boolean;\n readonly matchEnd: number;\n readonly matchStart: number;\n readonly normalizedQuote: string;\n readonly normalizedSentence: string;\n readonly score: number;\n readonly strategy: EvidenceQuoteMatchStrategy;\n}\n\nexport interface PreparedEvidenceQuote {\n readonly normalizedQuote: string;\n readonly quoteRaw: string;\n}\n\nexport function normalizeEvidenceDisplayText(text: string): string {\n return text\n .replace(/\\p{Default_Ignorable_Code_Point}/gu, \"\")\n .normalize(\"NFKC\")\n .replace(/\\s+/gu, \" \")\n .trim();\n}\n\nexport function normalizeEvidenceText(text: string): string {\n const separated = replaceSeparatorsWithSpace(text);\n const stripped = separated.replace(/\\p{Default_Ignorable_Code_Point}/gu, \"\");\n const compatible = stripped.normalize(\"NFKC\");\n const folded = caseFold(compatible);\n const decomposed = folded.normalize(\"NFD\").replace(/\\p{M}/gu, \"\");\n const reseparated = replaceSeparatorsWithSpace(decomposed);\n\n return reseparated.replace(/ +/gu, \" \").trim();\n}\n\nexport function scoreEvidenceQuote(input: {\n readonly quote: string;\n readonly sentence: string;\n}): EvidenceQuoteScore {\n return scorePreparedEvidenceQuote({\n preparedQuote: prepareEvidenceQuote(input.quote),\n sentence: input.sentence,\n });\n}\n\nexport function prepareEvidenceQuote(quote: string): PreparedEvidenceQuote {\n const quoteRaw = stripMarkup(quote).trim();\n\n return {\n normalizedQuote: normalizeEvidenceText(quoteRaw),\n quoteRaw,\n };\n}\n\nexport function scorePreparedEvidenceQuote(input: {\n readonly preparedQuote: PreparedEvidenceQuote;\n readonly sentence: string;\n}): EvidenceQuoteScore {\n const { normalizedQuote, quoteRaw } = input.preparedQuote;\n const sentenceRaw = stripMarkup(input.sentence).trim();\n const normalizedSentence = normalizeEvidenceText(sentenceRaw);\n\n if (normalizedQuote === \"\" || normalizedSentence === \"\") {\n return createEmptyScore(normalizedQuote, normalizedSentence);\n }\n\n if (quoteRaw === sentenceRaw) {\n return {\n exactNormalized: true,\n exactRaw: true,\n exactSubstring: true,\n matchEnd: normalizedSentence.length,\n matchStart: 0,\n normalizedQuote,\n normalizedSentence,\n score: EXACT_SCORE,\n strategy: \"exact\",\n };\n }\n\n if (normalizedQuote === normalizedSentence) {\n return {\n exactNormalized: true,\n exactRaw: false,\n exactSubstring: true,\n matchEnd: normalizedSentence.length,\n matchStart: 0,\n normalizedQuote,\n normalizedSentence,\n score: NORMALIZED_EXACT_SCORE,\n strategy: \"normalized_exact\",\n };\n }\n\n const substringStart = normalizedSentence.indexOf(normalizedQuote);\n\n if (substringStart !== -1) {\n const coverage =\n normalizedQuote.length / Math.max(1, normalizedSentence.length);\n\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: true,\n matchEnd: substringStart + normalizedQuote.length,\n matchStart: substringStart,\n normalizedQuote,\n normalizedSentence,\n score: Math.min(\n 0.99,\n SUBSTRING_BASE_SCORE + SUBSTRING_COVERAGE_BONUS * coverage,\n ),\n strategy: \"normalized_substring\",\n };\n }\n\n return scoreBestFuzzyWindow(normalizedQuote, normalizedSentence);\n}\n\nfunction scoreBestFuzzyWindow(\n normalizedQuote: string,\n normalizedSentence: string,\n): EvidenceQuoteScore {\n const minWindow = Math.max(\n 1,\n Math.floor(normalizedQuote.length * FUZZY_MIN_WINDOW_RATIO),\n );\n const maxWindow = Math.min(\n normalizedSentence.length,\n Math.max(\n minWindow,\n Math.ceil(normalizedQuote.length * FUZZY_MAX_WINDOW_RATIO),\n ),\n );\n let best: EvidenceQuoteScore = createEmptyScore(\n normalizedQuote,\n normalizedSentence,\n );\n\n for (let windowSize = minWindow; windowSize <= maxWindow; windowSize += 1) {\n for (\n let start = 0;\n start <= normalizedSentence.length - windowSize;\n start += 1\n ) {\n const window = normalizedSentence.slice(start, start + windowSize);\n const score = scoreWindow(normalizedQuote, window);\n\n if (score <= best.score) {\n continue;\n }\n\n best = {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: start + windowSize,\n matchStart: start,\n normalizedQuote,\n normalizedSentence,\n score,\n strategy: \"fuzzy_window\",\n };\n }\n }\n\n return best;\n}\n\nfunction scoreWindow(quote: string, window: string): number {\n const editScore = levenshteinSimilarity(quote, window);\n const substringScore = longestCommonSubstringScore(quote, window);\n const subsequenceScore = longestCommonSubsequenceScore(quote, window);\n const orderedCoverageScore = orderedCoverage(quote, window);\n const ngramScore = charNgramScore(quote, window);\n const lengthScore = lengthSimilarity(quote, window);\n const unorderedScore = unorderedCharOverlapScore(quote, window);\n const orderedScore = Math.max(\n editScore,\n substringScore,\n subsequenceScore,\n orderedCoverageScore,\n );\n let score =\n 0.2 * editScore +\n 0.18 * substringScore +\n 0.24 * subsequenceScore +\n 0.2 * orderedCoverageScore +\n 0.08 * ngramScore +\n 0.05 * lengthScore +\n 0.05 * unorderedScore;\n\n if (\n unorderedScore >= UNORDERED_PENALTY_THRESHOLD &&\n orderedScore < LOW_ORDERED_PENALTY_THRESHOLD\n ) {\n score *= 0.65;\n }\n\n return Math.max(0, Math.min(1, score));\n}\n\nfunction normalizeScalar(char: string): string {\n return CASE_FOLDING_OVERRIDES.get(char) ?? char.toLocaleLowerCase(\"und\");\n}\n\nfunction caseFold(text: string): string {\n let folded = \"\";\n\n for (const char of text) {\n folded += normalizeScalar(char);\n }\n\n return folded;\n}\n\nfunction replaceSeparatorsWithSpace(text: string): string {\n let output = \"\";\n\n for (const char of text) {\n output += isPreservedSymbol(char) || !isSeparator(char) ? char : \" \";\n }\n\n return output;\n}\n\nfunction isPreservedSymbol(char: string): boolean {\n return char === \"+\" || char === \"#\" || char === \"&\";\n}\n\nfunction isSeparator(char: string): boolean {\n return (\n /\\s/u.test(char) ||\n /\\p{Separator}/u.test(char) ||\n /[\\p{Dash_Punctuation}\\p{Connector_Punctuation}\\p{Initial_Punctuation}\\p{Final_Punctuation}\\p{Open_Punctuation}\\p{Close_Punctuation}\\p{Other_Punctuation}]/u.test(\n char,\n ) ||\n /[\\\\/.。.。、,,::;;!!??·・•|]/u.test(char)\n );\n}\n\nfunction stripMarkup(text: string): string {\n return text.replace(/<\\/?(?:mention|quote)\\b[^>]*>/giu, \"\");\n}\n\nfunction createEmptyScore(\n normalizedQuote: string,\n normalizedSentence: string,\n): EvidenceQuoteScore {\n return {\n exactNormalized: false,\n exactRaw: false,\n exactSubstring: false,\n matchEnd: -1,\n matchStart: -1,\n normalizedQuote,\n normalizedSentence,\n score: 0,\n strategy: \"empty\",\n };\n}\n\nfunction charNgramScore(left: string, right: string): number {\n const bigramScore = diceCoefficient(\n charNgrams(left, 2),\n charNgrams(right, 2),\n );\n const trigramScore = diceCoefficient(\n charNgrams(left, 3),\n charNgrams(right, 3),\n );\n\n return (bigramScore + trigramScore) / 2;\n}\n\nfunction charNgrams(text: string, size: number): ReadonlySet<string> {\n if (text.length <= size) {\n return text === \"\" ? new Set() : new Set([text]);\n }\n\n const result = new Set<string>();\n\n for (let index = 0; index <= text.length - size; index += 1) {\n result.add(text.slice(index, index + size));\n }\n\n return result;\n}\n\nfunction diceCoefficient(\n left: ReadonlySet<string>,\n right: ReadonlySet<string>,\n): number {\n if (left.size === 0 || right.size === 0) {\n return 0;\n }\n\n let overlap = 0;\n\n for (const value of left) {\n if (right.has(value)) {\n overlap += 1;\n }\n }\n\n return (2 * overlap) / (left.size + right.size);\n}\n\nfunction lengthSimilarity(left: string, right: string): number {\n return (\n 1 -\n Math.abs(left.length - right.length) / Math.max(left.length, right.length)\n );\n}\n\nfunction levenshteinSimilarity(left: string, right: string): number {\n if (left === right) {\n return 1;\n }\n if (left === \"\" || right === \"\") {\n return 0;\n }\n\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let current = new Array<number>(right.length + 1).fill(0);\n const leftChars = [...left];\n const rightChars = [...right];\n\n for (const [leftIndex, leftChar] of leftChars.entries()) {\n current[0] = leftIndex + 1;\n\n for (const [rightIndex, rightChar] of rightChars.entries()) {\n const insertCost = (current[rightIndex] ?? 0) + 1;\n const deleteCost = (previous[rightIndex + 1] ?? 0) + 1;\n const replaceCost =\n (previous[rightIndex] ?? 0) + (leftChar === rightChar ? 0 : 1);\n\n current[rightIndex + 1] = Math.min(insertCost, deleteCost, replaceCost);\n }\n\n [previous, current] = [current, previous];\n }\n\n const distance =\n previous[rightChars.length] ?? Math.max(left.length, right.length);\n\n return 1 - distance / Math.max(left.length, right.length);\n}\n\nfunction longestCommonSubstringScore(left: string, right: string): number {\n if (left === \"\" || right === \"\") {\n return 0;\n }\n\n const leftChars = [...left];\n const rightChars = [...right];\n let previous = new Array<number>(rightChars.length + 1).fill(0);\n let current = new Array<number>(rightChars.length + 1).fill(0);\n let longest = 0;\n\n for (let leftIndex = 1; leftIndex <= leftChars.length; leftIndex += 1) {\n for (let rightIndex = 1; rightIndex <= rightChars.length; rightIndex += 1) {\n current[rightIndex] =\n leftChars[leftIndex - 1] === rightChars[rightIndex - 1]\n ? (previous[rightIndex - 1] ?? 0) + 1\n : 0;\n longest = Math.max(longest, current[rightIndex] ?? 0);\n }\n\n [previous, current] = [current, previous];\n current.fill(0);\n }\n\n return (2 * longest) / (leftChars.length + rightChars.length);\n}\n\nfunction longestCommonSubsequenceScore(left: string, right: string): number {\n if (left === \"\" || right === \"\") {\n return 0;\n }\n\n const leftChars = [...left];\n const rightChars = [...right];\n let previous = new Array<number>(rightChars.length + 1).fill(0);\n let current = new Array<number>(rightChars.length + 1).fill(0);\n\n for (let leftIndex = 1; leftIndex <= leftChars.length; leftIndex += 1) {\n for (let rightIndex = 1; rightIndex <= rightChars.length; rightIndex += 1) {\n current[rightIndex] =\n leftChars[leftIndex - 1] === rightChars[rightIndex - 1]\n ? (previous[rightIndex - 1] ?? 0) + 1\n : Math.max(previous[rightIndex] ?? 0, current[rightIndex - 1] ?? 0);\n }\n\n [previous, current] = [current, previous];\n current.fill(0);\n }\n\n const longest = previous[rightChars.length] ?? 0;\n\n return (2 * longest) / (leftChars.length + rightChars.length);\n}\n\nfunction orderedCoverage(left: string, right: string): number {\n if (left === \"\" || right === \"\") {\n return 0;\n }\n\n const leftChars = [...left];\n const rightChars = [...right];\n let previous = new Array<number>(rightChars.length + 1).fill(0);\n let current = new Array<number>(rightChars.length + 1).fill(0);\n\n for (let leftIndex = 1; leftIndex <= leftChars.length; leftIndex += 1) {\n for (let rightIndex = 1; rightIndex <= rightChars.length; rightIndex += 1) {\n current[rightIndex] =\n leftChars[leftIndex - 1] === rightChars[rightIndex - 1]\n ? (previous[rightIndex - 1] ?? 0) + 1\n : Math.max(previous[rightIndex] ?? 0, current[rightIndex - 1] ?? 0);\n }\n\n [previous, current] = [current, previous];\n current.fill(0);\n }\n\n return (previous[rightChars.length] ?? 0) / leftChars.length;\n}\n\nfunction unorderedCharOverlapScore(left: string, right: string): number {\n const leftCounts = countChars(left);\n const rightCounts = countChars(right);\n let overlap = 0;\n let total = 0;\n\n for (const [char, leftCount] of leftCounts) {\n const rightCount = rightCounts.get(char) ?? 0;\n\n overlap += Math.min(leftCount, rightCount);\n total += leftCount;\n }\n\n return total === 0 ? 0 : overlap / total;\n}\n\nfunction countChars(text: string): Map<string, number> {\n const counts = new Map<string, number>();\n\n for (const char of text) {\n counts.set(char, (counts.get(char) ?? 0) + 1);\n }\n\n return counts;\n}\n","import {\n normalizeEvidenceDisplayText,\n prepareEvidenceQuote,\n scorePreparedEvidenceQuote,\n type PreparedEvidenceQuote,\n type EvidenceQuoteScore,\n} from \"./quote-score.js\";\nimport { splitTextIntoSentences } from \"./text.js\";\nimport type {\n EvidenceSelection,\n EvidenceSelectionCandidate,\n EvidenceSelectionFailure,\n EvidenceSelectionList,\n EvidenceSelectionResolution,\n EvidenceSelectionSentence,\n} from \"./types.js\";\n\nconst DIRECT_ID_MIN_SCORE = 0.72;\nconst DIRECT_ID_SUBSTRING_MIN_SCORE = 0.95;\nconst AUTO_TOP_MIN_SCORE = 0.72;\nconst AUTO_TOP_MIN_GAP = 0.15;\nconst LOW_CONFIDENCE_MAX_SCORE = 0.45;\nconst MAX_CANDIDATES = 5;\n\nexport function resolveEvidenceSelection(input: {\n readonly evidence: EvidenceSelection;\n readonly sentences: readonly EvidenceSelectionSentence[];\n}): readonly [\n resolution: EvidenceSelectionResolution | undefined,\n failure: EvidenceSelectionFailure | undefined,\n] {\n const quote = normalizeQuote(input.evidence.quote);\n const sentenceId = normalizeSentenceId(input.evidence.sentence_id);\n let directCandidateById: EvidenceSelectionCandidate | undefined;\n\n if (quote === \"\") {\n return [\n undefined,\n {\n candidates: [],\n code: \"invalid\",\n message: \"Evidence quote is missing or empty.\",\n },\n ];\n }\n\n const preparedQuote = prepareEvidenceQuote(quote);\n\n if (sentenceId !== undefined) {\n const directIndex = input.sentences.findIndex(\n (sentence) => sentence.id === sentenceId,\n );\n const directSentence = input.sentences[directIndex];\n\n if (directSentence !== undefined) {\n const directCandidate = createCandidate({\n index: directIndex,\n scored: scorePreparedEvidenceQuote({\n preparedQuote,\n sentence: directSentence.text,\n }),\n sentence: directSentence,\n sentences: input.sentences,\n });\n directCandidateById = directCandidate;\n\n if (isDirectExactCandidateTrusted(directCandidate)) {\n return [\n {\n candidate: directCandidate,\n confidence: directCandidate.score,\n sentenceIds: [directCandidate.sentence.sentenceId],\n strategy: `sentence_id+${directCandidate.strategy}`,\n },\n undefined,\n ];\n }\n }\n }\n\n const candidates = rankTopEvidenceQuoteCandidates(\n preparedQuote,\n input.sentences,\n MAX_CANDIDATES,\n );\n\n if (sentenceId !== undefined) {\n const directCandidate =\n candidates.find((candidate) => candidate.occurrenceId === sentenceId) ??\n directCandidateById;\n const topCandidate = candidates[0];\n\n if (\n directCandidate !== undefined &&\n topCandidate !== undefined &&\n isDirectCandidateTrusted(directCandidate, topCandidate)\n ) {\n return [\n {\n candidate: directCandidate,\n confidence: directCandidate.score,\n sentenceIds: [directCandidate.sentence.sentenceId],\n strategy: `sentence_id+${directCandidate.strategy}`,\n },\n undefined,\n ];\n }\n }\n\n const topCandidate = candidates[0];\n\n if (topCandidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n message:\n \"Evidence quote could not be matched: no candidates available.\",\n },\n ];\n }\n\n const secondScore = candidates[1]?.score ?? 0;\n const gap = topCandidate.score - secondScore;\n const boundaryHint = formatSentenceBoundaryHint(quote);\n\n if (topCandidate.score < LOW_CONFIDENCE_MAX_SCORE) {\n return [\n undefined,\n {\n candidates: candidates.slice(0, MAX_CANDIDATES),\n code: \"low_confidence\",\n message:\n \"Evidence quote could not be matched confidently. \" +\n `Best candidate score=${topCandidate.score.toFixed(3)}.` +\n boundaryHint,\n },\n ];\n }\n\n if (topCandidate.score >= AUTO_TOP_MIN_SCORE && gap >= AUTO_TOP_MIN_GAP) {\n return [\n {\n candidate: topCandidate,\n confidence: topCandidate.score,\n sentenceIds: [topCandidate.sentence.sentenceId],\n strategy: `quote_auto_top1:${topCandidate.strategy}`,\n },\n undefined,\n ];\n }\n\n return [\n undefined,\n {\n candidates: candidates.slice(0, MAX_CANDIDATES),\n code: \"ambiguous\",\n message:\n \"Evidence quote is ambiguous. Choose one candidate occurrence ID.\" +\n boundaryHint,\n },\n ];\n}\n\nfunction formatSentenceBoundaryHint(quote: string): string {\n if (splitTextIntoSentences(quote).length <= 1) {\n return \"\";\n }\n\n return (\n \" Evidence quote appears to contain more than one sentence. \" +\n \"Each evidence item must stay within one source sentence. \" +\n \"Recheck whether this evidence should be shortened, split into multiple evidence items, or removed.\"\n );\n}\n\nexport function resolveEvidenceSelectionList(input: {\n readonly evidence: EvidenceSelectionList;\n readonly sentences: readonly EvidenceSelectionSentence[];\n}): readonly [\n resolution: EvidenceSelectionResolution | undefined,\n failure: EvidenceSelectionFailure | undefined,\n] {\n const selections = normalizeEvidenceSelectionList(input.evidence);\n\n if (selections.length === 0) {\n return [\n undefined,\n {\n candidates: [],\n code: \"invalid\",\n message: \"Evidence selection list is empty.\",\n },\n ];\n }\n\n const sentenceIds: EvidenceSelectionResolution[\"sentenceIds\"][number][] = [];\n const strategies: string[] = [];\n let confidence = 1;\n let candidate: EvidenceSelectionCandidate | undefined;\n\n for (const selection of selections) {\n const [resolution, failure] = resolveEvidenceSelection({\n evidence: selection,\n sentences: input.sentences,\n });\n\n if (failure !== undefined) {\n return [undefined, failure];\n }\n\n if (resolution === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n message: \"Evidence selection could not be resolved.\",\n },\n ];\n }\n\n candidate ??= resolution.candidate;\n confidence = Math.min(confidence, resolution.confidence);\n sentenceIds.push(...resolution.sentenceIds);\n strategies.push(resolution.strategy);\n }\n\n if (candidate === undefined) {\n return [\n undefined,\n {\n candidates: [],\n code: \"none\",\n message: \"Evidence selection could not be resolved.\",\n },\n ];\n }\n\n return [\n {\n candidate,\n confidence,\n sentenceIds: uniqueSentenceIds(sentenceIds),\n strategy: `list:${strategies.join(\",\")}`,\n },\n undefined,\n ];\n}\n\nfunction normalizeEvidenceSelectionList(\n evidence: EvidenceSelectionList,\n): readonly EvidenceSelection[] {\n if (isEvidenceSelectionArray(evidence)) {\n return evidence;\n }\n\n return [evidence];\n}\n\nfunction isEvidenceSelectionArray(\n evidence: EvidenceSelectionList,\n): evidence is readonly EvidenceSelection[] {\n return Array.isArray(evidence);\n}\n\nexport function rankEvidenceQuote(\n quote: string,\n sentences: readonly EvidenceSelectionSentence[],\n): EvidenceSelectionCandidate[] {\n const preparedQuote = prepareEvidenceQuote(quote);\n\n return sentences\n .map((sentence, index) => {\n const scored = scorePreparedEvidenceQuote({\n preparedQuote,\n sentence: sentence.text,\n });\n\n return createCandidate({\n index,\n scored,\n sentence,\n sentences,\n });\n })\n .sort(compareCandidates);\n}\n\nfunction rankTopEvidenceQuoteCandidates(\n preparedQuote: PreparedEvidenceQuote,\n sentences: readonly EvidenceSelectionSentence[],\n limit: number,\n): EvidenceSelectionCandidate[] {\n const candidates: EvidenceSelectionCandidate[] = [];\n\n for (const [index, sentence] of sentences.entries()) {\n const scored = scorePreparedEvidenceQuote({\n preparedQuote,\n sentence: sentence.text,\n });\n const candidate = createCandidate({\n index,\n scored,\n sentence,\n sentences,\n });\n\n insertCandidate(candidates, candidate, limit);\n }\n\n return candidates;\n}\n\nfunction insertCandidate(\n candidates: EvidenceSelectionCandidate[],\n candidate: EvidenceSelectionCandidate,\n limit: number,\n): void {\n const insertAt = candidates.findIndex(\n (item) => compareCandidates(candidate, item) < 0,\n );\n\n if (insertAt === -1) {\n if (candidates.length < limit) {\n candidates.push(candidate);\n }\n return;\n }\n\n candidates.splice(insertAt, 0, candidate);\n\n if (candidates.length > limit) {\n candidates.pop();\n }\n}\n\nfunction createCandidate(input: {\n readonly index: number;\n readonly scored: EvidenceQuoteScore;\n readonly sentence: EvidenceSelectionSentence;\n readonly sentences: readonly EvidenceSelectionSentence[];\n}): EvidenceSelectionCandidate {\n return {\n exactNormalized: input.scored.exactNormalized,\n exactRaw: input.scored.exactRaw,\n exactSubstring: input.scored.exactSubstring,\n index: input.index,\n nextText: input.sentences[input.index + 1]?.text ?? \"\",\n occurrenceId: input.sentence.id,\n prevText: input.sentences[input.index - 1]?.text ?? \"\",\n score: input.scored.score,\n sentence: input.sentence,\n strategy: input.scored.strategy,\n };\n}\n\nfunction compareCandidates(\n left: EvidenceSelectionCandidate,\n right: EvidenceSelectionCandidate,\n): number {\n if (left.score !== right.score) {\n return right.score - left.score;\n }\n if (left.exactRaw !== right.exactRaw) {\n return left.exactRaw ? -1 : 1;\n }\n if (left.exactSubstring !== right.exactSubstring) {\n return left.exactSubstring ? -1 : 1;\n }\n if (left.exactNormalized !== right.exactNormalized) {\n return left.exactNormalized ? -1 : 1;\n }\n\n return left.index - right.index;\n}\n\nfunction isDirectCandidateTrusted(\n candidate: EvidenceSelectionCandidate,\n topCandidate: EvidenceSelectionCandidate,\n): boolean {\n const trustedScore =\n candidate.score >= DIRECT_ID_MIN_SCORE ||\n (candidate.exactSubstring &&\n candidate.score >= DIRECT_ID_SUBSTRING_MIN_SCORE);\n\n return (\n trustedScore &&\n (candidate.occurrenceId === topCandidate.occurrenceId ||\n topCandidate.score - candidate.score < AUTO_TOP_MIN_GAP)\n );\n}\n\nfunction isDirectExactCandidateTrusted(\n candidate: EvidenceSelectionCandidate,\n): boolean {\n return (\n candidate.score >= DIRECT_ID_SUBSTRING_MIN_SCORE && candidate.exactSubstring\n );\n}\n\nfunction normalizeQuote(value: string | undefined): string {\n return normalizeEvidenceDisplayText(value ?? \"\");\n}\n\nfunction normalizeSentenceId(value: string | undefined): string | undefined {\n const normalized = value?.trim();\n\n return normalized === \"\" ? undefined : normalized;\n}\n\nfunction uniqueSentenceIds(\n sentenceIds: EvidenceSelectionResolution[\"sentenceIds\"],\n): EvidenceSelectionResolution[\"sentenceIds\"] {\n const seen = new Set<string>();\n const unique: EvidenceSelectionResolution[\"sentenceIds\"][number][] = [];\n\n for (const sentenceId of sentenceIds) {\n const key = sentenceId.join(\":\");\n\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n unique.push(sentenceId);\n }\n\n return unique;\n}\n","export { EvidenceResolver } from \"./anchor-resolver.js\";\nexport type {\n EvidenceResolutionFailure,\n EvidenceResolutionResult,\n RankedSentenceCandidate,\n} from \"./anchor-types.js\";\nexport {\n EVIDENCE_SELECTION_JSON_SHAPE,\n EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n formatEvidenceSelectionChoicePrompt,\n} from \"./prompt.js\";\nexport {\n normalizeEvidenceDisplayText,\n normalizeEvidenceText,\n scoreEvidenceQuote,\n type EvidenceQuoteMatchStrategy,\n type EvidenceQuoteScore,\n} from \"./quote-score.js\";\nexport {\n rankEvidenceQuote,\n resolveEvidenceSelection,\n resolveEvidenceSelectionList,\n} from \"./selection-resolver.js\";\nexport { normalizeText, splitTextIntoSentences } from \"./text.js\";\nexport type {\n EvidenceSelection,\n EvidenceSelectionCandidate,\n EvidenceSelectionFailure,\n EvidenceSelectionList,\n EvidenceSelectionResolution,\n EvidenceSelectionSentence,\n EvidenceSentenceId,\n} from \"./types.js\";\n","import { z } from \"zod\";\n\nimport {\n GuaranteedParseValidationError,\n ParsedJsonError,\n requestGuaranteedJson,\n} from \"../../guaranteed/index.js\";\nimport type { LLMessage } from \"../../llm/index.js\";\nimport {\n ChunkImportance,\n ChunkRetention,\n expectChunkImportance,\n expectChunkRetention,\n type SentenceId,\n} from \"../../document/index.js\";\nimport {\n resolveEvidenceSelectionList,\n type EvidenceSelectionCandidate,\n type EvidenceSelectionList,\n EvidenceResolver,\n type EvidenceResolutionFailure,\n type RankedSentenceCandidate,\n} from \"../../evidence-selection/index.js\";\nimport type {\n ChunkBatch,\n ChunkExtractionSentence,\n ChunkImportanceAnnotation,\n ChunkLink,\n CognitiveChunk,\n SentenceTextSource,\n} from \"./types.js\";\nimport type { FragmentProjection, TextSpan } from \"./fragment-projection.js\";\n\nconst MAX_CHOICE_RETRIES = 3;\n\nconst chunkLinkSchema = z.object({\n from: z.union([z.number().int(), z.string()]),\n strength: z.string().optional(),\n to: z.union([z.number().int(), z.string()]),\n});\nconst evidenceSelectionItemSchema = z\n .object({\n quote: z.string().optional(),\n sentence_id: z.string().optional(),\n })\n .passthrough();\nconst chunkEvidenceSchema = z.union([\n z.record(z.string(), z.unknown()),\n z.array(evidenceSelectionItemSchema),\n]);\n\nconst userFocusedChunkSchema = z\n .object({\n content: z.string(),\n evidence: chunkEvidenceSchema.nullish(),\n label: z.string(),\n retention: z.enum([\n ChunkRetention.Verbatim,\n ChunkRetention.Detailed,\n ChunkRetention.Focused,\n ChunkRetention.Relevant,\n ]),\n temp_id: z.string(),\n })\n .passthrough();\n\nexport const userFocusedResponseSchema = z.object({\n chunks: z.array(userFocusedChunkSchema),\n fragment_summary: z.string(),\n links: z.array(chunkLinkSchema),\n});\n\nconst bookCoherenceChunkSchema = z\n .object({\n content: z.string(),\n evidence: chunkEvidenceSchema.nullish(),\n importance: z.enum([\n ChunkImportance.Critical,\n ChunkImportance.Important,\n ChunkImportance.Helpful,\n ]),\n label: z.string(),\n temp_id: z.string(),\n })\n .passthrough();\n\nconst importanceAnnotationSchema = z.object({\n chunk_id: z.number().int(),\n importance: z.enum([\n ChunkImportance.Critical,\n ChunkImportance.Important,\n ChunkImportance.Helpful,\n ]),\n});\n\nexport const bookCoherenceResponseSchema = z.object({\n chunks: z.array(bookCoherenceChunkSchema),\n importance_annotations: z.array(importanceAnnotationSchema),\n links: z.array(chunkLinkSchema),\n});\n\nconst choiceResponseSchema = z.object({\n choice: z.string(),\n});\n\ntype UserFocusedChunkData = z.infer<typeof userFocusedChunkSchema>;\ntype BookCoherenceChunkData = z.infer<typeof bookCoherenceChunkSchema>;\nexport type UserFocusedResponseData = z.infer<typeof userFocusedResponseSchema>;\nexport type BookCoherenceResponseData = z.infer<\n typeof bookCoherenceResponseSchema\n>;\ntype ExtractedChunkData = UserFocusedChunkData | BookCoherenceChunkData;\ntype RawChunkLink = z.infer<typeof chunkLinkSchema>;\ntype RawChunkEvidence = z.infer<typeof chunkEvidenceSchema>;\ntype ChoiceFieldName = \"evidence\" | \"start_anchor\" | \"end_anchor\";\n\nexport enum ChunkMetadataField {\n Retention = \"retention\",\n Importance = \"importance\",\n}\n\nexport interface ExtractChunksResult {\n readonly chunkBatch: ChunkBatch;\n readonly fragmentSummary?: string;\n}\n\ninterface ResolveChunkEvidenceInput {\n readonly data: ExtractedChunkData;\n readonly chunkIndex: number;\n readonly chunkLabel: string;\n readonly isLastGenerationAttempt: boolean;\n}\n\ninterface SelectAmbiguousCandidateInput {\n readonly candidates: readonly RankedSentenceCandidate[];\n readonly chunkData: ExtractedChunkData;\n readonly chunkIndex: number;\n readonly chunkLabel: string;\n readonly fieldName: ChoiceFieldName;\n}\n\ntype GuaranteedChoiceRequest = (\n messages: readonly LLMessage[],\n index: number,\n maxRetries: number,\n) => Promise<string>;\n\nexport class ChunkBatchParser<\n TData extends UserFocusedResponseData | BookCoherenceResponseData,\n> {\n readonly #choiceSystemPrompt: string;\n readonly #evidenceResolver = new EvidenceResolver();\n readonly #metadataField: ChunkMetadataField;\n readonly #projection: FragmentProjection;\n readonly #responseIntentClassifierPrompt: string;\n readonly #requestChoice: GuaranteedChoiceRequest;\n readonly #sentenceTextByKey: Readonly<Record<string, string>>;\n readonly #sentenceTextSource: SentenceTextSource;\n readonly #wordsCountByKey: Readonly<Record<string, number>>;\n readonly #validImportanceChunkIds: Readonly<Record<string, true>> | undefined;\n readonly #visibleChunkIds: readonly number[];\n\n public constructor(input: {\n choiceSystemPrompt: string;\n metadataField: ChunkMetadataField;\n projection: FragmentProjection;\n responseIntentClassifierPrompt: string;\n requestChoice: GuaranteedChoiceRequest;\n sentenceTextSource: SentenceTextSource;\n sentences: readonly ChunkExtractionSentence[];\n validImportanceChunkIds?: readonly number[];\n visibleChunkIds: readonly number[];\n }) {\n this.#choiceSystemPrompt = input.choiceSystemPrompt;\n this.#metadataField = input.metadataField;\n this.#projection = input.projection;\n this.#responseIntentClassifierPrompt = input.responseIntentClassifierPrompt;\n this.#requestChoice = input.requestChoice;\n this.#sentenceTextSource = input.sentenceTextSource;\n this.#visibleChunkIds = input.visibleChunkIds;\n\n this.#wordsCountByKey = createWordsCountRecord(\n input.projection.sentences.map((sentence) => ({\n sentenceId: sentence.sentenceId,\n wordsCount: sentence.wordsCount,\n })),\n );\n this.#sentenceTextByKey = createSentenceTextRecord(input.projection);\n this.#validImportanceChunkIds =\n input.validImportanceChunkIds === undefined\n ? undefined\n : createMembershipRecord(input.validImportanceChunkIds);\n }\n\n public async parse(\n parsedData: TData,\n input: {\n isLastGenerationAttempt: boolean;\n },\n ): Promise<ExtractChunksResult> {\n const issues: string[] = [];\n const chunks: ChunkBatch[\"chunks\"] = [];\n const tempIds: string[] = [];\n const importanceAnnotations =\n \"importance_annotations\" in parsedData\n ? this.#validateImportanceAnnotations(\n parsedData.importance_annotations,\n issues,\n )\n : undefined;\n const fragmentSummary =\n \"fragment_summary\" in parsedData\n ? parsedData.fragment_summary\n : undefined;\n const links = normalizeChunkLinks(parsedData.links);\n\n for (const [index, data] of parsedData.chunks.entries()) {\n const chunkIndex = index + 1;\n const chunkIssues: string[] = [];\n const label = data.label.trim();\n const content = data.content.trim();\n\n if (label === \"\") {\n chunkIssues.push(\n `Chunk #${chunkIndex}: Missing or empty \"label\" field`,\n );\n }\n\n if (content === \"\") {\n chunkIssues.push(\n `Chunk #${chunkIndex}: Missing or empty \"content\" field`,\n );\n }\n\n const [matchedSentenceIds, evidenceFailure] =\n await this.#resolveChunkEvidence({\n chunkIndex,\n chunkLabel: label,\n data,\n isLastGenerationAttempt: input.isLastGenerationAttempt,\n });\n\n if (matchedSentenceIds.length === 0) {\n if (evidenceFailure !== undefined) {\n chunkIssues.push(\n `Chunk #${chunkIndex} (\"${label}\"): ${evidenceFailure.message}`,\n );\n } else {\n chunkIssues.push(\n `Chunk #${chunkIndex} (\"${label}\"): Missing evidence`,\n );\n }\n }\n\n if (chunkIssues.length > 0) {\n issues.push(...chunkIssues);\n continue;\n }\n\n const primarySentenceId = matchedSentenceIds[0];\n\n if (primarySentenceId === undefined) {\n issues.push(\n `Chunk #${chunkIndex} (\"${label}\"): Unable to resolve any sentence IDs`,\n );\n continue;\n }\n\n const totalWordsCount = matchedSentenceIds.reduce((sum, sentenceId) => {\n return (\n sum + (this.#wordsCountByKey[createSentenceKey(sentenceId)] ?? 0)\n );\n }, 0);\n\n if (this.#metadataField === ChunkMetadataField.Retention) {\n const chunkData = data as UserFocusedChunkData;\n\n chunks.push({\n content,\n generation: 0,\n id: 0,\n label,\n links: [],\n retention: expectChunkRetention(chunkData.retention),\n sentenceId: primarySentenceId,\n sentenceIds: [...matchedSentenceIds],\n wordsCount: totalWordsCount,\n });\n } else {\n const chunkData = data as BookCoherenceChunkData;\n\n chunks.push({\n content,\n generation: 0,\n id: 0,\n importance: expectChunkImportance(chunkData.importance),\n label,\n links: [],\n sentenceId: primarySentenceId,\n sentenceIds: [...matchedSentenceIds],\n wordsCount: totalWordsCount,\n });\n }\n\n tempIds.push(data.temp_id);\n }\n\n const validLinks = this.#filterAndValidateLinks({\n issues,\n links,\n tempIds,\n visibleChunkIds: this.#visibleChunkIds,\n });\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n return {\n chunkBatch: {\n chunks,\n links: validLinks,\n orderCorrect: true,\n tempIds,\n ...(importanceAnnotations === undefined\n ? {}\n : { importanceAnnotations }),\n },\n ...(fragmentSummary === undefined ? {} : { fragmentSummary }),\n };\n }\n\n public async getChunkSourceSentences(\n chunk: CognitiveChunk,\n ): Promise<string[]> {\n const sourceSentences: string[] = [];\n\n for (const sentenceId of chunk.sentenceIds) {\n const sentenceKey = createSentenceKey(sentenceId);\n const sentenceText = this.#sentenceTextByKey[sentenceKey];\n\n if (sentenceText !== undefined) {\n sourceSentences.push(sentenceText);\n continue;\n }\n\n sourceSentences.push(\n await this.#sentenceTextSource.getSentence(sentenceId),\n );\n }\n\n return sourceSentences;\n }\n\n async #resolveChunkEvidence(\n input: ResolveChunkEvidenceInput,\n ): Promise<\n readonly [\n sentenceIds: readonly SentenceId[],\n failure: EvidenceResolutionFailure | undefined,\n ]\n > {\n const evidence = input.data.evidence;\n\n if (evidence === undefined || evidence === null) {\n return [[], undefined];\n }\n\n if (!isRecord(evidence) && !Array.isArray(evidence)) {\n return [\n [],\n {\n candidates: [],\n code: \"invalid_evidence\",\n fieldName: \"evidence\",\n message: `Chunk #${input.chunkIndex} (\"${input.chunkLabel}\"): evidence must be an object or array`,\n },\n ];\n }\n\n const projectedSentences = this.#projection.sentences;\n const candidateSentenceIds = projectedSentences.map(\n (sentence) => sentence.sentenceId,\n );\n const selectionSentences = projectedSentences.map((sentence, index) => ({\n id: `S${index + 1}`,\n sentenceId: sentence.sentenceId,\n text: sentence.projectedText,\n }));\n const selectionEvidence = createEvidenceSelectionList(evidence);\n const [selectionResolution, selectionFailure] =\n selectionEvidence === undefined\n ? [undefined, undefined]\n : resolveEvidenceSelectionList({\n evidence: selectionEvidence,\n sentences: selectionSentences,\n });\n\n if (selectionResolution !== undefined) {\n return [selectionResolution.sentenceIds, undefined];\n }\n\n const exactMatchSentenceIds = Array.isArray(evidence)\n ? []\n : this.#resolveExactProjectionEvidence(evidence);\n\n if (exactMatchSentenceIds.length > 0) {\n return [exactMatchSentenceIds, undefined];\n }\n\n const candidateTexts = projectedSentences.map(\n (sentence) => sentence.projectedText,\n );\n const [resolution, failure] = Array.isArray(evidence)\n ? [undefined, undefined]\n : this.#evidenceResolver.resolve(\n evidence,\n candidateSentenceIds,\n candidateTexts,\n );\n\n if (resolution !== undefined) {\n return [resolution.sentenceIds, undefined];\n }\n\n const fallbackFailure =\n selectionFailure === undefined\n ? failure\n : toEvidenceResolutionFailure(selectionFailure, \"evidence\");\n\n if (fallbackFailure === undefined) {\n return [[], undefined];\n }\n\n const shouldUseChoice =\n fallbackFailure.code.startsWith(\"ambiguous\") ||\n (fallbackFailure.code === \"low_confidence\" &&\n input.isLastGenerationAttempt &&\n fallbackFailure.candidates.length > 0);\n\n if (!shouldUseChoice) {\n return [[], fallbackFailure];\n }\n\n const choiceFieldName = toChoiceFieldName(fallbackFailure.fieldName);\n\n if (choiceFieldName === undefined) {\n return [[], fallbackFailure];\n }\n\n const [choiceCandidate, choiceFailure] =\n await this.#chooseAmbiguousCandidate({\n candidates: fallbackFailure.candidates,\n chunkData: input.data,\n chunkIndex: input.chunkIndex,\n chunkLabel: input.chunkLabel,\n fieldName: choiceFieldName,\n });\n\n if (choiceFailure !== undefined) {\n if (choiceFailure.code === \"choice_parse_failed_full_fragment\") {\n return [candidateSentenceIds, undefined];\n }\n\n return [[], choiceFailure];\n }\n\n if (choiceCandidate === undefined) {\n return [\n [],\n {\n candidates: fallbackFailure.candidates,\n code: \"choice_failed\",\n fieldName: fallbackFailure.fieldName,\n message: `Second-stage choice failed for ${fallbackFailure.fieldName}: no candidate returned.`,\n },\n ];\n }\n\n if (choiceFieldName === \"evidence\") {\n return [[choiceCandidate.sentenceId], undefined];\n }\n\n if (Array.isArray(evidence)) {\n return [[], fallbackFailure];\n }\n\n if (input.isLastGenerationAttempt) {\n const [resolved, resolveFailure] =\n this.#evidenceResolver.resolveWithOverrides({\n candidateSentenceIds,\n candidateTexts,\n evidence,\n overrides: {\n [choiceFieldName]: choiceCandidate,\n },\n });\n\n if (resolved !== undefined) {\n return [resolved.sentenceIds, undefined];\n }\n\n return [[], resolveFailure];\n }\n\n const repairedEvidence: Record<string, unknown> = {\n ...evidence,\n [choiceFieldName]: {\n mode: \"full\",\n text: choiceCandidate.text,\n },\n };\n const [resolved, resolveFailure] = this.#evidenceResolver.resolve(\n repairedEvidence,\n candidateSentenceIds,\n candidateTexts,\n );\n\n if (resolved !== undefined) {\n return [resolved.sentenceIds, undefined];\n }\n\n return [[], resolveFailure];\n }\n\n #resolveExactProjectionEvidence(\n evidence: Record<string, unknown>,\n ): SentenceId[] {\n const startValue = evidence.start_anchor ?? evidence.start;\n const [startAnchor, startFailure] = this.#evidenceResolver.parseAnchor(\n startValue,\n \"start_anchor\",\n );\n\n if (startFailure !== undefined || startAnchor?.text === undefined) {\n return [];\n }\n\n const startMatch = expectSingleSpan(\n this.#projection.findExactMatches(startAnchor.text),\n );\n\n if (startMatch === undefined) {\n return [];\n }\n\n const endValue = evidence.end_anchor ?? evidence.end;\n\n if (endValue === undefined) {\n return this.#projection.resolveSentenceIds(startMatch);\n }\n\n const [endAnchor, endFailure] = this.#evidenceResolver.parseAnchor(\n endValue,\n \"end_anchor\",\n );\n\n if (endFailure !== undefined || endAnchor?.text === undefined) {\n return [];\n }\n\n const endMatch = expectSingleSpan(\n this.#projection.findExactMatches(endAnchor.text),\n );\n\n if (endMatch === undefined || endMatch.offset < startMatch.offset) {\n return [];\n }\n\n return this.#projection.resolveSentenceIds({\n length: endMatch.offset + endMatch.length - startMatch.offset,\n offset: startMatch.offset,\n });\n }\n\n async #chooseAmbiguousCandidate(\n input: SelectAmbiguousCandidateInput,\n ): Promise<\n readonly [\n candidate: RankedSentenceCandidate | undefined,\n failure: EvidenceResolutionFailure | undefined,\n ]\n > {\n const messages = this.#buildChoiceMessages(input);\n const candidateIds = input.candidates.map(\n (candidate) => candidate.occurrenceId,\n );\n\n try {\n const choice = await requestGuaranteedJson({\n maxRetries: MAX_CHOICE_RETRIES,\n messages,\n parse: (data) => {\n const candidate = input.candidates.find(\n (item) => item.occurrenceId === data.choice,\n );\n\n if (candidate === undefined) {\n throw new ParsedJsonError([\n `Invalid choice \"${data.choice}\". Expected one of: ${candidateIds.join(\", \")}`,\n ]);\n }\n\n return candidate;\n },\n responseIntentClassifierPrompt: this.#responseIntentClassifierPrompt,\n request: this.#requestChoice,\n schema: choiceResponseSchema,\n });\n\n return [choice, undefined];\n } catch (error) {\n if (isParsedJsonValidationFailure(error)) {\n return [\n undefined,\n {\n candidates: input.candidates,\n code: \"choice_parse_failed_full_fragment\",\n fieldName: input.fieldName,\n message:\n `Second-stage choice parse validation failed for ${input.fieldName}; ` +\n \"falling back to the full fragment span.\",\n },\n ];\n }\n\n return [\n undefined,\n {\n candidates: input.candidates,\n code: \"choice_failed\",\n fieldName: input.fieldName,\n message: `Second-stage choice failed for ${input.fieldName}: ${formatError(error)}`,\n },\n ];\n }\n }\n\n #buildChoiceMessages(input: SelectAmbiguousCandidateInput): LLMessage[] {\n return [\n {\n content: this.#choiceSystemPrompt,\n role: \"system\",\n },\n {\n content:\n `Previously generated chunk (do NOT rewrite it):\\n` +\n `\\`\\`\\`json\\n${JSON.stringify(input.chunkData, null, 2)}\\n\\`\\`\\`\\n\\n` +\n `Resolve only this field: \"${input.fieldName}\" for chunk #${input.chunkIndex} [${input.chunkLabel}].\\n` +\n \"Choose exactly one candidate occurrence ID from the list below.\\n\\n\" +\n input.candidates.map(formatChoiceCandidate).join(\"\\n\"),\n role: \"user\",\n },\n ];\n }\n\n #validateImportanceAnnotations(\n annotations: readonly {\n readonly chunk_id: number;\n readonly importance: ChunkImportance;\n }[],\n issues: string[],\n ): ChunkImportanceAnnotation[] | undefined {\n if (annotations.length === 0) {\n return [];\n }\n\n if (this.#validImportanceChunkIds === undefined) {\n return annotations.map((annotation) => ({\n chunkId: annotation.chunk_id,\n importance: expectChunkImportance(annotation.importance),\n }));\n }\n\n const result: ChunkImportanceAnnotation[] = [];\n\n for (const annotation of annotations) {\n if (!hasMembership(this.#validImportanceChunkIds, annotation.chunk_id)) {\n issues.push(\n `importance_annotations references unknown chunk_id ${annotation.chunk_id}`,\n );\n continue;\n }\n\n result.push({\n chunkId: annotation.chunk_id,\n importance: expectChunkImportance(annotation.importance),\n });\n }\n\n return result;\n }\n\n #filterAndValidateLinks(input: {\n issues: string[];\n links: readonly ChunkLink[];\n tempIds: readonly string[];\n visibleChunkIds: readonly number[];\n }): ChunkLink[] {\n const validTempIds = createMembershipRecord(input.tempIds);\n const validChunkIds = createMembershipRecord(input.visibleChunkIds);\n const retainedLinks: ChunkLink[] = [];\n\n for (const [index, link] of input.links.entries()) {\n const fromValid = this.#validateLinkReference({\n fieldName: \"from\",\n index: index + 1,\n issues: input.issues,\n reference: link.from,\n validChunkIds,\n validTempIds,\n });\n const toValid = this.#validateLinkReference({\n fieldName: \"to\",\n index: index + 1,\n issues: input.issues,\n reference: link.to,\n validChunkIds,\n validTempIds,\n });\n\n if (fromValid && toValid) {\n retainedLinks.push(link);\n }\n }\n\n return retainedLinks;\n }\n\n #validateLinkReference(input: {\n fieldName: \"from\" | \"to\";\n index: number;\n issues: string[];\n reference: number | string;\n validChunkIds: Readonly<Record<string, true>>;\n validTempIds: Readonly<Record<string, true>>;\n }): boolean {\n if (typeof input.reference === \"string\") {\n return hasMembership(input.validTempIds, input.reference);\n }\n\n if (!hasMembership(input.validChunkIds, input.reference)) {\n input.issues.push(\n `Link #${input.index}: \"${input.fieldName}\" chunk_id ${input.reference} does not exist in visible chunks`,\n );\n\n return false;\n }\n\n return true;\n }\n}\n\nfunction createWordsCountRecord(\n sentences: readonly Pick<\n ChunkExtractionSentence,\n \"sentenceId\" | \"wordsCount\"\n >[],\n): Readonly<Record<string, number>> {\n const wordsCountByKey = createEmptyRecord<number>();\n\n for (const sentence of sentences) {\n const sentenceKey = createSentenceKey(sentence.sentenceId);\n wordsCountByKey[sentenceKey] = sentence.wordsCount;\n }\n\n return wordsCountByKey;\n}\n\nfunction createSentenceTextRecord(\n projection: FragmentProjection,\n): Readonly<Record<string, string>> {\n const record = createEmptyRecord<string>();\n\n for (const sentence of projection.sentences) {\n record[createSentenceKey(sentence.sentenceId)] = sentence.rawText;\n }\n\n return record;\n}\n\nfunction normalizeChunkLinks(links: readonly RawChunkLink[]): ChunkLink[] {\n return links.map((link) => {\n if (link.strength === undefined) {\n return {\n from: link.from,\n to: link.to,\n };\n }\n\n return {\n from: link.from,\n strength: link.strength,\n to: link.to,\n };\n });\n}\n\nfunction formatChoiceCandidate(candidate: RankedSentenceCandidate): string {\n return [\n candidate.occurrenceId,\n `prev: ${formatChoiceText(candidate.prevText)}`,\n `text: ${formatChoiceText(candidate.text)}`,\n `next: ${formatChoiceText(candidate.nextText)}`,\n ].join(\"\\n\");\n}\n\nfunction formatChoiceText(text: string): string {\n const collapsed = text.replace(/\\s+/gu, \" \").trim();\n\n return collapsed === \"\" ? \"(none)\" : collapsed;\n}\n\nfunction toChoiceFieldName(value: string): ChoiceFieldName | undefined {\n return value === \"evidence\" ||\n value === \"start_anchor\" ||\n value === \"end_anchor\"\n ? value\n : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction formatError(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n}\n\nfunction isParsedJsonValidationFailure(error: unknown): boolean {\n return (\n error instanceof GuaranteedParseValidationError &&\n error.cause instanceof ParsedJsonError\n );\n}\n\nfunction createSentenceKey(sentenceId: SentenceId): string {\n return sentenceId.join(\":\");\n}\n\nfunction createMembershipRecord(\n values: readonly (number | string)[],\n): Readonly<Record<string, true>> {\n const record = createEmptyRecord<true>();\n\n for (const value of values) {\n record[String(value)] = true;\n }\n\n return record;\n}\n\nfunction hasMembership(\n record: Readonly<Record<string, true>>,\n value: number | string,\n): boolean {\n return hasIndexedValue(record, String(value));\n}\n\nfunction hasIndexedValue<TValue>(\n record: Readonly<Record<string, TValue>>,\n key: string,\n): boolean {\n return Object.hasOwn(record, key);\n}\n\nfunction createEmptyRecord<TValue>(): Record<string, TValue> {\n return Object.create(null) as Record<string, TValue>;\n}\n\nfunction expectSingleSpan(spans: readonly TextSpan[]): TextSpan | undefined {\n return spans.length === 1 ? spans[0] : undefined;\n}\n\nfunction createEvidenceSelectionList(\n evidence: RawChunkEvidence,\n): EvidenceSelectionList | undefined {\n if (Array.isArray(evidence)) {\n return evidence.map(createEvidenceSelection);\n }\n\n const hasSelectionEvidence =\n typeof evidence.quote === \"string\" ||\n typeof evidence.sentence_id === \"string\";\n\n return hasSelectionEvidence ? createEvidenceSelection(evidence) : undefined;\n}\n\nfunction createEvidenceSelection(evidence: {\n readonly quote?: unknown;\n readonly sentence_id?: unknown;\n}): {\n readonly quote?: string;\n readonly sentence_id?: string;\n} {\n return {\n ...(typeof evidence.quote === \"string\" ? { quote: evidence.quote } : {}),\n ...(typeof evidence.sentence_id === \"string\"\n ? { sentence_id: evidence.sentence_id }\n : {}),\n };\n}\n\nfunction toEvidenceResolutionFailure(\n failure: {\n readonly candidates: readonly EvidenceSelectionCandidate[];\n readonly code: string;\n readonly message: string;\n },\n fieldName: string,\n): EvidenceResolutionFailure {\n return {\n candidates: failure.candidates.map(toRankedSentenceCandidate),\n code: failure.code,\n fieldName,\n message: failure.message,\n };\n}\n\nfunction toRankedSentenceCandidate(\n candidate: EvidenceSelectionCandidate,\n): RankedSentenceCandidate {\n return {\n exactNormalized: candidate.exactNormalized,\n exactRaw: candidate.exactRaw,\n exactSubstring: candidate.exactSubstring,\n index: candidate.index,\n nextText: candidate.nextText,\n occurrenceId: candidate.occurrenceId,\n prevText: candidate.prevText,\n score: candidate.score,\n sentenceId: candidate.sentence.sentenceId,\n text: candidate.sentence.text,\n };\n}\n","import type { SentenceId } from \"../../document/index.js\";\nimport type { ChunkExtractionSentence } from \"./types.js\";\n\nconst SENTENCE_SEPARATOR = \" \";\n\nconst STANDARDIZATION_MAP = {\n '\"': \""\",\n \"\\\\\": \"\\",\n} as const;\n\nexport interface TextSpan {\n readonly length: number;\n readonly offset: number;\n}\n\nexport interface ProjectedFragmentSentence {\n readonly endOffset: number;\n readonly projectedText: string;\n readonly rawText: string;\n readonly sentenceId: SentenceId;\n readonly startOffset: number;\n readonly wordsCount: number;\n}\n\nexport class FragmentProjection {\n readonly #projectedText: string;\n readonly #rawText: string;\n readonly #sentences: readonly ProjectedFragmentSentence[];\n\n public constructor(sentences: readonly ChunkExtractionSentence[]) {\n const projectedSentences: ProjectedFragmentSentence[] = [];\n let rawText = \"\";\n let projectedText = \"\";\n let offset = 0;\n\n for (const [index, sentence] of sentences.entries()) {\n if (index > 0) {\n rawText += SENTENCE_SEPARATOR;\n projectedText += SENTENCE_SEPARATOR;\n offset += SENTENCE_SEPARATOR.length;\n }\n\n const projectedSentenceText = projectFragmentText(sentence.text);\n const startOffset = offset;\n const endOffset = startOffset + projectedSentenceText.length;\n\n rawText += sentence.text;\n projectedText += projectedSentenceText;\n projectedSentences.push({\n endOffset,\n projectedText: projectedSentenceText,\n rawText: sentence.text,\n sentenceId: sentence.sentenceId,\n startOffset,\n wordsCount: sentence.wordsCount,\n });\n offset = endOffset;\n }\n\n this.#rawText = rawText;\n this.#projectedText = projectedText;\n this.#sentences = projectedSentences;\n }\n\n public get projectedText(): string {\n return this.#projectedText;\n }\n\n public get rawText(): string {\n return this.#rawText;\n }\n\n public get sentences(): readonly ProjectedFragmentSentence[] {\n return this.#sentences;\n }\n\n public findExactMatches(text: string): readonly TextSpan[] {\n const query = projectFragmentText(text).trim();\n\n if (query === \"\") {\n return [];\n }\n\n const matches: TextSpan[] = [];\n let startIndex = 0;\n\n while (startIndex <= this.#projectedText.length - query.length) {\n const offset = this.#projectedText.indexOf(query, startIndex);\n\n if (offset === -1) {\n break;\n }\n\n matches.push({\n length: query.length,\n offset,\n });\n startIndex = offset + 1;\n }\n\n return matches;\n }\n\n public projectText(text: string): string {\n return projectFragmentText(text);\n }\n\n public resolveSentenceIds(span: TextSpan): SentenceId[] {\n if (span.length <= 0) {\n return [];\n }\n\n const rangeStart = span.offset;\n const rangeEnd = span.offset + span.length;\n\n return this.#sentences\n .filter(\n (sentence) =>\n sentence.endOffset > rangeStart && sentence.startOffset < rangeEnd,\n )\n .map((sentence) => sentence.sentenceId);\n }\n}\n\nexport function projectFragmentText(text: string): string {\n let result = \"\";\n\n for (const char of text) {\n result +=\n STANDARDIZATION_MAP[char as keyof typeof STANDARDIZATION_MAP] ?? char;\n }\n\n return result;\n}\n","import { detect, validateISO2 } from \"tinyld\";\n\nimport { getLanguageCode, type Language } from \"./language.js\";\n\nexport function detectLanguageCode(text: string): string | undefined {\n const normalizedText = text.trim();\n\n if (normalizedText === \"\") {\n return undefined;\n }\n\n try {\n return normalizeLanguageCode(detect(normalizedText));\n } catch {\n return undefined;\n }\n}\n\nexport function getLanguageDetectionCode(language: Language): string {\n return getLanguageCode(language);\n}\n\nfunction normalizeLanguageCode(languageCode: string): string | undefined {\n const normalizedLanguageCode = languageCode.trim().toLowerCase();\n const directLanguageCode = validateISO2(normalizedLanguageCode);\n\n if (directLanguageCode !== \"\") {\n return directLanguageCode;\n }\n\n const baseLanguageCode = normalizedLanguageCode.split(\"-\")[0];\n\n if (baseLanguageCode === undefined) {\n return undefined;\n }\n\n const validatedBaseLanguageCode = validateISO2(baseLanguageCode);\n\n return validatedBaseLanguageCode === \"\"\n ? undefined\n : validatedBaseLanguageCode;\n}\n","import {\n detectLanguageCode,\n getLanguageDetectionCode,\n} from \"../../common/tinyld-language.js\";\nimport { type Language } from \"../../common/language.js\";\n\nexport function needsTranslation(input: {\n content: string;\n label: string;\n targetLanguage: Language;\n}): boolean {\n const targetLanguageCode = getLanguageDetectionCode(input.targetLanguage);\n\n return (\n fieldNeedsTranslation(input.label, targetLanguageCode) ||\n fieldNeedsTranslation(input.content, targetLanguageCode)\n );\n}\n\nfunction fieldNeedsTranslation(\n text: string,\n targetLanguageCode: string,\n): boolean {\n if (text.trim() === \"\") {\n return false;\n }\n\n const detectedLanguageCode = detectLanguageCode(text);\n\n if (detectedLanguageCode === undefined) {\n return true;\n }\n\n return detectedLanguageCode !== targetLanguageCode;\n}\n","export const USER_FOCUSED_PROMPT_TEMPLATE =\n \"topologization/user_focused_extraction\";\nexport const BOOK_COHERENCE_PROMPT_TEMPLATE =\n \"topologization/book_coherence_extraction\";\nexport const EVIDENCE_CHOICE_PROMPT_TEMPLATE = \"topologization/evidence_choice\";\nexport const TRANSLATE_CHUNKS_PROMPT_TEMPLATE =\n \"topologization/translate_chunks\";\n","import { z, type ZodType } from \"zod\";\n\nimport {\n GuaranteedParseValidationError,\n GuaranteedRequestFailureError,\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n} from \"../../guaranteed/index.js\";\nimport type { Language } from \"../../common/language.js\";\nimport {\n EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n normalizeEvidenceDisplayText,\n} from \"../../evidence-selection/index.js\";\nimport type { LLMessage, LLM } from \"../../llm/index.js\";\nimport {\n bookCoherenceResponseSchema,\n ChunkBatchParser,\n ChunkMetadataField,\n type BookCoherenceResponseData,\n type ExtractChunksResult,\n type UserFocusedResponseData,\n userFocusedResponseSchema,\n} from \"./parser.js\";\nimport { FragmentProjection } from \"./fragment-projection.js\";\nimport { projectFragmentText } from \"./fragment-projection.js\";\nimport { needsTranslation } from \"./language.js\";\nimport {\n BOOK_COHERENCE_PROMPT_TEMPLATE,\n EVIDENCE_CHOICE_PROMPT_TEMPLATE,\n TRANSLATE_CHUNKS_PROMPT_TEMPLATE,\n USER_FOCUSED_PROMPT_TEMPLATE,\n} from \"./prompt-templates.js\";\nimport type {\n ChunkBatch,\n ChunkBatchOptions,\n ChunkExtractionSentence,\n ChunkTranslationInput,\n ChunkTranslationOutput,\n ExtractBookCoherenceInput,\n ExtractUserFocusedInput,\n ExtractUserFocusedResult,\n} from \"./types.js\";\n\nconst MAX_CHUNK_REGENERATIONS = 7;\n\ninterface ExtractChunksInput<\n TData extends UserFocusedResponseData | BookCoherenceResponseData,\n> {\n readonly emptyChunkBatch: ChunkBatch;\n readonly messages: readonly LLMessage[];\n readonly metadataField: ChunkMetadataField;\n readonly projection: FragmentProjection;\n readonly schema: ZodType<TData>;\n readonly sentences: readonly ChunkExtractionSentence[];\n readonly validImportanceChunkIds?: readonly number[];\n readonly visibleChunkIds: readonly number[];\n}\n\ninterface ExtractChunksOutput<\n TData extends UserFocusedResponseData | BookCoherenceResponseData,\n> {\n readonly parser: ChunkBatchParser<TData>;\n readonly result: ExtractChunksResult;\n}\nconst TRANSLATED_CHUNK_SCHEMA = z.object({\n content: z.string(),\n id: z.number(),\n label: z.string(),\n});\nconst TRANSLATED_CHUNKS_SCHEMA = z.array(TRANSLATED_CHUNK_SCHEMA);\nconst MAX_TRANSLATION_RETRIES = 3;\n\nexport class ChunkExtractor<S extends string> {\n readonly #extractionGuidance: string;\n readonly #llm: LLM<S>;\n readonly #scopes: ChunkBatchOptions<S>[\"scopes\"];\n readonly #sentenceTextSource: ChunkBatchOptions<S>[\"sentenceTextSource\"];\n readonly #userLanguage: Language | undefined;\n\n public constructor(options: ChunkBatchOptions<S>) {\n this.#extractionGuidance = options.extractionGuidance;\n this.#llm = options.llm;\n this.#scopes = options.scopes;\n this.#sentenceTextSource = options.sentenceTextSource;\n this.#userLanguage = options.userLanguage;\n }\n\n public async extractUserFocused(\n input: ExtractUserFocusedInput,\n ): Promise<ExtractUserFocusedResult> {\n const projection = new FragmentProjection(input.sentences);\n const messages = this.#buildMessages({\n promptTemplateName: USER_FOCUSED_PROMPT_TEMPLATE,\n templateContext: {\n extraction_guidance: this.#extractionGuidance,\n evidence_selection_prompt: EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n user_language: this.#userLanguage,\n working_memory: input.workingMemoryPrompt,\n },\n text: formatEvidenceSelectionSourceText(projection),\n });\n const extraction = await this.#extractChunks({\n emptyChunkBatch: {\n chunks: [],\n links: [],\n orderCorrect: true,\n tempIds: [],\n },\n messages,\n metadataField: ChunkMetadataField.Retention,\n projection,\n schema: userFocusedResponseSchema,\n sentences: input.sentences,\n visibleChunkIds: input.visibleChunkIds,\n });\n\n return {\n chunkBatch: await this.#ensureChunkBatchLanguage(\n extraction.result.chunkBatch,\n extraction.parser,\n ),\n fragmentSummary: extraction.result.fragmentSummary ?? \"\",\n };\n }\n\n public async extractBookCoherence(\n input: ExtractBookCoherenceInput,\n ): Promise<ChunkBatch> {\n const projection = new FragmentProjection(input.sentences);\n const messages = this.#buildMessages({\n promptTemplateName: BOOK_COHERENCE_PROMPT_TEMPLATE,\n templateContext: {\n user_focused_chunks: input.userFocusedChunks.map((chunk) => ({\n content: chunk.content,\n id: chunk.id,\n label: chunk.label,\n })),\n evidence_selection_prompt: EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n user_language: this.#userLanguage,\n working_memory: input.workingMemoryPrompt,\n },\n text: formatEvidenceSelectionSourceText(projection),\n });\n const extraction = await this.#extractChunks({\n emptyChunkBatch: {\n chunks: [],\n importanceAnnotations: [],\n links: [],\n orderCorrect: true,\n tempIds: [],\n },\n messages,\n metadataField: ChunkMetadataField.Importance,\n projection,\n schema: bookCoherenceResponseSchema,\n sentences: input.sentences,\n validImportanceChunkIds: input.userFocusedChunks.map((chunk) => chunk.id),\n visibleChunkIds: input.visibleChunkIds,\n });\n\n return await this.#ensureChunkBatchLanguage(\n extraction.result.chunkBatch,\n extraction.parser,\n );\n }\n\n #buildMessages(input: {\n promptTemplateName: string;\n templateContext: Record<string, unknown>;\n text: string;\n }): LLMessage[] {\n return [\n {\n content: this.#llm.loadSystemPrompt(\n input.promptTemplateName,\n input.templateContext,\n ),\n role: \"system\",\n },\n {\n content: input.text,\n role: \"user\",\n },\n ];\n }\n\n async #extractChunks<\n TData extends UserFocusedResponseData | BookCoherenceResponseData,\n >(input: ExtractChunksInput<TData>): Promise<ExtractChunksOutput<TData>> {\n return await this.#llm.withContext(\n async (context): Promise<ExtractChunksOutput<TData>> => {\n const parser = new ChunkBatchParser<TData>({\n metadataField: input.metadataField,\n projection: input.projection,\n responseIntentClassifierPrompt: this.#llm.loadSystemPrompt(\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n ),\n sentenceTextSource: this.#sentenceTextSource,\n sentences: input.sentences,\n visibleChunkIds: input.visibleChunkIds,\n choiceSystemPrompt: this.#llm.loadSystemPrompt(\n EVIDENCE_CHOICE_PROMPT_TEMPLATE,\n {\n extraction_guidance: this.#extractionGuidance,\n metadata_field: input.metadataField,\n user_language: this.#userLanguage,\n },\n ),\n requestChoice: async (messages, index, maxRetries) =>\n await context.request(messages, {\n retryIndex: index,\n retryMax: maxRetries,\n scope: this.#scopes.choice,\n useCache: false,\n }),\n ...(input.validImportanceChunkIds === undefined\n ? {}\n : {\n validImportanceChunkIds: input.validImportanceChunkIds,\n }),\n });\n\n try {\n const result = await requestGuaranteedJson({\n messages: input.messages,\n schema: input.schema,\n maxRetries: MAX_CHUNK_REGENERATIONS,\n parse: async (data, index, maxRetries) =>\n await parser.parse(data, {\n isLastGenerationAttempt: index >= maxRetries,\n }),\n responseIntentClassifierPrompt: this.#llm.loadSystemPrompt(\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n ),\n request: async (messages, index, maxRetries) =>\n await context.request(messages, {\n retryIndex: index,\n retryMax: maxRetries,\n scope: this.#scopes.extraction,\n }),\n });\n\n return {\n parser,\n result,\n };\n } catch (error) {\n if (isRecoverableChunkExtractionFailure(error)) {\n return {\n parser,\n result: {\n chunkBatch: input.emptyChunkBatch,\n },\n };\n }\n throw error;\n }\n },\n );\n }\n\n /**\n * 对于 AI 来说,原本应该直接生成正确语言的文字和摘要,但因为\n * 强迫 AI 在不同语言之间切换非常困难,经常会出现语言不符合要求的情况。\n *\n * 所以我们的处理方案是:\n * 1. 通过程序找到那些与要求语言不一致的句子\n * 2. 直接再用 AI 把这些句子翻译过去\n *\n * 这是一个保底手段。\n */\n async #ensureChunkBatchLanguage(\n chunkBatch: ChunkBatch,\n parser: ChunkBatchParser<\n UserFocusedResponseData | BookCoherenceResponseData\n >,\n ): Promise<ChunkBatch> {\n if (this.#userLanguage === undefined || chunkBatch.chunks.length === 0) {\n return chunkBatch;\n }\n\n const translationInput: ChunkTranslationInput[] = [];\n\n for (const [index, chunk] of chunkBatch.chunks.entries()) {\n if (\n !needsTranslation({\n content: chunk.content,\n label: chunk.label,\n targetLanguage: this.#userLanguage,\n })\n ) {\n continue;\n }\n\n translationInput.push({\n content: chunk.content,\n id: index,\n label: chunk.label,\n sourceSentences: await parser.getChunkSourceSentences(chunk),\n });\n }\n\n if (translationInput.length === 0) {\n return chunkBatch;\n }\n\n try {\n const translatedChunks = await this.#translateChunks(\n translationInput,\n this.#userLanguage,\n );\n const translatedById = createTranslatedChunkRecord(translatedChunks);\n\n return {\n ...chunkBatch,\n chunks: chunkBatch.chunks.map((chunk, index) => {\n const translated = translatedById[String(index)];\n\n if (translated === undefined) {\n return chunk;\n }\n\n return {\n ...chunk,\n content: translated.content,\n label: translated.label,\n };\n }),\n };\n } catch {\n return chunkBatch;\n }\n }\n\n async #translateChunks(\n chunks: readonly ChunkTranslationInput[],\n userLanguage: Language,\n ): Promise<readonly ChunkTranslationOutput[]> {\n return await requestGuaranteedJson({\n messages: [\n {\n content: this.#llm.loadSystemPrompt(\n TRANSLATE_CHUNKS_PROMPT_TEMPLATE,\n {\n user_language: userLanguage,\n },\n ),\n role: \"system\",\n },\n {\n content: JSON.stringify(chunks, undefined, 2),\n role: \"user\",\n },\n ],\n schema: TRANSLATED_CHUNKS_SCHEMA,\n maxRetries: MAX_TRANSLATION_RETRIES,\n parse: (data) => {\n validateTranslatedChunks(chunks, data);\n return data;\n },\n responseIntentClassifierPrompt: this.#llm.loadSystemPrompt(\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n ),\n request: async (messages, index, maxRetries) =>\n await this.#llm.request(messages, {\n retryIndex: index,\n retryMax: maxRetries,\n scope: this.#scopes.extraction,\n }),\n });\n }\n}\n\nfunction formatEvidenceSelectionSourceText(\n projection: FragmentProjection,\n): string {\n return projection.sentences\n .map(\n (sentence, index) =>\n `S${index + 1}: ${projectFragmentText(\n normalizeEvidenceDisplayText(sentence.rawText),\n )}`,\n )\n .join(\"\\n\");\n}\n\nfunction isParsedJsonValidationFailure(error: unknown): boolean {\n return (\n error instanceof GuaranteedParseValidationError &&\n error.cause instanceof ParsedJsonError\n );\n}\n\nfunction isRecoverableChunkExtractionFailure(error: unknown): boolean {\n return (\n isParsedJsonValidationFailure(error) ||\n error instanceof GuaranteedRequestFailureError\n );\n}\n\nfunction createTranslatedChunkRecord(\n translatedChunks: readonly ChunkTranslationOutput[],\n): Readonly<Record<string, ChunkTranslationOutput>> {\n const record = Object.create(null) as Record<string, ChunkTranslationOutput>;\n\n for (const chunk of translatedChunks) {\n record[String(chunk.id)] = chunk;\n }\n\n return record;\n}\n\nfunction validateTranslatedChunks(\n chunks: readonly ChunkTranslationInput[],\n translatedChunks: readonly ChunkTranslationOutput[],\n): void {\n const issues: string[] = [];\n\n if (translatedChunks.length !== chunks.length) {\n issues.push(\n `Expected ${chunks.length} translated chunk(s), got ${translatedChunks.length}`,\n );\n }\n\n for (const [index, chunk] of chunks.entries()) {\n const translatedChunk = translatedChunks[index];\n\n if (translatedChunk === undefined) {\n issues.push(`Missing translated chunk at position ${index}`);\n continue;\n }\n\n if (translatedChunk.id !== chunk.id) {\n issues.push(\n `Translated chunk at position ${index} must keep id ${chunk.id}, got ${translatedChunk.id}`,\n );\n }\n }\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n}\n","import { ChunkExtractor } from \"./extractor.js\";\nimport type {\n ChunkBatch,\n ChunkBatchOptions,\n ExtractBookCoherenceInput,\n ExtractUserFocusedInput,\n ExtractUserFocusedResult,\n} from \"./types.js\";\n\nexport async function extractUserFocusedChunkBatch<S extends string>(\n options: ChunkBatchOptions<S>,\n input: ExtractUserFocusedInput,\n): Promise<ExtractUserFocusedResult> {\n const extractor = new ChunkExtractor(options);\n\n return await extractor.extractUserFocused(input);\n}\n\nexport async function extractBookCoherenceChunkBatch<S extends string>(\n options: ChunkBatchOptions<S>,\n input: ExtractBookCoherenceInput,\n): Promise<ChunkBatch> {\n const extractor = new ChunkExtractor(options);\n\n return await extractor.extractBookCoherence(input);\n}\n","import type {\n SentenceStreamAdapter,\n SentenceStreamItem,\n TextStream,\n} from \"./types.js\";\n\nconst SENTENCE_BOUNDARY_PATTERN =\n /(?:[.!?。!?…]+|(?:\\r?\\n)\\s*(?:\\r?\\n)?|[.!?。!?…]+[\"'”’」』】)》〉〕〗]*)$/u;\n\nclass IntlSegmenterSentenceStreamAdapter implements SentenceStreamAdapter {\n readonly #graphemeSegmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n readonly #sentenceSegmenter = new Intl.Segmenter(undefined, {\n granularity: \"sentence\",\n });\n readonly #wordSegmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n\n public async *pipe(stream: TextStream): AsyncIterable<SentenceStreamItem> {\n let buffer = \"\";\n let offset = 0;\n\n for await (const chunk of stream) {\n if (chunk === \"\") {\n continue;\n }\n\n buffer += chunk;\n\n const { remainder, sentences } = this.#drainSentences(buffer, false);\n buffer = remainder;\n\n for (const sentenceText of sentences) {\n yield {\n offset,\n text: sentenceText,\n wordsCount: this.#countWords(sentenceText),\n };\n offset += sentenceText.length;\n }\n }\n\n const { sentences } = this.#drainSentences(buffer, true);\n\n for (const sentenceText of sentences) {\n yield {\n offset,\n text: sentenceText,\n wordsCount: this.#countWords(sentenceText),\n };\n offset += sentenceText.length;\n }\n }\n\n #drainSentences(\n buffer: string,\n flushAll: boolean,\n ): {\n readonly remainder: string;\n readonly sentences: readonly string[];\n } {\n if (buffer.trim() === \"\") {\n return {\n remainder: \"\",\n sentences: [],\n };\n }\n\n const segments = Array.from(this.#sentenceSegmenter.segment(buffer));\n\n if (segments.length === 0) {\n return {\n remainder: buffer,\n sentences: [],\n };\n }\n\n if (flushAll || this.#endsWithSentenceBoundary(buffer)) {\n return {\n remainder: \"\",\n sentences: segments\n .map((segment) => segment.segment.trim())\n .filter((segment) => segment !== \"\"),\n };\n }\n\n const lastSegment = segments[segments.length - 1];\n\n if (lastSegment === undefined) {\n return {\n remainder: buffer,\n sentences: [],\n };\n }\n\n return {\n remainder: buffer.slice(lastSegment.index),\n sentences: segments\n .slice(0, -1)\n .map((segment) => segment.segment.trim())\n .filter((segment) => segment !== \"\"),\n };\n }\n\n #countWords(text: string): number {\n let wordsCount = 0;\n\n for (const segment of this.#wordSegmenter.segment(text)) {\n if (\"isWordLike\" in segment && segment.isWordLike) {\n wordsCount += 1;\n }\n }\n\n if (wordsCount > 0) {\n return wordsCount;\n }\n\n return this.#countGraphemes(text);\n }\n\n #countGraphemes(text: string): number {\n let unitCount = 0;\n\n for (const _segment of this.#graphemeSegmenter.segment(text)) {\n unitCount += 1;\n }\n\n return unitCount;\n }\n\n #endsWithSentenceBoundary(buffer: string): boolean {\n return SENTENCE_BOUNDARY_PATTERN.test(buffer.trimEnd());\n }\n}\n\nexport function createDefaultSentenceStreamAdapter(): SentenceStreamAdapter {\n return new IntlSegmenterSentenceStreamAdapter();\n}\n","import { createDefaultSentenceStreamAdapter } from \"./intl-segmenter.js\";\nimport type {\n SegmentTextStreamOptions,\n SentenceStreamItem,\n TextStream,\n} from \"./types.js\";\n\nexport function segmentTextStream(\n stream: TextStream,\n options?: SegmentTextStreamOptions,\n): AsyncIterable<SentenceStreamItem> {\n const adapter = options?.adapter ?? createDefaultSentenceStreamAdapter();\n\n return adapter.pipe(stream);\n}\n","import { Attention } from \"./attention/attention.js\";\nimport {\n extractBookCoherenceChunkBatch,\n extractUserFocusedChunkBatch,\n} from \"./chunk-batch/extract.js\";\nimport { segmentTextStream } from \"./segment/segment.js\";\nimport type { ChunkBatchOptions } from \"./chunk-batch/types.js\";\nimport type {\n ReaderChunk,\n ReaderGraphDelta,\n ReaderOptions,\n ReaderSegment,\n ReaderSentence,\n ReaderTextStream,\n} from \"./types.js\";\n\nexport class Reader<S extends string> {\n readonly #attention: Attention;\n readonly #chunkBatchOptions: ChunkBatchOptions<S>;\n readonly #segmenter: ReaderOptions<S>[\"segmenter\"];\n\n public constructor(options: ReaderOptions<S>) {\n this.#attention = new Attention(\n options.attention.capacity,\n options.attention.generationDecayFactor,\n options.attention.idGenerator,\n );\n this.#chunkBatchOptions = {\n extractionGuidance: options.extractionGuidance,\n llm: options.llm,\n scopes: options.scopes,\n sentenceTextSource: options.sentenceTextSource,\n ...(options.userLanguage === undefined\n ? {}\n : {\n userLanguage: options.userLanguage,\n }),\n };\n this.#segmenter = options.segmenter;\n }\n\n public get capacity(): number {\n return this.#attention.capacity;\n }\n\n public segment(stream: ReaderTextStream): AsyncIterable<ReaderSegment> {\n if (this.#segmenter === undefined) {\n return segmentTextStream(stream);\n }\n\n return segmentTextStream(stream, {\n adapter: this.#segmenter,\n });\n }\n\n public async extractUserFocused(input: {\n readonly sentences: readonly ReaderSentence[];\n readonly text: string;\n }): Promise<{\n readonly delta: ReaderGraphDelta;\n readonly fragmentSummary: string;\n }> {\n const context = this.#attention.createChunkBatchContext();\n const result = await extractUserFocusedChunkBatch(this.#chunkBatchOptions, {\n sentences: input.sentences,\n text: input.text,\n visibleChunkIds: context.visibleChunkIds,\n workingMemoryPrompt: context.workingMemoryPrompt,\n });\n\n return {\n delta: await this.#attention.acceptChunkBatch(result.chunkBatch),\n fragmentSummary: result.fragmentSummary,\n };\n }\n\n public async extractBookCoherence(input: {\n readonly sentences: readonly ReaderSentence[];\n readonly text: string;\n readonly userFocusedChunks: readonly ReaderChunk[];\n }): Promise<ReaderGraphDelta> {\n const context = this.#attention.createChunkBatchContext({\n includeCurrentFragment: false,\n });\n const linkableChunkIds = [\n ...context.visibleChunkIds,\n ...input.userFocusedChunks.map((chunk) => chunk.id),\n ];\n const chunkBatch = await extractBookCoherenceChunkBatch(\n this.#chunkBatchOptions,\n {\n sentences: input.sentences,\n text: input.text,\n userFocusedChunks: input.userFocusedChunks,\n visibleChunkIds: linkableChunkIds,\n workingMemoryPrompt: context.workingMemoryPrompt,\n },\n );\n\n return await this.#attention.acceptChunkBatch(chunkBatch);\n }\n\n public completeFragment(input: {\n readonly allChunks: readonly ReaderChunk[];\n readonly getSuccessorChunkIds: (chunkId: number) => readonly number[];\n }): void {\n this.#attention.completeFragment(input);\n }\n\n public clear(): void {\n this.#attention.clear();\n }\n}\n","export { Reader } from \"./reader.js\";\nexport { segmentTextStream } from \"./segment/segment.js\";\nexport type {\n ReaderChunk,\n ReaderGraphDelta,\n ReaderGraphEdge,\n ReaderImportanceAnnotation,\n ReaderOptions,\n ReaderSegment,\n ReaderSegmenter,\n ReaderSentence,\n ReaderTextStream,\n} from \"./types.js\";\n","import type {\n ChunkRecord,\n ReadonlyDocument,\n SnakeRecord,\n} from \"../document/index.js\";\n\ninterface Clue {\n readonly clueId: number;\n readonly chunks: readonly ChunkRecord[];\n readonly isMerged: boolean;\n readonly label: string;\n readonly sourceSnakeIds: readonly number[];\n readonly weight: number;\n}\n\nexport async function extractCluesFromDocument(input: {\n groupId: number;\n maxClues: number;\n serialId: number;\n document: ReadonlyDocument;\n}): Promise<readonly Clue[]> {\n const snakeIds = await input.document.snakes.listIdsByGroup(\n input.serialId,\n input.groupId,\n );\n const snakes = await Promise.all(\n snakeIds.map(\n async (snakeId) => await input.document.snakes.getById(snakeId),\n ),\n );\n const clues = await Promise.all(\n snakes\n .filter((snake): snake is SnakeRecord => snake !== undefined)\n .map(async (snake) => await convertSnakeToClue(snake, input.document)),\n );\n\n if (clues.length <= input.maxClues) {\n return normalizeClueWeights(clues);\n }\n\n let currentClues = [...clues];\n let mergedClueId = -1;\n\n while (currentClues.length > input.maxClues) {\n currentClues.sort(compareClueByWeightDescending);\n\n const cutoffRank = Math.floor(input.maxClues * 0.75);\n const candidates = currentClues.slice(cutoffRank);\n\n if (candidates.length < 2) {\n break;\n }\n\n const pair = findBestMergePair(candidates);\n const [leftClue, rightClue] =\n pair ?? [...currentClues].sort(compareClueByWeightAscending).slice(0, 2);\n\n if (leftClue === undefined || rightClue === undefined) {\n break;\n }\n\n currentClues = currentClues.filter(\n (clue) => clue !== leftClue && clue !== rightClue,\n );\n currentClues.push(mergeClues(leftClue, rightClue, mergedClueId));\n mergedClueId -= 1;\n }\n\n return normalizeClueWeights(currentClues);\n}\n\nfunction calculateSentenceReduction(leftClue: Clue, rightClue: Clue): number {\n const leftSentenceIndexes = collectSentenceIndexes(leftClue.chunks);\n const rightSentenceIndexes = collectSentenceIndexes(rightClue.chunks);\n const mergedSentenceIndexes = Object.create(null) as Record<string, true>;\n let mergedCount = 0;\n\n for (const sentenceIndex of leftSentenceIndexes) {\n mergedSentenceIndexes[String(sentenceIndex)] = true;\n mergedCount += 1;\n }\n\n for (const sentenceIndex of rightSentenceIndexes) {\n const sentenceKey = String(sentenceIndex);\n\n if (mergedSentenceIndexes[sentenceKey] === true) {\n continue;\n }\n\n mergedSentenceIndexes[sentenceKey] = true;\n mergedCount += 1;\n }\n\n return leftSentenceIndexes.length + rightSentenceIndexes.length - mergedCount;\n}\n\nfunction collectSentenceIndexes(chunks: readonly ChunkRecord[]): number[] {\n const sentenceIndexRecord = Object.create(null) as Record<string, true>;\n const sentenceIndexes: number[] = [];\n\n for (const chunk of chunks) {\n for (const sentenceId of chunk.sentenceIds) {\n const sentenceIndex = sentenceId[1];\n const sentenceKey = String(sentenceIndex);\n\n if (sentenceIndexRecord[sentenceKey] === true) {\n continue;\n }\n\n sentenceIndexRecord[sentenceKey] = true;\n sentenceIndexes.push(sentenceIndex);\n }\n }\n\n sentenceIndexes.sort(compareNumber);\n\n return sentenceIndexes;\n}\n\nfunction compareClueByWeightAscending(left: Clue, right: Clue): number {\n return left.weight - right.weight;\n}\n\nfunction compareClueByWeightDescending(left: Clue, right: Clue): number {\n return right.weight - left.weight;\n}\n\nfunction compareChunkBySentenceId(\n left: ChunkRecord,\n right: ChunkRecord,\n): number {\n const [leftSerialId, leftSentenceIndex] = left.sentenceId;\n const [rightSerialId, rightSentenceIndex] = right.sentenceId;\n\n if (leftSerialId !== rightSerialId) {\n return leftSerialId - rightSerialId;\n }\n\n return leftSentenceIndex - rightSentenceIndex;\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nasync function convertSnakeToClue(\n snake: SnakeRecord,\n document: ReadonlyDocument,\n): Promise<Clue> {\n const chunkIds = await document.snakeChunks.listChunkIds(snake.id);\n const chunks = (\n await Promise.all(\n chunkIds.map(async (chunkId) => await document.chunks.getById(chunkId)),\n )\n ).filter((chunk): chunk is ChunkRecord => chunk !== undefined);\n\n return {\n clueId: snake.id,\n chunks,\n isMerged: false,\n label: `${snake.firstLabel} -> ${snake.lastLabel}`,\n sourceSnakeIds: [snake.id],\n weight: snake.weight,\n };\n}\n\nfunction findBestMergePair(\n clues: readonly Clue[],\n): readonly [Clue, Clue] | undefined {\n let bestPair: [Clue, Clue] | undefined;\n let bestReduction = -1;\n\n for (let leftIndex = 0; leftIndex < clues.length; leftIndex += 1) {\n const leftClue = clues[leftIndex];\n\n if (leftClue === undefined) {\n continue;\n }\n\n for (\n let rightIndex = leftIndex + 1;\n rightIndex < clues.length;\n rightIndex += 1\n ) {\n const rightClue = clues[rightIndex];\n\n if (rightClue === undefined) {\n continue;\n }\n\n const reduction = calculateSentenceReduction(leftClue, rightClue);\n\n if (reduction <= bestReduction) {\n continue;\n }\n\n bestReduction = reduction;\n bestPair = [leftClue, rightClue];\n }\n }\n\n return bestPair;\n}\n\nfunction mergeClues(leftClue: Clue, rightClue: Clue, clueId: number): Clue {\n const sourceSnakeIds = [\n ...leftClue.sourceSnakeIds,\n ...rightClue.sourceSnakeIds,\n ].sort(compareNumber);\n const chunks = [...leftClue.chunks, ...rightClue.chunks].sort(\n compareChunkBySentenceId,\n );\n\n return {\n clueId,\n chunks,\n isMerged: true,\n label: `Merged minor clues (${sourceSnakeIds.length})`,\n sourceSnakeIds,\n weight: leftClue.weight + rightClue.weight,\n };\n}\n\nfunction normalizeClueWeights(clues: readonly Clue[]): readonly Clue[] {\n const totalWeight = clues.reduce((sum, clue) => sum + clue.weight, 0);\n const normalizedClues = clues.map((clue) => ({\n ...clue,\n weight: totalWeight === 0 ? 0 : clue.weight / totalWeight,\n }));\n\n normalizedClues.sort(compareClueByWeightDescending);\n\n return normalizedClues;\n}\n\nexport type { Clue };\n","export const CLUE_REVIEWER_PROMPT_TEMPLATE = \"editor/clue_reviewer\";\nexport const CLUE_REVIEWER_GENERATOR_PROMPT_TEMPLATE =\n \"editor/clue_reviewer_generator\";\nexport const REVISION_FEEDBACK_PROMPT_TEMPLATE = \"editor/revision_feedback\";\nexport const TEXT_COMPRESSOR_PROMPT_TEMPLATE = \"editor/text_compressor\";\n","import type { LLMessage, LLM } from \"../llm/index.js\";\nimport { TEXT_COMPRESSOR_PROMPT_TEMPLATE } from \"./prompt-templates.js\";\n\nexport class CompressionRequester<S extends string> {\n readonly #compressionRatio: number;\n readonly #llm: LLM<S>;\n readonly #scope: S;\n readonly #userLanguage: string | undefined;\n\n public constructor(\n llm: LLM<S>,\n scope: S,\n compressionRatio: number,\n userLanguage?: string,\n ) {\n this.#compressionRatio = compressionRatio;\n this.#llm = llm;\n this.#scope = scope;\n this.#userLanguage = userLanguage;\n }\n\n public async request(input: {\n markedText: string;\n previousCompressedText?: string;\n revisionFeedback?: string;\n targetLength: number;\n }): Promise<string> {\n const acceptableMin = Math.floor(input.targetLength * 0.85);\n const acceptableMax = Math.floor(input.targetLength * 1.15);\n const systemPrompt = this.#llm.loadSystemPrompt(\n TEXT_COMPRESSOR_PROMPT_TEMPLATE,\n {\n acceptable_max: acceptableMax,\n acceptable_min: acceptableMin,\n compression_ratio: Math.floor(this.#compressionRatio * 100),\n original_length: input.markedText.length,\n target_length: input.targetLength,\n user_language: this.#userLanguage,\n },\n );\n const messages = buildCompressionMessages(\n {\n markedText: input.markedText,\n systemPrompt,\n },\n input.previousCompressedText,\n input.revisionFeedback,\n );\n\n return (\n await this.#llm.request(messages, {\n scope: this.#scope,\n })\n ).trim();\n }\n}\n\nfunction buildCompressionMessages(\n input: {\n markedText: string;\n systemPrompt: string;\n },\n previousCompressedText?: string,\n revisionFeedback?: string,\n): LLMessage[] {\n const messages: LLMessage[] = [\n {\n content: input.systemPrompt,\n role: \"system\",\n },\n {\n content: input.markedText,\n role: \"user\",\n },\n ];\n\n if (previousCompressedText !== undefined && revisionFeedback !== undefined) {\n messages.push(\n {\n content: previousCompressedText,\n role: \"assistant\",\n },\n {\n content: revisionFeedback,\n role: \"user\",\n },\n );\n }\n\n return messages;\n}\n","import {\n createEnumValueAsserter,\n createEnumValueGuard,\n} from \"../utils/enum.js\";\n\nexport enum ReviewSeverity {\n Critical = \"critical\",\n Major = \"major\",\n Minor = \"minor\",\n}\n\nexport const isReviewSeverity = createEnumValueGuard(ReviewSeverity);\nexport const expectReviewSeverity = createEnumValueAsserter(\n ReviewSeverity,\n \"review severity\",\n);\n\nexport interface ClueReviewerInfo {\n readonly clueId: number;\n readonly label: string;\n readonly reviewerInfo: string;\n readonly weight: number;\n}\n\nexport interface ReviewIssue {\n readonly problem: string;\n readonly severity: ReviewSeverity;\n readonly suggestion: string;\n}\n\nexport interface ReviewResult {\n readonly clueId: number;\n readonly issues: readonly ReviewIssue[];\n readonly weight: number;\n}\n\nexport interface CompressionVersion {\n readonly iteration: number;\n readonly reviews: readonly ReviewResult[];\n readonly score: number;\n readonly text: string;\n}\n","import type { LLM } from \"../llm/index.js\";\nimport { REVISION_FEEDBACK_PROMPT_TEMPLATE } from \"./prompt-templates.js\";\nimport type { CompressionVersion, ReviewIssue, ReviewResult } from \"./types.js\";\nimport { ReviewSeverity } from \"./types.js\";\n\nconst REVIEW_SEVERITY_VALUE: Readonly<Record<ReviewSeverity, number>> =\n Object.freeze({\n [ReviewSeverity.Critical]: 9,\n [ReviewSeverity.Major]: 3,\n [ReviewSeverity.Minor]: 1,\n });\n\nexport function calculateScore(reviews: readonly ReviewResult[]): number {\n let totalScore = 0;\n\n for (const review of reviews) {\n for (const issue of review.issues) {\n totalScore += REVIEW_SEVERITY_VALUE[issue.severity] * review.weight;\n }\n }\n\n return totalScore;\n}\n\nexport function createRevisionFeedback<S extends string>(input: {\n llm: LLM<S>;\n reviews: readonly ReviewResult[];\n}): string {\n const allIssues = collectIssues(input.reviews);\n const visibleIssues = allIssues.slice(0, 9);\n const hiddenCount = allIssues.length - visibleIssues.length;\n const issueLines: string[] = [];\n\n for (let index = 0; index < visibleIssues.length; index += 1) {\n const issue = visibleIssues[index];\n\n if (issue === undefined) {\n continue;\n }\n\n issueLines.push(\n `${index + 1}. [${issue.severity.toUpperCase()}]`,\n ` Problem: ${issue.problem}`,\n );\n\n if (issue.suggestion !== \"\") {\n issueLines.push(` Suggestion: ${issue.suggestion}`);\n }\n\n issueLines.push(\"\");\n }\n\n if (hiddenCount > 0) {\n issueLines.push(\n `... and ${hiddenCount} more issues hidden (lower priority)`,\n );\n }\n\n return input.llm.loadSystemPrompt(REVISION_FEEDBACK_PROMPT_TEMPLATE, {\n issues_description: issueLines.join(\"\\n\"),\n });\n}\n\nexport function formatIssuesForLog(reviews: readonly ReviewResult[]): string {\n const issues = collectIssues(reviews);\n\n if (issues.length === 0) {\n return \"No issues found - all reviewers are satisfied.\\n\";\n }\n\n const lines: string[] = [];\n\n for (let index = 0; index < issues.length; index += 1) {\n const issue = issues[index];\n\n if (issue === undefined) {\n continue;\n }\n\n lines.push(\n `${index + 1}. [${issue.severity.toUpperCase()}]`,\n ` Problem: ${issue.problem}`,\n );\n\n if (issue.suggestion !== \"\") {\n lines.push(` Suggestion: ${issue.suggestion}`);\n }\n\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n\nexport function pickBestVersion(\n versions: readonly CompressionVersion[],\n): CompressionVersion {\n const bestVersion = versions.reduce<CompressionVersion | undefined>(\n (currentBest, version) => {\n if (currentBest === undefined || version.score < currentBest.score) {\n return version;\n }\n\n return currentBest;\n },\n undefined,\n );\n\n if (bestVersion === undefined) {\n throw new Error(\"Compression failed: no versions generated\");\n }\n\n return bestVersion;\n}\n\nfunction collectIssues(\n reviews: readonly ReviewResult[],\n): Array<ReviewIssue & { readonly weight: number }> {\n const issues = reviews.flatMap((review) =>\n review.issues.map((issue) => ({\n ...issue,\n weight: review.weight,\n })),\n );\n\n issues.sort((left, right) => {\n const severityDelta =\n REVIEW_SEVERITY_VALUE[right.severity] -\n REVIEW_SEVERITY_VALUE[left.severity];\n\n if (severityDelta !== 0) {\n return severityDelta;\n }\n\n return right.weight - left.weight;\n });\n\n return issues;\n}\n","import {\n detectLanguageCode,\n getLanguageDetectionCode,\n} from \"../common/tinyld-language.js\";\nimport { type Language } from \"../common/language.js\";\nimport { ReviewSeverity, type ReviewResult } from \"./types.js\";\n\nexport interface LanguageReview {\n readonly detectedLanguageCode: string;\n readonly review: ReviewResult;\n readonly targetLanguageCode: string;\n}\n\nexport function checkOutputLanguage(input: {\n compressedText: string;\n userLanguage?: Language;\n}): LanguageReview | undefined {\n if (input.userLanguage === undefined) {\n return undefined;\n }\n\n const targetLanguageCode = getLanguageDetectionCode(input.userLanguage);\n\n const detectedLanguageCode = detectLanguageCode(input.compressedText);\n\n if (\n detectedLanguageCode === undefined ||\n detectedLanguageCode === targetLanguageCode\n ) {\n return undefined;\n }\n\n return {\n detectedLanguageCode,\n review: {\n clueId: -1,\n issues: [\n {\n problem: `Output language error: detected ${detectedLanguageCode}, but ${targetLanguageCode} (${input.userLanguage}) is required.`,\n severity: ReviewSeverity.Critical,\n suggestion: `Please translate the entire compressed text to ${input.userLanguage}. Maintain all information integrity and ensure the translation sounds natural and native, not machine-translated.`,\n },\n ],\n weight: 1,\n },\n targetLanguageCode,\n };\n}\n","import { appendFile, writeFile } from \"fs/promises\";\nimport { allocateArtifactPath } from \"../common/logging.js\";\n\nimport type { Language } from \"../common/language.js\";\nimport type { Clue } from \"./clue.js\";\nimport { formatIssuesForLog } from \"./feedback.js\";\nimport type { CompressionVersion, ReviewResult } from \"./types.js\";\n\nexport class CompressionLog {\n readonly #compressionRatio: number;\n readonly #groupId: number;\n readonly #logDirPath: string | undefined;\n readonly #maxIterations: number;\n readonly #serialId: number;\n #filePath: string | undefined;\n\n public constructor(\n serialId: number,\n groupId: number,\n options: {\n readonly compressionRatio: number;\n readonly logDirPath?: string;\n readonly maxIterations: number;\n },\n ) {\n this.#compressionRatio = options.compressionRatio;\n this.#groupId = groupId;\n this.#logDirPath = options.logDirPath;\n this.#maxIterations = options.maxIterations;\n this.#serialId = serialId;\n }\n\n public async appendCompressionResult(input: {\n compressedText: string;\n thinkingText: string;\n }): Promise<void> {\n if (this.#filePath === undefined) {\n return;\n }\n\n const parts: string[] = [];\n\n if (input.thinkingText !== \"\") {\n parts.push(\n \"Thinking:\",\n \"-\".repeat(80),\n input.thinkingText,\n \"-\".repeat(80),\n \"\",\n );\n }\n\n parts.push(\n `Compressed Text (${input.compressedText.length} characters):`,\n \"-\".repeat(80),\n input.compressedText,\n \"-\".repeat(80),\n \"\",\n \"\",\n );\n\n await appendFile(this.#filePath, `${parts.join(\"\\n\")}\\n`, \"utf8\");\n }\n\n public async appendFinalSelection(\n bestVersion: CompressionVersion,\n originalLength: number,\n ): Promise<void> {\n if (this.#filePath === undefined) {\n return;\n }\n\n const parts = [\n \"\",\n \"=\".repeat(80),\n \"FINAL SELECTION\",\n \"=\".repeat(80),\n \"\",\n `Selected: Iteration ${bestVersion.iteration}/${this.#maxIterations}`,\n `Score: ${bestVersion.score.toFixed(2)}`,\n `Length: ${bestVersion.text.length} characters`,\n `Compression ratio: ${(bestVersion.text.length / originalLength).toFixed(1)}%`,\n \"\",\n \"=\".repeat(80),\n \"\",\n ];\n\n if (bestVersion.score > 0) {\n parts.push(\n \"REMAINING UNRESOLVED ISSUES\",\n \"=\".repeat(80),\n \"\",\n formatIssuesForLog(bestVersion.reviews),\n \"=\".repeat(80),\n \"\",\n );\n }\n\n await appendFile(this.#filePath, `${parts.join(\"\\n\")}\\n`, \"utf8\");\n }\n\n public async appendIterationHeader(\n iteration: number,\n revisionFeedback: string | undefined,\n ): Promise<void> {\n if (this.#filePath === undefined) {\n return;\n }\n\n const parts = [\n \"\",\n \"=\".repeat(80),\n `ITERATION ${iteration}/${this.#maxIterations}`,\n \"=\".repeat(80),\n \"\",\n ];\n\n if (revisionFeedback !== undefined && revisionFeedback.trim() !== \"\") {\n parts.push(\n \"Revision Feedback:\",\n \"-\".repeat(80),\n revisionFeedback,\n \"-\".repeat(80),\n \"\",\n );\n }\n\n await appendFile(this.#filePath, `${parts.join(\"\\n\")}\\n`, \"utf8\");\n }\n\n public async appendLanguageMismatch(input: {\n detectedLanguageCode: string;\n review: ReviewResult;\n targetLanguageCode: string;\n userLanguage: Language | undefined;\n }): Promise<void> {\n if (this.#filePath === undefined) {\n return;\n }\n\n const issue = input.review.issues[0];\n\n if (issue === undefined) {\n return;\n }\n\n const parts = [\n \"\",\n \"!\".repeat(80),\n \"LANGUAGE MISMATCH DETECTED\",\n \"!\".repeat(80),\n `Expected: ${input.targetLanguageCode} (${input.userLanguage ?? \"unknown\"})`,\n `Detected: ${input.detectedLanguageCode}`,\n `Issue: ${issue.problem}`,\n `Suggestion: ${issue.suggestion}`,\n \"!\".repeat(80),\n \"\",\n ];\n\n await appendFile(this.#filePath, `${parts.join(\"\\n\")}\\n`, \"utf8\");\n }\n\n public async initialize(clues: readonly Clue[]): Promise<void> {\n if (this.#logDirPath === undefined) {\n return;\n }\n\n const timestamp = formatTimestamp(new Date());\n\n this.#filePath = allocateArtifactPath({\n category: \"editor\",\n logDirPath: this.#logDirPath,\n prefix: `compression-serial-${this.#serialId}-group-${this.#groupId}`,\n });\n\n if (this.#filePath === undefined) {\n return;\n }\n\n await writeFile(\n this.#filePath,\n [\n \"=== Text Compression Log ===\",\n `Serial: ${this.#serialId}, Group: ${this.#groupId}`,\n `Started at: ${timestamp}`,\n `Compression ratio target: ${Math.round(this.#compressionRatio * 100)}%`,\n `Max iterations: ${this.#maxIterations}`,\n \"\",\n \"\",\n formatChunkHierarchy(clues, this.#groupId, this.#serialId),\n \"\",\n ].join(\"\\n\"),\n \"utf8\",\n );\n }\n}\n\nfunction formatChunkHierarchy(\n clues: readonly Clue[],\n groupId: number,\n serialId: number,\n): string {\n const parts = [\n \"=\".repeat(80),\n `CHUNK HIERARCHY - Serial ${serialId}, Group ${groupId}`,\n \"=\".repeat(80),\n \"\",\n ];\n\n for (let clueIndex = 0; clueIndex < clues.length; clueIndex += 1) {\n const clue = clues[clueIndex];\n\n if (clue === undefined) {\n continue;\n }\n\n parts.push(\n `Clue #${clueIndex + 1} (ID: ${clue.clueId})`,\n `|- Weight: ${clue.weight.toFixed(4)}`,\n `|- Label: ${clue.label}`,\n `|- Source snakes: ${clue.sourceSnakeIds.join(\", \")}`,\n `|- Merged: ${clue.isMerged ? \"yes\" : \"no\"}`,\n `\\\\- Chunks: ${clue.chunks.length}`,\n \"\",\n );\n\n for (let chunkIndex = 0; chunkIndex < clue.chunks.length; chunkIndex += 1) {\n const chunk = clue.chunks[chunkIndex];\n\n if (chunk === undefined) {\n continue;\n }\n\n const contentPreview =\n chunk.content.length > 60\n ? `${chunk.content.slice(0, 60)}...`\n : chunk.content;\n\n parts.push(\n ` - Chunk ${chunkIndex + 1}/${clue.chunks.length} (ID: ${chunk.id})`,\n ` Label: ${chunk.label}`,\n ` Retention: ${chunk.retention ?? \"N/A\"}`,\n ` Importance: ${chunk.importance ?? \"N/A\"}`,\n ` Content: ${contentPreview}`,\n \"\",\n );\n }\n }\n\n parts.push(\"=\".repeat(80), \"\");\n\n return parts.join(\"\\n\");\n}\n\nfunction formatTimestamp(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n const hours = String(date.getHours()).padStart(2, \"0\");\n const minutes = String(date.getMinutes()).padStart(2, \"0\");\n const seconds = String(date.getSeconds()).padStart(2, \"0\");\n\n return `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`;\n}\n","import {\n ChunkRetention,\n type ChunkImportance,\n type ChunkRecord,\n type FragmentRecord,\n type ReadonlySerialFragments,\n} from \"../document/index.js\";\nexport async function formatClueAsBook(input: {\n chunks: readonly ChunkRecord[];\n serialFragments: ReadonlySerialFragments;\n fullMarkup?: boolean;\n wrapHighRetention?: boolean;\n}): Promise<string> {\n return await formatChunksAsBook({\n chunks: input.chunks,\n segmentStartIndexes: await listSegmentStartIndexesCoveringChunks(\n input.chunks,\n input.serialFragments,\n ),\n serialFragments: input.serialFragments,\n ...(input.fullMarkup === undefined\n ? {}\n : {\n fullMarkup: input.fullMarkup,\n }),\n ...(input.wrapHighRetention === undefined\n ? {}\n : {\n wrapHighRetention: input.wrapHighRetention,\n }),\n });\n}\n\nexport async function formatChunksAsBook(input: {\n chunks: readonly ChunkRecord[];\n segmentStartIndexes: readonly number[];\n serialFragments: ReadonlySerialFragments;\n fullMarkup?: boolean;\n wrapHighRetention?: boolean;\n}): Promise<string> {\n if (input.segmentStartIndexes.length === 0) {\n return \"\";\n }\n\n const chunkCoverage = createChunkCoverage(input.chunks);\n const segmentStartIndexes = [...input.segmentStartIndexes].sort(\n compareNumber,\n );\n const allSegmentStartIndexes = [\n ...(await input.serialFragments.listFragmentIds()),\n ].sort(compareNumber);\n const fragments = await loadFragments(\n listRequiredSegmentStartIndexes(\n segmentStartIndexes,\n allSegmentStartIndexes,\n ),\n input.serialFragments,\n );\n const resultParts: string[] = [];\n\n for (let index = 0; index < segmentStartIndexes.length; index += 1) {\n const startSentenceIndex = segmentStartIndexes[index];\n\n if (startSentenceIndex === undefined) {\n continue;\n }\n\n const fragment = fragments[String(startSentenceIndex)];\n\n if (fragment === undefined) {\n continue;\n }\n\n if (index > 0) {\n const previousStartSentenceIndex = segmentStartIndexes[index - 1];\n\n if (\n previousStartSentenceIndex !== undefined &&\n startSentenceIndex > previousStartSentenceIndex\n ) {\n const skippedSummary = collectSkippedSummary({\n fragments,\n skippedStartIndexes: listSkippedSegmentStartIndexes(\n allSegmentStartIndexes,\n previousStartSentenceIndex,\n startSentenceIndex,\n ),\n });\n\n if (skippedSummary !== \"\") {\n resultParts.push(skippedSummary);\n }\n }\n }\n\n resultParts.push(\n buildFragmentMarkup({\n chunkCoverage,\n fragment,\n fullMarkup: input.fullMarkup ?? false,\n wrapHighRetention: input.wrapHighRetention ?? false,\n }),\n );\n }\n\n return resultParts.join(\"\\n\\n\");\n}\n\nfunction buildFragmentMarkup(input: {\n chunkCoverage: Record<string, readonly ChunkRecord[] | undefined>;\n fragment: FragmentRecord;\n fullMarkup: boolean;\n wrapHighRetention: boolean;\n}): string {\n const sentenceRanges: Array<{\n readonly endIndex: number;\n readonly startIndex: number;\n readonly chunkAttributes?: ChunkAttributes;\n }> = [];\n const { fragment } = input;\n let sentenceIndex = 0;\n\n while (sentenceIndex < fragment.sentences.length) {\n const sentence = fragment.sentences[sentenceIndex];\n\n if (sentence === undefined) {\n sentenceIndex += 1;\n continue;\n }\n\n const sentenceId = createSentenceKey(\n fragment.serialId,\n fragment.fragmentId + sentenceIndex,\n );\n const coveredChunks = input.chunkCoverage[sentenceId];\n\n if (coveredChunks === undefined) {\n sentenceRanges.push({\n endIndex: sentenceIndex + 1,\n startIndex: sentenceIndex,\n });\n sentenceIndex += 1;\n continue;\n }\n\n const chunkAttributes = mergeChunkAttributes(coveredChunks);\n let endIndex = sentenceIndex + 1;\n\n while (endIndex < fragment.sentences.length) {\n const nextSentenceId = createSentenceKey(\n fragment.serialId,\n fragment.fragmentId + endIndex,\n );\n const nextCoveredChunks = input.chunkCoverage[nextSentenceId];\n\n if (nextCoveredChunks === undefined) {\n break;\n }\n\n const nextAttributes = mergeChunkAttributes(nextCoveredChunks);\n\n if (nextAttributes.label !== chunkAttributes.label) {\n break;\n }\n\n endIndex += 1;\n }\n\n sentenceRanges.push({\n chunkAttributes,\n endIndex,\n startIndex: sentenceIndex,\n });\n sentenceIndex = endIndex;\n }\n\n const parts = sentenceRanges.map((range) =>\n renderSentenceRange({\n fragment,\n fullMarkup: input.fullMarkup,\n range,\n wrapHighRetention: input.wrapHighRetention,\n }),\n );\n\n return parts.join(\" \");\n}\n\nfunction collectSkippedSummary(input: {\n fragments: Record<string, FragmentRecord | undefined>;\n skippedStartIndexes: readonly number[];\n}): string {\n const summaries: string[] = [];\n\n for (const startSentenceIndex of input.skippedStartIndexes) {\n const summary =\n input.fragments[String(startSentenceIndex)]?.summary?.trim() ?? \"\";\n\n if (summary !== \"\") {\n summaries.push(summary);\n }\n }\n\n return summaries.join(\" \");\n}\n\nfunction createChunkCoverage(\n chunks: readonly ChunkRecord[],\n): Record<string, readonly ChunkRecord[] | undefined> {\n const coverage = Object.create(null) as Record<\n string,\n readonly ChunkRecord[] | undefined\n >;\n\n for (const chunk of chunks) {\n for (const sentenceId of chunk.sentenceIds) {\n const coverageKey = createSentenceKey(...sentenceId);\n const coveredChunks = coverage[coverageKey];\n\n coverage[coverageKey] =\n coveredChunks === undefined ? [chunk] : [...coveredChunks, chunk];\n }\n }\n\n return coverage;\n}\n\nfunction createSentenceKey(serialId: number, sentenceIndex: number): string {\n return `${serialId}:${sentenceIndex}`;\n}\n\nfunction listRequiredSegmentStartIndexes(\n selectedStartIndexes: readonly number[],\n allStartIndexes: readonly number[],\n): number[] {\n const required = new Set(selectedStartIndexes);\n\n for (let index = 1; index < selectedStartIndexes.length; index += 1) {\n const previousStartIndex = selectedStartIndexes[index - 1];\n const nextStartIndex = selectedStartIndexes[index];\n\n if (previousStartIndex === undefined || nextStartIndex === undefined) {\n continue;\n }\n\n for (const skippedStartIndex of listSkippedSegmentStartIndexes(\n allStartIndexes,\n previousStartIndex,\n nextStartIndex,\n )) {\n required.add(skippedStartIndex);\n }\n }\n\n return [...required].sort(compareNumber);\n}\n\nfunction listSkippedSegmentStartIndexes(\n allStartIndexes: readonly number[],\n previousStartIndex: number,\n nextStartIndex: number,\n): number[] {\n return allStartIndexes.filter(\n (startIndex) =>\n startIndex > previousStartIndex && startIndex < nextStartIndex,\n );\n}\n\nasync function listSegmentStartIndexesCoveringChunks(\n chunks: readonly ChunkRecord[],\n serialFragments: ReadonlySerialFragments,\n): Promise<number[]> {\n const allSegmentStartIndexes = [\n ...(await serialFragments.listFragmentIds()),\n ].sort(compareNumber);\n const startIndexRecord = Object.create(null) as Record<string, true>;\n const startIndexes: number[] = [];\n\n for (const chunk of chunks) {\n for (const sentenceId of chunk.sentenceIds) {\n const startSentenceIndex = findContainingSegmentStartIndex(\n allSegmentStartIndexes,\n sentenceId[1],\n );\n\n if (startSentenceIndex === undefined) {\n continue;\n }\n\n const startIndexKey = String(startSentenceIndex);\n\n if (startIndexRecord[startIndexKey] === true) {\n continue;\n }\n\n startIndexRecord[startIndexKey] = true;\n startIndexes.push(startSentenceIndex);\n }\n }\n\n startIndexes.sort(compareNumber);\n\n return startIndexes;\n}\n\nfunction findContainingSegmentStartIndex(\n segmentStartIndexes: readonly number[],\n sentenceIndex: number,\n): number | undefined {\n let containingStartIndex: number | undefined;\n\n for (const startIndex of segmentStartIndexes) {\n if (startIndex > sentenceIndex) {\n break;\n }\n\n containingStartIndex = startIndex;\n }\n\n return containingStartIndex;\n}\n\nasync function loadFragments(\n startSentenceIndexes: readonly number[],\n serialFragments: ReadonlySerialFragments,\n): Promise<Record<string, FragmentRecord | undefined>> {\n const fragments = Object.create(null) as Record<\n string,\n FragmentRecord | undefined\n >;\n\n await Promise.all(\n startSentenceIndexes.map(async (startSentenceIndex) => {\n fragments[String(startSentenceIndex)] =\n await serialFragments.getFragment(startSentenceIndex);\n }),\n );\n\n return fragments;\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\ninterface ChunkAttributes {\n readonly importance?: ChunkImportance;\n readonly label: string;\n readonly retention?: ChunkRetention;\n}\n\nfunction mergeChunkAttributes(\n chunksAtPosition: readonly ChunkRecord[],\n): ChunkAttributes {\n const retentionOrder = createMetadataOrder({\n detailed: 3,\n focused: 2,\n relevant: 1,\n verbatim: 4,\n });\n const importanceOrder = createMetadataOrder({\n critical: 3,\n helpful: 1,\n important: 2,\n });\n let bestRetentionChunk = chunksAtPosition[0];\n let bestImportanceChunk = chunksAtPosition[0];\n\n for (const chunk of chunksAtPosition) {\n if (\n getMetadataRank(chunk.retention, retentionOrder) >\n getMetadataRank(bestRetentionChunk?.retention, retentionOrder)\n ) {\n bestRetentionChunk = chunk;\n }\n\n if (\n getMetadataRank(chunk.importance, importanceOrder) >\n getMetadataRank(bestImportanceChunk?.importance, importanceOrder)\n ) {\n bestImportanceChunk = chunk;\n }\n }\n\n const bestChunk =\n getMetadataRank(bestRetentionChunk?.retention, retentionOrder) >=\n getMetadataRank(bestImportanceChunk?.importance, importanceOrder)\n ? bestRetentionChunk\n : bestImportanceChunk;\n\n return {\n label: bestChunk?.label ?? \"\",\n ...(bestImportanceChunk?.importance === undefined\n ? {}\n : {\n importance: bestImportanceChunk.importance,\n }),\n ...(bestRetentionChunk?.retention === undefined\n ? {}\n : {\n retention: bestRetentionChunk.retention,\n }),\n };\n}\n\nfunction createMetadataOrder<TValue extends string>(\n values: Record<TValue, number>,\n): Readonly<Record<TValue, number>> {\n return Object.freeze(values);\n}\n\nfunction getMetadataRank<TValue extends string>(\n value: TValue | undefined,\n order: Readonly<Record<TValue, number>>,\n): number {\n if (value === undefined) {\n return 0;\n }\n\n return order[value] ?? 0;\n}\n\nfunction renderSentenceRange(input: {\n fragment: FragmentRecord;\n fullMarkup: boolean;\n range: {\n readonly endIndex: number;\n readonly startIndex: number;\n readonly chunkAttributes?: ChunkAttributes;\n };\n wrapHighRetention: boolean;\n}): string {\n const textSegment = input.fragment.sentences\n .slice(input.range.startIndex, input.range.endIndex)\n .map((sentence) => sentence.text)\n .join(\" \");\n const chunkAttributes = input.range.chunkAttributes;\n\n if (chunkAttributes === undefined) {\n return textSegment;\n }\n\n if (input.fullMarkup) {\n const attributes = [`label=\"${chunkAttributes.label}\"`];\n\n if (chunkAttributes.retention !== undefined) {\n attributes.push(`retention=\"${chunkAttributes.retention}\"`);\n }\n\n if (chunkAttributes.importance !== undefined) {\n attributes.push(`importance=\"${chunkAttributes.importance}\"`);\n }\n\n return `<chunk ${attributes.join(\" \")}>${textSegment}</chunk>`;\n }\n\n if (\n input.wrapHighRetention &&\n (chunkAttributes.retention === ChunkRetention.Verbatim ||\n chunkAttributes.retention === ChunkRetention.Detailed)\n ) {\n return `<chunk retention=\"${chunkAttributes.retention}\">${textSegment}</chunk>`;\n }\n\n return textSegment;\n}\n","export function cleanChunkTags(text: string): string {\n return text.replace(/<chunk(?:\\s+[^>]*)?>/g, \"\").replace(/<\\/chunk>/g, \"\");\n}\n\nexport function extractCompressedText(fullResponse: string): string {\n const matchedCompressedSection = fullResponse.match(\n /##\\s*(?:Compressed\\s+Text|压缩文本)\\s*\\n+(.*?)(?:\\n+---|\\*\\*CRITICAL\\*\\*|$)/is,\n );\n\n if (matchedCompressedSection?.[1] !== undefined) {\n return unwrapMarkdownCodeFence(matchedCompressedSection[1].trim());\n }\n\n return unwrapMarkdownCodeFence(fullResponse.trim());\n}\n\nexport function extractThinkingText(fullResponse: string): string {\n const matchedCompressedSection = fullResponse.match(\n /##\\s*(?:Compressed\\s+Text|压缩文本)\\s*/i,\n );\n\n if (matchedCompressedSection?.index === undefined) {\n return \"\";\n }\n\n return fullResponse.slice(0, matchedCompressedSection.index).trim();\n}\n\nfunction unwrapMarkdownCodeFence(text: string): string {\n const matchedFence = text.match(/^```(?:\\w+)?\\s*\\n([\\s\\S]*?)\\n```$/);\n\n return matchedFence?.[1]?.trim() ?? text;\n}\n","import { z } from \"zod\";\n\nimport { getLogger } from \"../common/logging.js\";\nimport {\n GuaranteedRequestFailureError,\n requestGuaranteedJson,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n} from \"../guaranteed/index.js\";\nimport type { Language } from \"../common/language.js\";\nimport type { LLMessage, LLM } from \"../llm/index.js\";\nimport type { ReadonlySerialFragments } from \"../document/index.js\";\nimport type { Clue } from \"./clue.js\";\nimport {\n CLUE_REVIEWER_GENERATOR_PROMPT_TEMPLATE,\n CLUE_REVIEWER_PROMPT_TEMPLATE,\n} from \"./prompt-templates.js\";\nimport { formatClueAsBook } from \"./markup.js\";\nimport {\n ReviewSeverity,\n expectReviewSeverity,\n type ClueReviewerInfo,\n type ReviewResult,\n} from \"./types.js\";\n\nconst REVIEW_SEVERITY_VALUES = [\n ReviewSeverity.Critical,\n ReviewSeverity.Major,\n ReviewSeverity.Minor,\n] as const;\n\nconst reviewIssueSchema = z.object({\n problem: z.string(),\n severity: z.enum(REVIEW_SEVERITY_VALUES),\n suggestion: z.string().default(\"\"),\n});\nconst reviewResponseSchema = z.object({\n issues: z.array(reviewIssueSchema),\n});\n\nexport type ReviewerHistories = Record<\n string,\n readonly [compressedText: string, rawResponse: string] | undefined\n>;\n\nexport class CompressionReviewer<S extends string> {\n readonly #llm: LLM<S>;\n readonly #reviewGuideScope: S;\n readonly #reviewScope: S;\n readonly #serialFragments: ReadonlySerialFragments;\n readonly #userLanguage: Language | undefined;\n\n public constructor(\n llm: LLM<S>,\n serialFragments: ReadonlySerialFragments,\n scopes: {\n readonly reviewGuide: S;\n readonly review: S;\n },\n userLanguage?: Language,\n ) {\n this.#llm = llm;\n this.#reviewGuideScope = scopes.reviewGuide;\n this.#reviewScope = scopes.review;\n this.#serialFragments = serialFragments;\n this.#userLanguage = userLanguage;\n }\n\n public async generateClueReviewers(\n clues: readonly Clue[],\n ): Promise<readonly ClueReviewerInfo[]> {\n return await Promise.all(\n clues.map(\n async (clue) =>\n await this.#llm.request(async (request) => {\n const clueText = await formatClueAsBook({\n chunks: clue.chunks,\n fullMarkup: true,\n serialFragments: this.#serialFragments,\n });\n const messages: LLMessage[] = [\n {\n content: this.#llm.loadSystemPrompt(\n CLUE_REVIEWER_GENERATOR_PROMPT_TEMPLATE,\n ),\n role: \"system\",\n },\n {\n content: clueText,\n role: \"user\",\n },\n ];\n const reviewerInfo = await request(messages, {\n scope: this.#reviewGuideScope,\n });\n\n return {\n clueId: clue.clueId,\n label: clue.label,\n reviewerInfo: reviewerInfo.trim(),\n weight: clue.weight,\n };\n }),\n ),\n );\n }\n\n public async reviewCompression(\n compressedText: string,\n clueReviewers: readonly ClueReviewerInfo[],\n reviewerHistories: ReviewerHistories,\n ): Promise<{\n readonly rawResponses: Readonly<Record<string, string | undefined>>;\n readonly reviews: readonly ReviewResult[];\n }> {\n const results = await Promise.all(\n clueReviewers.map(\n async (clueReviewer) =>\n await this.#llm.request(async (request) => {\n const systemPrompt = this.#llm.loadSystemPrompt(\n CLUE_REVIEWER_PROMPT_TEMPLATE,\n {\n thread_info: clueReviewer.reviewerInfo,\n user_language: this.#userLanguage,\n },\n );\n const previousHistory =\n reviewerHistories[String(clueReviewer.clueId)];\n const messages = buildReviewMessages(\n {\n compressedText,\n systemPrompt,\n },\n previousHistory,\n );\n\n try {\n return await requestGuaranteedJson({\n messages,\n parse: (data) => ({\n rawResponse: JSON.stringify(data),\n review: {\n clueId: clueReviewer.clueId,\n issues: data.issues.map((issue) => ({\n ...issue,\n severity: expectReviewSeverity(issue.severity),\n })),\n weight: clueReviewer.weight,\n },\n }),\n responseIntentClassifierPrompt: this.#llm.loadSystemPrompt(\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n ),\n request: async (retryMessages, retryIndex, retryMax) =>\n await request(retryMessages, {\n retryIndex,\n retryMax,\n scope: this.#reviewScope,\n useCache: false,\n }),\n schema: reviewResponseSchema,\n });\n } catch (error) {\n if (!(error instanceof GuaranteedRequestFailureError)) {\n throw error;\n }\n\n getLogger({\n clueId: clueReviewer.clueId,\n component: \"editor-review\",\n }).warn(\n { error },\n \"Compression reviewer failed to produce valid JSON; treating it as no issues.\",\n );\n\n return {\n rawResponse: undefined,\n review: {\n clueId: clueReviewer.clueId,\n issues: [],\n weight: clueReviewer.weight,\n },\n };\n }\n }),\n ),\n );\n const rawResponses = Object.create(null) as Record<\n string,\n string | undefined\n >;\n\n for (const result of results) {\n rawResponses[String(result.review.clueId)] = result.rawResponse;\n }\n\n return {\n rawResponses,\n reviews: results.map((result) => result.review),\n };\n }\n}\n\nfunction buildReviewMessages(\n input: {\n compressedText: string;\n systemPrompt: string;\n },\n previousHistory?: readonly [compressedText: string, rawResponse: string],\n): LLMessage[] {\n if (previousHistory === undefined) {\n return [\n {\n content: input.systemPrompt,\n role: \"system\",\n },\n {\n content: input.compressedText,\n role: \"user\",\n },\n ];\n }\n\n return [\n {\n content: input.systemPrompt,\n role: \"system\",\n },\n {\n content: previousHistory[0],\n role: \"user\",\n },\n {\n content: previousHistory[1],\n role: \"assistant\",\n },\n {\n content: input.compressedText,\n role: \"user\",\n },\n ];\n}\n","import type { Language } from \"../common/language.js\";\nimport type { LLM } from \"../llm/index.js\";\nimport type {\n ChunkRecord,\n ReadonlyDocument,\n ReadonlySerialFragments,\n} from \"../document/index.js\";\nimport { extractCluesFromDocument, type Clue } from \"./clue.js\";\nimport { CompressionRequester } from \"./compressor.js\";\nimport {\n calculateScore,\n createRevisionFeedback,\n pickBestVersion,\n} from \"./feedback.js\";\nimport { checkOutputLanguage } from \"./language-review.js\";\nimport { CompressionLog } from \"./log.js\";\nimport { formatChunksAsBook } from \"./markup.js\";\nimport {\n cleanChunkTags,\n extractCompressedText,\n extractThinkingText,\n} from \"./response.js\";\nimport { CompressionReviewer, type ReviewerHistories } from \"./review.js\";\nimport type { CompressionVersion } from \"./types.js\";\n\nexport interface EditorScopes<S extends string> {\n readonly compress: S;\n readonly review: S;\n readonly reviewGuide: S;\n}\n\nexport interface EditorOptions<S extends string> {\n readonly compressionRatio?: number;\n readonly document?: ReadonlyDocument;\n readonly groupId: number;\n readonly llm: LLM<S>;\n readonly logDirPath?: string;\n readonly maxClues?: number;\n readonly maxIterations?: number;\n readonly scopes: EditorScopes<S>;\n readonly serialId: number;\n readonly userLanguage?: Language;\n /** @deprecated Use `document` instead. */\n readonly workspace?: ReadonlyDocument;\n}\n\nexport async function compressText<S extends string>(\n options: EditorOptions<S>,\n): Promise<string> {\n return await new EditorOperation(options).run();\n}\n\nclass EditorOperation<S extends string> {\n readonly #compressionRatio: number;\n readonly #groupId: number;\n readonly #llm: LLM<S>;\n readonly #log: CompressionLog;\n readonly #maxClues: number;\n readonly #maxIterations: number;\n readonly #compressor: CompressionRequester<S>;\n readonly #reviewer: CompressionReviewer<S>;\n readonly #reviewScope: EditorScopes<S>;\n readonly #serialFragments: ReadonlySerialFragments;\n readonly #serialId: number;\n readonly #userLanguage: Language | undefined;\n readonly #document: ReadonlyDocument;\n\n public constructor(options: EditorOptions<S>) {\n const document = resolveDocument(options);\n\n this.#compressionRatio = options.compressionRatio ?? 0.2;\n this.#groupId = options.groupId;\n this.#llm = options.llm;\n this.#maxClues = options.maxClues ?? 10;\n this.#maxIterations = options.maxIterations ?? 5;\n this.#reviewScope = options.scopes;\n this.#serialFragments = document.getSerialFragments(options.serialId);\n this.#serialId = options.serialId;\n this.#userLanguage = options.userLanguage;\n this.#document = document;\n this.#compressor = new CompressionRequester(\n this.#llm,\n options.scopes.compress,\n this.#compressionRatio,\n this.#userLanguage,\n );\n this.#log = new CompressionLog(this.#serialId, this.#groupId, {\n compressionRatio: this.#compressionRatio,\n maxIterations: this.#maxIterations,\n ...(options.logDirPath === undefined\n ? {}\n : {\n logDirPath: options.logDirPath,\n }),\n });\n this.#reviewer = new CompressionReviewer(\n this.#llm,\n this.#serialFragments,\n {\n review: this.#reviewScope.review,\n reviewGuide: this.#reviewScope.reviewGuide,\n },\n this.#userLanguage,\n );\n }\n\n public async run(): Promise<string> {\n const groups = await this.#listTargetGroups();\n const segmentStartIndexes = await this.#getGroupSegmentStartIndexes(groups);\n\n if (segmentStartIndexes.length === 0) {\n return \"\";\n }\n\n const clues = await extractCluesFromDocument({\n document: this.#document,\n groupId: this.#groupId,\n maxClues: this.#maxClues,\n serialId: this.#serialId,\n });\n const originalText = await this.#getFullText(groups);\n\n if (originalText.trim() === \"\") {\n return \"\";\n }\n\n await this.#log.initialize(clues);\n\n const markedOriginalText = await formatChunksAsBook({\n chunks: listClueChunks(clues),\n segmentStartIndexes,\n serialFragments: this.#serialFragments,\n wrapHighRetention: true,\n });\n const clueReviewers = await this.#reviewer.generateClueReviewers(clues);\n const targetLength = Math.floor(\n originalText.length * this.#compressionRatio,\n );\n const versions: CompressionVersion[] = [];\n const reviewerHistories = Object.create(null) as ReviewerHistories;\n let previousCompressedText: string | undefined;\n let revisionFeedback: string | undefined;\n\n for (let iteration = 1; iteration <= this.#maxIterations; iteration += 1) {\n await this.#log.appendIterationHeader(iteration, revisionFeedback);\n\n const fullResponse = await this.#compressor.request({\n markedText: markedOriginalText,\n targetLength,\n ...(previousCompressedText === undefined\n ? {}\n : {\n previousCompressedText,\n }),\n ...(revisionFeedback === undefined\n ? {}\n : {\n revisionFeedback,\n }),\n });\n const compressedText = cleanChunkTags(\n extractCompressedText(fullResponse),\n );\n const thinkingText = extractThinkingText(fullResponse);\n\n await this.#log.appendCompressionResult({\n compressedText,\n thinkingText,\n });\n\n const reviewOutput = await this.#reviewer.reviewCompression(\n compressedText,\n clueReviewers,\n reviewerHistories,\n );\n const reviews = [...reviewOutput.reviews];\n const languageReview = checkOutputLanguage({\n compressedText,\n ...(this.#userLanguage === undefined\n ? {}\n : {\n userLanguage: this.#userLanguage,\n }),\n });\n\n if (languageReview !== undefined) {\n reviews.push(languageReview.review);\n await this.#log.appendLanguageMismatch({\n detectedLanguageCode: languageReview.detectedLanguageCode,\n review: languageReview.review,\n targetLanguageCode: languageReview.targetLanguageCode,\n userLanguage: this.#userLanguage,\n });\n }\n\n const score = calculateScore(reviews);\n\n versions.push({\n iteration,\n reviews,\n score,\n text: compressedText,\n });\n\n if (score === 0) {\n break;\n }\n\n if (iteration >= this.#maxIterations) {\n continue;\n }\n\n revisionFeedback = createRevisionFeedback({\n llm: this.#llm,\n reviews,\n });\n previousCompressedText = compressedText;\n\n for (const clueId of Object.keys(reviewOutput.rawResponses)) {\n const rawResponse = reviewOutput.rawResponses[clueId];\n\n if (rawResponse === undefined) {\n continue;\n }\n\n reviewerHistories[clueId] = [compressedText, rawResponse];\n }\n }\n\n const bestVersion = pickBestVersion(versions);\n\n await this.#log.appendFinalSelection(bestVersion, originalText.length);\n\n return bestVersion.text;\n }\n\n async #listTargetGroups(): Promise<readonly SentenceGroup[]> {\n return (await this.#document.fragmentGroups.listBySerial(this.#serialId))\n .filter((record) => record.groupId === this.#groupId)\n .map((record) => ({\n endSentenceIndex: record.endSentenceIndex,\n startSentenceIndex: record.startSentenceIndex,\n }));\n }\n\n async #getGroupSegmentStartIndexes(\n groups: readonly SentenceGroup[],\n ): Promise<number[]> {\n const segmentStartIndexes = [\n ...(await this.#serialFragments.listFragmentIds()),\n ].sort(compareNumber);\n const coveredStartIndexes = new Set<number>();\n\n for (const group of groups) {\n for (let index = 0; index < segmentStartIndexes.length; index += 1) {\n const startSentenceIndex = segmentStartIndexes[index];\n\n if (startSentenceIndex === undefined) {\n continue;\n }\n\n const nextStartSentenceIndex = segmentStartIndexes[index + 1];\n const endSentenceIndex =\n nextStartSentenceIndex === undefined\n ? Infinity\n : nextStartSentenceIndex - 1;\n\n if (\n startSentenceIndex <= group.endSentenceIndex &&\n endSentenceIndex >= group.startSentenceIndex\n ) {\n coveredStartIndexes.add(startSentenceIndex);\n }\n }\n }\n\n return [...coveredStartIndexes].sort(compareNumber);\n }\n\n async #getFullText(groups: readonly SentenceGroup[]): Promise<string> {\n const fragments = await Promise.all(\n groups.map(async (group) => ({\n sentences: await this.#listGroupSentences(\n group.startSentenceIndex,\n group.endSentenceIndex,\n ),\n })),\n );\n\n return fragments\n .flatMap((fragment) =>\n fragment.sentences.map((sentence) => sentence.text),\n )\n .join(\" \");\n }\n\n async #listGroupSentences(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<readonly { readonly text: string }[]> {\n if (this.#serialFragments.listSentencesInRange !== undefined) {\n return await this.#serialFragments.listSentencesInRange(\n startSentenceIndex,\n endSentenceIndex,\n );\n }\n\n const segmentStartIndexes = [\n ...(await this.#serialFragments.listFragmentIds()),\n ].sort(compareNumber);\n const fragments = await Promise.all(\n segmentStartIndexes\n .filter((segmentStartIndex, index) => {\n const nextStartSentenceIndex = segmentStartIndexes[index + 1];\n const segmentEndSentenceIndex =\n nextStartSentenceIndex === undefined\n ? Infinity\n : nextStartSentenceIndex - 1;\n\n return (\n segmentStartIndex <= endSentenceIndex &&\n segmentEndSentenceIndex >= startSentenceIndex\n );\n })\n .map(\n async (startSentenceIndex) =>\n await this.#serialFragments.getFragment(startSentenceIndex),\n ),\n );\n\n return fragments.flatMap((fragment) =>\n fragment.sentences.filter((_sentence, index) => {\n const sentenceIndex = fragment.fragmentId + index;\n\n return (\n sentenceIndex >= startSentenceIndex &&\n sentenceIndex <= endSentenceIndex\n );\n }),\n );\n }\n}\n\ninterface SentenceGroup {\n readonly endSentenceIndex: number;\n readonly startSentenceIndex: number;\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction listClueChunks(clues: readonly Clue[]): ChunkRecord[] {\n return clues.flatMap((clue) => clue.chunks);\n}\n\nfunction resolveDocument<S extends string>(\n options: EditorOptions<S>,\n): ReadonlyDocument {\n const document = options.document ?? options.workspace;\n\n if (document === undefined) {\n throw new Error(\"Editor requires a document\");\n }\n\n return document;\n}\n","export { compressText } from \"./editor.js\";\nexport type { EditorOptions, EditorScopes } from \"./editor.js\";\n","import type {\n ChunkRecord,\n ReadingEdgeRecord,\n ReadonlySerialFragments,\n} from \"../document/index.js\";\n\nexport interface SegmentInfo {\n readonly startSentenceIndex: number;\n readonly wordsCount: number;\n readonly startIncision: number;\n readonly endIncision: number;\n}\n\nexport async function computeNormalizedSegmentIncisions(input: {\n chunks: readonly ChunkRecord[];\n edges: readonly ReadingEdgeRecord[];\n fragments: ReadonlySerialFragments;\n}): Promise<SegmentInfo[]> {\n const segmentWordsCounts = await loadSegmentWordsCounts(input.fragments);\n\n return normalizeIncisions(\n computeSegmentIncisions({\n chunks: input.chunks,\n edges: input.edges,\n segmentWordsCounts,\n }),\n );\n}\n\nasync function loadSegmentWordsCounts(\n fragments: ReadonlySerialFragments,\n): Promise<Readonly<Record<string, number>>> {\n const wordsCounts = createNumberRecord();\n const startSentenceIndexes = await fragments.listFragmentIds();\n\n for (const startSentenceIndex of startSentenceIndexes) {\n const segment = await fragments.getFragment(startSentenceIndex);\n\n wordsCounts[String(startSentenceIndex)] = segment.sentences.reduce(\n (total, sentence) => total + sentence.wordsCount,\n 0,\n );\n }\n\n return wordsCounts;\n}\n\nfunction computeSegmentIncisions(input: {\n chunks: readonly ChunkRecord[];\n edges: readonly ReadingEdgeRecord[];\n segmentWordsCounts: Readonly<Record<string, number>>;\n}): SegmentInfo[] {\n const startSentenceIndexes = Object.keys(input.segmentWordsCounts)\n .map(Number)\n .sort(compareNumber);\n const chunkIdsBySegmentStartIndex = createNumberListRecord();\n const chunkWeightsById = createNumberRecord();\n const edgesByChunkId = createReadingEdgeListRecord();\n\n for (const chunk of input.chunks) {\n const startSentenceIndex = findContainingSegmentStartIndex(\n startSentenceIndexes,\n chunk.sentenceId[1],\n );\n\n if (startSentenceIndex === undefined) {\n continue;\n }\n\n const segmentKey = String(startSentenceIndex);\n\n chunkWeightsById[String(chunk.id)] = chunk.weight;\n if (chunkIdsBySegmentStartIndex[segmentKey] === undefined) {\n chunkIdsBySegmentStartIndex[segmentKey] = [];\n }\n chunkIdsBySegmentStartIndex[segmentKey]?.push(chunk.id);\n }\n\n for (const edge of input.edges) {\n const fromKey = String(edge.fromId);\n const toKey = String(edge.toId);\n\n if (edgesByChunkId[fromKey] === undefined) {\n edgesByChunkId[fromKey] = [];\n }\n if (edgesByChunkId[toKey] === undefined) {\n edgesByChunkId[toKey] = [];\n }\n\n edgesByChunkId[fromKey]?.push(edge);\n edgesByChunkId[toKey]?.push(edge);\n }\n\n const chunksById = createChunkRecord();\n\n for (const chunk of input.chunks) {\n chunksById[String(chunk.id)] = chunk;\n }\n\n return startSentenceIndexes\n .filter(\n (startSentenceIndex) =>\n chunkIdsBySegmentStartIndex[String(startSentenceIndex)] !== undefined,\n )\n .map((startSentenceIndex) => {\n const chunkIds =\n chunkIdsBySegmentStartIndex[String(startSentenceIndex)] ?? [];\n let endIncision = 0;\n let startIncision = 0;\n\n for (const chunkId of chunkIds) {\n const chunkWeight = chunkWeightsById[String(chunkId)] ?? 0;\n const chunkEdges = edgesByChunkId[String(chunkId)] ?? [];\n const externalEdges = chunkEdges\n .map((edge) => ({\n otherChunkId: edge.fromId === chunkId ? edge.toId : edge.fromId,\n weight: edge.weight,\n }))\n .map((edge) => {\n const otherChunk = chunksById[String(edge.otherChunkId)];\n\n if (otherChunk === undefined) {\n return undefined;\n }\n\n return {\n otherSegmentStartIndex: findContainingSegmentStartIndex(\n startSentenceIndexes,\n otherChunk.sentenceId[1],\n ),\n weight: edge.weight,\n };\n })\n .filter(\n (\n edge,\n ): edge is {\n otherSegmentStartIndex: number;\n weight: number;\n } =>\n edge !== undefined &&\n edge.otherSegmentStartIndex !== undefined &&\n edge.otherSegmentStartIndex !== startSentenceIndex,\n );\n\n if (externalEdges.length === 0) {\n continue;\n }\n\n const totalExternalWeight = externalEdges.reduce(\n (total, edge) => total + edge.weight,\n 0,\n );\n\n if (totalExternalWeight === 0) {\n continue;\n }\n\n for (const edge of externalEdges) {\n const halfWeight = (chunkWeight / totalExternalWeight) * edge.weight;\n\n if (edge.otherSegmentStartIndex < startSentenceIndex) {\n startIncision += halfWeight;\n continue;\n }\n\n endIncision += halfWeight;\n }\n }\n\n return {\n endIncision,\n startSentenceIndex,\n startIncision,\n wordsCount: input.segmentWordsCounts[String(startSentenceIndex)] ?? 0,\n };\n });\n}\n\nfunction findContainingSegmentStartIndex(\n startSentenceIndexes: readonly number[],\n sentenceIndex: number,\n): number | undefined {\n let containingSegmentStartIndex: number | undefined;\n\n for (const startSentenceIndex of startSentenceIndexes) {\n if (startSentenceIndex > sentenceIndex) {\n break;\n }\n\n containingSegmentStartIndex = startSentenceIndex;\n }\n\n return containingSegmentStartIndex;\n}\n\nfunction normalizeIncisions(\n segmentInfos: readonly SegmentInfo[],\n): SegmentInfo[] {\n const allIncisions = segmentInfos\n .flatMap((segmentInfo) => [\n segmentInfo.startIncision,\n segmentInfo.endIncision,\n ])\n .filter((incision) => incision > 0)\n .sort(compareNumber);\n\n if (allIncisions.length === 0) {\n return [...segmentInfos];\n }\n\n const thresholdIndex = Math.min(\n Math.floor(allIncisions.length * 0.98),\n allIncisions.length - 1,\n );\n const threshold = allIncisions[thresholdIndex] ?? 0;\n const normalValues = allIncisions.filter((incision) => incision < threshold);\n\n if (normalValues.length === 0) {\n return segmentInfos.map((segmentInfo) => ({\n ...segmentInfo,\n endIncision: segmentInfo.endIncision > 0 ? 10 : 0,\n startIncision: segmentInfo.startIncision > 0 ? 10 : 0,\n }));\n }\n\n const minValue = Math.min(...normalValues);\n const maxValue = Math.max(...normalValues);\n const minLog = minValue > 0 ? Math.log(minValue) : 0;\n const maxLog = maxValue > 0 ? Math.log(maxValue) : 0;\n const logRange = maxLog - minLog === 0 ? 1 : maxLog - minLog;\n\n return segmentInfos.map((segmentInfo) => ({\n ...segmentInfo,\n endIncision: normalizeIncision(segmentInfo.endIncision, {\n logRange,\n minLog,\n threshold,\n }),\n startIncision: normalizeIncision(segmentInfo.startIncision, {\n logRange,\n minLog,\n threshold,\n }),\n }));\n}\n\nfunction normalizeIncision(\n incision: number,\n input: {\n logRange: number;\n minLog: number;\n threshold: number;\n },\n): number {\n if (incision === 0) {\n return 0;\n }\n\n if (incision >= input.threshold) {\n return 10;\n }\n\n const incisionLog = incision > 0 ? Math.log(incision) : input.minLog;\n\n return Math.max(\n 1,\n Math.min(\n 10,\n Math.round(1 + ((incisionLog - input.minLog) / input.logRange) * 9),\n ),\n );\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction createChunkRecord(): Record<string, ChunkRecord | undefined> {\n return Object.create(null) as Record<string, ChunkRecord | undefined>;\n}\n\nfunction createReadingEdgeListRecord(): Record<\n string,\n ReadingEdgeRecord[] | undefined\n> {\n return Object.create(null) as Record<string, ReadingEdgeRecord[] | undefined>;\n}\n\nfunction createNumberListRecord(): Record<string, number[] | undefined> {\n return Object.create(null) as Record<string, number[] | undefined>;\n}\n\nfunction createNumberRecord(): Record<string, number> {\n return Object.create(null) as Record<string, number>;\n}\n","import type { SentenceGroupRecord } from \"../document/index.js\";\nimport type { SegmentInfo } from \"./segment-incision.js\";\n\ninterface SegmentResource {\n readonly count: number;\n readonly startIncision: number;\n readonly endIncision: number;\n readonly startSentenceIndex: number;\n}\n\nexport function createSegmentGroups(input: {\n segmentInfos: readonly SegmentInfo[];\n groupWordsCount: number;\n serialId: number;\n}): SentenceGroupRecord[] {\n if (input.segmentInfos.length === 0) {\n return [];\n }\n\n const resources = input.segmentInfos.map(\n (segmentInfo): SegmentResource => ({\n count: segmentInfo.wordsCount,\n endIncision: segmentInfo.endIncision,\n startSentenceIndex: segmentInfo.startSentenceIndex,\n startIncision: segmentInfo.startIncision,\n }),\n );\n const groups = allocateSegmentGroups(resources, input.groupWordsCount);\n\n return groups.map((startSentenceIndexes, groupId) => ({\n endSentenceIndex: Math.max(...startSentenceIndexes),\n groupId,\n serialId: input.serialId,\n startSentenceIndex: Math.min(...startSentenceIndexes),\n }));\n}\n\nfunction allocateSegmentGroups(\n resources: readonly SegmentResource[],\n maxCount: number,\n): number[][] {\n return splitResources(resources, maxCount).map((group) =>\n group.map((resource) => resource.startSentenceIndex),\n );\n}\n\nfunction splitResources(\n resources: readonly SegmentResource[],\n maxCount: number,\n): SegmentResource[][] {\n if (resources.length === 0) {\n return [];\n }\n\n if (maxCount <= 0) {\n return resources.map((resource) => [resource]);\n }\n\n const totalCount = resources.reduce((total, resource) => {\n return total + resource.count;\n }, 0);\n\n if (resources.length === 1 || totalCount <= maxCount) {\n return [[...resources]];\n }\n\n const splitIndex = findPreferredSplitIndex(resources, maxCount);\n\n if (splitIndex === undefined) {\n return splitGreedily(resources, maxCount);\n }\n\n const left = resources.slice(0, splitIndex + 1);\n const right = resources.slice(splitIndex + 1);\n\n if (\n left.length === 0 ||\n right.length === 0 ||\n left.length === resources.length ||\n right.length === resources.length\n ) {\n return splitGreedily(resources, maxCount);\n }\n\n return [\n ...splitResources(left, maxCount),\n ...splitResources(right, maxCount),\n ];\n}\n\nfunction findPreferredSplitIndex(\n resources: readonly SegmentResource[],\n maxCount: number,\n): number | undefined {\n const totalCount = resources.reduce((total, resource) => {\n return total + resource.count;\n }, 0);\n let leftCount = 0;\n let bestIndex: number | undefined;\n let bestBoundary = 0;\n let bestOversizedSides = Number.POSITIVE_INFINITY;\n let bestOverflow = Number.POSITIVE_INFINITY;\n\n for (let index = 0; index < resources.length - 1; index += 1) {\n const current = resources[index];\n const next = resources[index + 1];\n\n if (current === undefined || next === undefined) {\n continue;\n }\n\n leftCount += current.count;\n const rightCount = totalCount - leftCount;\n const boundary = current.endIncision + next.startIncision;\n\n if (boundary <= 0) {\n continue;\n }\n\n const oversizedSides =\n Number(leftCount > maxCount) + Number(rightCount > maxCount);\n const overflow =\n Math.max(0, leftCount - maxCount) + Math.max(0, rightCount - maxCount);\n\n if (\n oversizedSides < bestOversizedSides ||\n (oversizedSides === bestOversizedSides && boundary > bestBoundary) ||\n (oversizedSides === bestOversizedSides &&\n boundary === bestBoundary &&\n overflow < bestOverflow)\n ) {\n bestBoundary = boundary;\n bestIndex = index;\n bestOversizedSides = oversizedSides;\n bestOverflow = overflow;\n }\n }\n\n return bestIndex;\n}\n\nfunction splitGreedily(\n resources: readonly SegmentResource[],\n maxCount: number,\n): SegmentResource[][] {\n const groups: SegmentResource[][] = [];\n let currentGroup: SegmentResource[] = [];\n let currentCount = 0;\n\n for (const resource of resources) {\n if (currentGroup.length > 0 && currentCount + resource.count > maxCount) {\n groups.push(currentGroup);\n currentGroup = [];\n currentCount = 0;\n }\n\n currentGroup.push(resource);\n currentCount += resource.count;\n\n if (currentCount >= maxCount) {\n groups.push(currentGroup);\n currentGroup = [];\n currentCount = 0;\n }\n }\n\n if (currentGroup.length > 0) {\n groups.push(currentGroup);\n }\n\n return groups;\n}\n","import type { SentenceGroupRecord } from \"../document/index.js\";\nimport { computeNormalizedSegmentIncisions } from \"./segment-incision.js\";\nimport { createSegmentGroups } from \"./resource-segmentation.js\";\n\nexport async function groupSegments(input: {\n edges: Parameters<typeof computeNormalizedSegmentIncisions>[0][\"edges\"];\n fragments: Parameters<\n typeof computeNormalizedSegmentIncisions\n >[0][\"fragments\"];\n groupWordsCount: number;\n chunks: Parameters<typeof computeNormalizedSegmentIncisions>[0][\"chunks\"];\n serialId: number;\n}): Promise<SentenceGroupRecord[]> {\n const segmentInfos = await computeNormalizedSegmentIncisions({\n chunks: input.chunks,\n edges: input.edges,\n fragments: input.fragments,\n });\n\n return createSegmentGroups({\n segmentInfos,\n groupWordsCount: input.groupWordsCount,\n serialId: input.serialId,\n });\n}\n","import {\n ChunkImportance,\n ChunkRetention,\n type ChunkRecord,\n type ReadingEdgeRecord,\n} from \"../document/index.js\";\n\nconst MIN_EDGE_WEIGHT = 0.1;\n\nconst IMPORTANCE_WEIGHTS: Readonly<Record<ChunkImportance, number>> = {\n [ChunkImportance.Critical]: 9,\n [ChunkImportance.Helpful]: 1,\n [ChunkImportance.Important]: 3,\n};\n\nconst LINK_STRENGTH_WEIGHTS: Readonly<Record<string, number>> = {\n critical: 9,\n helpful: 1,\n important: 3,\n};\n\nconst RETENTION_WEIGHTS: Readonly<Record<ChunkRetention, number>> = {\n [ChunkRetention.Detailed]: 9,\n [ChunkRetention.Focused]: 3,\n [ChunkRetention.Relevant]: 1,\n [ChunkRetention.Verbatim]: 27,\n};\n\nexport function computeChunkWeights(\n chunks: readonly ChunkRecord[],\n): Readonly<Record<string, number>> {\n const weights = createNumberRecord();\n\n for (const chunk of chunks) {\n weights[String(chunk.id)] = computeChunkWeight(chunk);\n }\n\n return weights;\n}\n\nexport function computeReadingEdgeWeights(input: {\n chunkWeights: Readonly<Record<string, number>>;\n edges: readonly ReadingEdgeRecord[];\n}): Readonly<Record<string, number>> {\n const strengthsByChunkId = createNumberRecord();\n\n for (const edge of input.edges) {\n const strengthWeight = getLinkStrengthWeight(edge.strength);\n\n strengthsByChunkId[String(edge.fromId)] =\n (strengthsByChunkId[String(edge.fromId)] ?? 0) + strengthWeight;\n strengthsByChunkId[String(edge.toId)] =\n (strengthsByChunkId[String(edge.toId)] ?? 0) + strengthWeight;\n }\n\n const edgeWeights = createNumberRecord();\n\n for (const edge of input.edges) {\n const strengthWeight = getLinkStrengthWeight(edge.strength);\n const fromChunkWeight = input.chunkWeights[String(edge.fromId)] ?? 0;\n const toChunkWeight = input.chunkWeights[String(edge.toId)] ?? 0;\n const fromTotalStrength = strengthsByChunkId[String(edge.fromId)] ?? 0;\n const toTotalStrength = strengthsByChunkId[String(edge.toId)] ?? 0;\n const fromHalfWeight =\n fromTotalStrength === 0\n ? 0\n : fromChunkWeight * (strengthWeight / fromTotalStrength);\n const toHalfWeight =\n toTotalStrength === 0\n ? 0\n : toChunkWeight * (strengthWeight / toTotalStrength);\n\n edgeWeights[getReadingEdgeKey(edge.fromId, edge.toId)] = Math.max(\n fromHalfWeight + toHalfWeight,\n MIN_EDGE_WEIGHT,\n );\n }\n\n return edgeWeights;\n}\n\nexport function getReadingEdgeKey(fromId: number, toId: number): string {\n return `${fromId}:${toId}`;\n}\n\nfunction computeChunkWeight(chunk: ChunkRecord): number {\n return (\n getRetentionWeight(chunk.retention) + getImportanceWeight(chunk.importance)\n );\n}\n\nfunction getImportanceWeight(importance: ChunkImportance | undefined): number {\n if (importance === undefined) {\n return 0;\n }\n\n return IMPORTANCE_WEIGHTS[importance] ?? 0;\n}\n\nfunction getLinkStrengthWeight(strength: string | undefined): number {\n if (strength === undefined) {\n return 1;\n }\n\n return LINK_STRENGTH_WEIGHTS[strength] ?? 1;\n}\n\nfunction getRetentionWeight(retention: ChunkRetention | undefined): number {\n if (retention === undefined) {\n return 0;\n }\n\n return RETENTION_WEIGHTS[retention] ?? 0;\n}\n\nfunction createNumberRecord(): Record<string, number> {\n return Object.create(null) as Record<string, number>;\n}\n","import type { ChunkRecord, ReadingEdgeRecord } from \"../document/index.js\";\nimport { getReadingEdgeKey } from \"./weights.js\";\n\nexport interface SnakeGraphEdge {\n readonly fromSnakeIndex: number;\n readonly toSnakeIndex: number;\n readonly weight: number;\n}\n\nexport function buildSnakeGraph(input: {\n chunksById: Readonly<Record<string, ChunkRecord | undefined>>;\n edges: readonly ReadingEdgeRecord[];\n snakes: readonly (readonly number[])[];\n}): SnakeGraphEdge[] {\n const chunkIdToSnakeIndex = createOptionalNumberRecord();\n const interSnakeWeightByKey = createNumberRecord();\n\n for (const [snakeIndex, snakeChunkIds] of input.snakes.entries()) {\n for (const chunkId of snakeChunkIds) {\n chunkIdToSnakeIndex[String(chunkId)] = snakeIndex;\n }\n }\n\n for (const edge of input.edges) {\n const fromSnakeIndex = chunkIdToSnakeIndex[String(edge.fromId)];\n const toSnakeIndex = chunkIdToSnakeIndex[String(edge.toId)];\n\n if (\n fromSnakeIndex === undefined ||\n toSnakeIndex === undefined ||\n fromSnakeIndex === toSnakeIndex\n ) {\n continue;\n }\n\n const edgeKey = getReadingEdgeKey(fromSnakeIndex, toSnakeIndex);\n\n interSnakeWeightByKey[edgeKey] =\n (interSnakeWeightByKey[edgeKey] ?? 0) + edge.weight;\n }\n\n const normalizedSnakeWeightByKey = createNumberRecord();\n\n for (const [edgeKey, weight] of Object.entries(interSnakeWeightByKey)) {\n const [fromSnakeIndexText = \"\", toSnakeIndexText = \"\"] = edgeKey.split(\":\");\n const fromSnakeIndex = Number(fromSnakeIndexText);\n const toSnakeIndex = Number(toSnakeIndexText);\n const fromSnakeStartChunkId = input.snakes[fromSnakeIndex]?.[0];\n const toSnakeStartChunkId = input.snakes[toSnakeIndex]?.[0];\n\n if (\n fromSnakeStartChunkId === undefined ||\n toSnakeStartChunkId === undefined\n ) {\n continue;\n }\n\n const fromChunk = input.chunksById[String(fromSnakeStartChunkId)];\n const toChunk = input.chunksById[String(toSnakeStartChunkId)];\n\n if (fromChunk === undefined || toChunk === undefined) {\n continue;\n }\n\n const normalizedEdgeKey =\n compareChunkBySentence(fromChunk, toChunk) < 0\n ? getReadingEdgeKey(fromSnakeIndex, toSnakeIndex)\n : getReadingEdgeKey(toSnakeIndex, fromSnakeIndex);\n\n normalizedSnakeWeightByKey[normalizedEdgeKey] =\n (normalizedSnakeWeightByKey[normalizedEdgeKey] ?? 0) + weight;\n }\n\n return Object.keys(normalizedSnakeWeightByKey)\n .sort(compareEdgeKey)\n .map((edgeKey) => {\n const [fromSnakeIndexText = \"\", toSnakeIndexText = \"\"] =\n edgeKey.split(\":\");\n\n return {\n fromSnakeIndex: Number(fromSnakeIndexText),\n toSnakeIndex: Number(toSnakeIndexText),\n weight: normalizedSnakeWeightByKey[edgeKey] ?? 0,\n };\n });\n}\n\nfunction compareChunkBySentence(left: ChunkRecord, right: ChunkRecord): number {\n const [leftSerialId, leftSentenceIndex] = left.sentenceId;\n const [rightSerialId, rightSentenceIndex] = right.sentenceId;\n\n if (leftSerialId !== rightSerialId) {\n return leftSerialId - rightSerialId;\n }\n\n if (leftSentenceIndex !== rightSentenceIndex) {\n return leftSentenceIndex - rightSentenceIndex;\n }\n\n return left.id - right.id;\n}\n\nfunction compareEdgeKey(left: string, right: string): number {\n const [leftFromIdText = \"\", leftToIdText = \"\"] = left.split(\":\");\n const [rightFromIdText = \"\", rightToIdText = \"\"] = right.split(\":\");\n const leftFromId = Number(leftFromIdText);\n const rightFromId = Number(rightFromIdText);\n\n if (leftFromId !== rightFromId) {\n return leftFromId - rightFromId;\n }\n\n return Number(leftToIdText) - Number(rightToIdText);\n}\n\nfunction createNumberRecord(): Record<string, number> {\n return Object.create(null) as Record<string, number>;\n}\n\nfunction createOptionalNumberRecord(): Record<string, number | undefined> {\n return Object.create(null) as Record<string, number | undefined>;\n}\n","import type { ChunkRecord, ReadingEdgeRecord } from \"../document/index.js\";\nimport { getReadingEdgeKey } from \"./weights.js\";\n\nconst ATTRIBUTE_BONUS_WEIGHT = 3;\nconst DEFAULT_LINK_STRENGTH_WEIGHT = 1;\nconst DEFAULT_SNAKE_WORDS_COUNT = 280;\nconst DECAY_FACTOR = 0.75;\nconst MIN_EDGE_WEIGHT = 0.1;\n\ninterface DetectorGraph {\n readonly chunksById: Readonly<Record<string, ChunkRecord | undefined>>;\n readonly edgeByKey: Readonly<Record<string, ReadingEdgeRecord | undefined>>;\n readonly fingerprintEdgeWeightsByKey: Readonly<Record<string, number>>;\n readonly incomingEdgesByChunkId: Readonly<\n Record<string, readonly ReadingEdgeRecord[] | undefined>\n >;\n readonly outgoingEdgesByChunkId: Readonly<\n Record<string, readonly ReadingEdgeRecord[] | undefined>\n >;\n readonly sortedChunkIds: readonly number[];\n readonly undirectedAdjacentChunkIdsByChunkId: Readonly<\n Record<string, readonly number[] | undefined>\n >;\n}\n\ninterface MergeConfig {\n readonly enableBonus: boolean;\n readonly snakeWordsCount: number;\n}\n\nexport function splitConnectedComponents(input: {\n chunkIds: readonly number[];\n edges: readonly ReadingEdgeRecord[];\n}): number[][] {\n const sortedChunkIds = [...input.chunkIds].sort(compareNumber);\n const chunkIdRecord = createBooleanRecord();\n const adjacentChunkIdsByChunkId = createNumberListRecord();\n const visitedChunkIds = createBooleanRecord();\n const components: number[][] = [];\n\n for (const chunkId of sortedChunkIds) {\n chunkIdRecord[String(chunkId)] = true;\n }\n\n for (const edge of input.edges) {\n if (\n chunkIdRecord[String(edge.fromId)] !== true ||\n chunkIdRecord[String(edge.toId)] !== true\n ) {\n continue;\n }\n\n attachChunkAdjacency(adjacentChunkIdsByChunkId, edge.fromId, edge.toId);\n attachChunkAdjacency(adjacentChunkIdsByChunkId, edge.toId, edge.fromId);\n }\n\n for (const chunkId of sortedChunkIds) {\n if (visitedChunkIds[String(chunkId)] === true) {\n continue;\n }\n\n const stack = [chunkId];\n const component: number[] = [];\n visitedChunkIds[String(chunkId)] = true;\n\n while (stack.length > 0) {\n const currentChunkId = stack.pop();\n\n if (currentChunkId === undefined) {\n continue;\n }\n\n component.push(currentChunkId);\n\n for (const nextChunkId of adjacentChunkIdsByChunkId[\n String(currentChunkId)\n ] ?? []) {\n if (visitedChunkIds[String(nextChunkId)] === true) {\n continue;\n }\n\n visitedChunkIds[String(nextChunkId)] = true;\n stack.push(nextChunkId);\n }\n }\n\n component.sort(compareNumber);\n components.push(component);\n }\n\n components.sort((left, right) => {\n const leftMinChunkId = left[0] ?? Number.POSITIVE_INFINITY;\n const rightMinChunkId = right[0] ?? Number.POSITIVE_INFINITY;\n\n return leftMinChunkId - rightMinChunkId;\n });\n\n return components;\n}\n\nexport function detectSnakesInComponent(input: {\n chunks: readonly ChunkRecord[];\n edges: readonly ReadingEdgeRecord[];\n snakeWordsCount?: number;\n}): number[][] {\n if (input.chunks.length === 0) {\n return [];\n }\n\n const graph = createDetectorGraph(input.chunks, input.edges);\n\n if (!isWeaklyConnected(graph)) {\n throw new Error(\n \"Snake detector requires a weakly connected component input\",\n );\n }\n\n const snakeWordsCount = input.snakeWordsCount ?? DEFAULT_SNAKE_WORDS_COUNT;\n const totalWordsCount = input.chunks.reduce((sum, chunk) => {\n return sum + chunk.wordsCount;\n }, 0);\n\n if (totalWordsCount <= snakeWordsCount) {\n return [\n [...input.chunks].sort(compareChunkBySentence).map((chunk) => chunk.id),\n ];\n }\n\n const fingerprints = computeAllFingerprints(graph);\n\n validateFingerprints(fingerprints);\n\n const phaseOneClusters = greedyMerge(\n graph,\n fingerprints,\n {\n enableBonus: true,\n snakeWordsCount,\n },\n undefined,\n );\n const phaseTwoClusters = greedyMerge(\n graph,\n fingerprints,\n {\n enableBonus: false,\n snakeWordsCount,\n },\n phaseOneClusters,\n );\n\n return [...phaseTwoClusters.values()].map((cluster) =>\n [...cluster].sort((leftId, rightId) => {\n const leftChunk = graph.chunksById[String(leftId)];\n const rightChunk = graph.chunksById[String(rightId)];\n\n if (leftChunk === undefined || rightChunk === undefined) {\n return leftId - rightId;\n }\n\n return compareChunkBySentence(leftChunk, rightChunk);\n }),\n );\n}\n\nfunction attachChunkAdjacency(\n adjacentChunkIdsByChunkId: Record<string, number[] | undefined>,\n fromId: number,\n toId: number,\n): void {\n const existingAdjacentChunkIds =\n adjacentChunkIdsByChunkId[String(fromId)] ?? [];\n\n if (existingAdjacentChunkIds.includes(toId)) {\n return;\n }\n\n adjacentChunkIdsByChunkId[String(fromId)] = [\n ...existingAdjacentChunkIds,\n toId,\n ];\n}\n\nfunction clustersConnected(\n graph: DetectorGraph,\n leftCluster: readonly number[],\n rightCluster: readonly number[],\n): boolean {\n for (const leftChunkId of leftCluster) {\n for (const rightChunkId of rightCluster) {\n if (\n graph.edgeByKey[getReadingEdgeKey(leftChunkId, rightChunkId)] !==\n undefined ||\n graph.edgeByKey[getReadingEdgeKey(rightChunkId, leftChunkId)] !==\n undefined\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n\nfunction compareChunkBySentence(left: ChunkRecord, right: ChunkRecord): number {\n const [leftSerialId, leftSentenceIndex] = left.sentenceId;\n const [rightSerialId, rightSentenceIndex] = right.sentenceId;\n\n if (leftSerialId !== rightSerialId) {\n return leftSerialId - rightSerialId;\n }\n\n if (leftSentenceIndex !== rightSentenceIndex) {\n return leftSentenceIndex - rightSentenceIndex;\n }\n\n return left.id - right.id;\n}\n\nfunction compareEdgeQueueEntry(\n left: EdgeQueueEntry,\n right: EdgeQueueEntry,\n): number {\n if (left.value !== right.value) {\n return right.value - left.value;\n }\n\n if (left.leftClusterId !== right.leftClusterId) {\n return left.leftClusterId - right.leftClusterId;\n }\n\n return left.rightClusterId - right.rightClusterId;\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction computeAllFingerprints(\n graph: DetectorGraph,\n): Readonly<Record<string, Readonly<Record<string, number>>>> {\n const fingerprints = createNumberMatrixRecord();\n\n for (const startChunkId of graph.sortedChunkIds) {\n const concentrations = createNumberRecord();\n let currentLayer = [startChunkId];\n const visitedChunkIds = createBooleanRecord();\n\n concentrations[String(startChunkId)] = 1;\n visitedChunkIds[String(startChunkId)] = true;\n\n while (currentLayer.length > 0) {\n const nextLayerConcentrations = createNumberRecord();\n\n for (const currentChunkId of currentLayer) {\n const currentConcentration =\n concentrations[String(currentChunkId)] ?? 0;\n let totalWeight = 0;\n const neighbors: Array<readonly [number, number]> = [];\n\n for (const edge of graph.incomingEdgesByChunkId[\n String(currentChunkId)\n ] ?? []) {\n if (visitedChunkIds[String(edge.fromId)] === true) {\n continue;\n }\n\n const weight =\n graph.fingerprintEdgeWeightsByKey[\n getReadingEdgeKey(edge.fromId, currentChunkId)\n ] ??\n graph.fingerprintEdgeWeightsByKey[\n getReadingEdgeKey(currentChunkId, edge.fromId)\n ] ??\n DEFAULT_LINK_STRENGTH_WEIGHT;\n\n neighbors.push([edge.fromId, weight]);\n totalWeight += weight;\n }\n\n for (const edge of graph.outgoingEdgesByChunkId[\n String(currentChunkId)\n ] ?? []) {\n if (visitedChunkIds[String(edge.toId)] === true) {\n continue;\n }\n\n const weight =\n graph.fingerprintEdgeWeightsByKey[\n getReadingEdgeKey(currentChunkId, edge.toId)\n ] ??\n graph.fingerprintEdgeWeightsByKey[\n getReadingEdgeKey(edge.toId, currentChunkId)\n ] ??\n DEFAULT_LINK_STRENGTH_WEIGHT;\n\n neighbors.push([edge.toId, weight]);\n totalWeight += weight;\n }\n\n if (totalWeight <= 0) {\n continue;\n }\n\n for (const [neighborChunkId, weight] of neighbors) {\n const contribution =\n currentConcentration * (weight / totalWeight) * DECAY_FACTOR;\n\n nextLayerConcentrations[String(neighborChunkId)] =\n (nextLayerConcentrations[String(neighborChunkId)] ?? 0) +\n contribution;\n }\n }\n\n currentLayer = Object.keys(nextLayerConcentrations).map(Number);\n\n for (const nextChunkId of currentLayer) {\n concentrations[String(nextChunkId)] =\n nextLayerConcentrations[String(nextChunkId)] ?? 0;\n visitedChunkIds[String(nextChunkId)] = true;\n }\n }\n\n const totalConcentration = Object.values(concentrations).reduce(\n (sum, concentration) => sum + concentration,\n 0,\n );\n\n if (totalConcentration > 0) {\n for (const chunkId of Object.keys(concentrations)) {\n concentrations[chunkId] =\n (concentrations[chunkId] ?? 0) / totalConcentration;\n }\n }\n\n fingerprints[String(startChunkId)] = concentrations;\n }\n\n return fingerprints;\n}\n\nfunction computeClusterWordsCount(\n graph: DetectorGraph,\n cluster: readonly number[],\n): number {\n return cluster.reduce((sum, chunkId) => {\n return sum + (graph.chunksById[String(chunkId)]?.wordsCount ?? 0);\n }, 0);\n}\n\nfunction computeMergeValue(\n graph: DetectorGraph,\n leftCluster: readonly number[],\n rightCluster: readonly number[],\n fingerprints: Readonly<Record<string, Readonly<Record<string, number>>>>,\n): number {\n let edgeCount = 0;\n let minValue = Number.POSITIVE_INFINITY;\n\n for (const leftChunkId of leftCluster) {\n for (const rightChunkId of rightCluster) {\n const edge =\n graph.edgeByKey[getReadingEdgeKey(leftChunkId, rightChunkId)] ??\n graph.edgeByKey[getReadingEdgeKey(rightChunkId, leftChunkId)];\n\n if (edge === undefined) {\n continue;\n }\n\n edgeCount += 1;\n\n const leftFingerprint = fingerprints[String(leftChunkId)] ?? {};\n const rightFingerprint = fingerprints[String(rightChunkId)] ?? {};\n let squaredSum = 0;\n\n for (const targetChunkId of Object.keys(leftFingerprint)) {\n const diff =\n (leftFingerprint[targetChunkId] ?? 0) -\n (rightFingerprint[targetChunkId] ?? 0);\n\n squaredSum += diff * diff;\n }\n\n let deltaValue = getLinkStrengthWeight(edge.strength);\n const leftChunk = graph.chunksById[String(leftChunkId)];\n const rightChunk = graph.chunksById[String(rightChunkId)];\n\n if (\n leftChunk?.retention !== undefined &&\n rightChunk?.retention !== undefined\n ) {\n deltaValue += ATTRIBUTE_BONUS_WEIGHT;\n }\n if (\n leftChunk?.importance !== undefined &&\n rightChunk?.importance !== undefined\n ) {\n deltaValue += ATTRIBUTE_BONUS_WEIGHT;\n }\n\n const distance = Math.sqrt(squaredSum);\n const value = (1 - distance / 2) * deltaValue;\n\n minValue = Math.min(minValue, value);\n }\n }\n\n if (edgeCount === 0) {\n return Number.NEGATIVE_INFINITY;\n }\n\n return minValue;\n}\n\nfunction createBooleanRecord(): Record<string, boolean | undefined> {\n return Object.create(null) as Record<string, boolean | undefined>;\n}\n\nfunction createDetectorGraph(\n chunks: readonly ChunkRecord[],\n edges: readonly ReadingEdgeRecord[],\n): DetectorGraph {\n const chunksById = createChunkRecord();\n const edgeByKey = createEdgeRecord();\n const incomingEdgesByChunkId = createReadingEdgeListRecord();\n const outgoingEdgesByChunkId = createReadingEdgeListRecord();\n const sortedChunkIds = [...chunks]\n .map((chunk) => chunk.id)\n .sort(compareNumber);\n const undirectedAdjacentChunkIdsByChunkId = createNumberListRecord();\n\n for (const chunk of chunks) {\n chunksById[String(chunk.id)] = chunk;\n }\n\n for (const edge of edges) {\n if (\n chunksById[String(edge.fromId)] === undefined ||\n chunksById[String(edge.toId)] === undefined\n ) {\n continue;\n }\n\n const edgeKey = getReadingEdgeKey(edge.fromId, edge.toId);\n\n edgeByKey[edgeKey] = edge;\n if (incomingEdgesByChunkId[String(edge.toId)] === undefined) {\n incomingEdgesByChunkId[String(edge.toId)] = [];\n }\n if (outgoingEdgesByChunkId[String(edge.fromId)] === undefined) {\n outgoingEdgesByChunkId[String(edge.fromId)] = [];\n }\n\n incomingEdgesByChunkId[String(edge.toId)]?.push(edge);\n outgoingEdgesByChunkId[String(edge.fromId)]?.push(edge);\n attachChunkAdjacency(\n undirectedAdjacentChunkIdsByChunkId,\n edge.fromId,\n edge.toId,\n );\n attachChunkAdjacency(\n undirectedAdjacentChunkIdsByChunkId,\n edge.toId,\n edge.fromId,\n );\n }\n\n return {\n chunksById,\n edgeByKey,\n fingerprintEdgeWeightsByKey: computeFingerprintEdgeWeights(\n chunksById,\n edges,\n ),\n incomingEdgesByChunkId,\n outgoingEdgesByChunkId,\n sortedChunkIds,\n undirectedAdjacentChunkIdsByChunkId,\n };\n}\n\nfunction createChunkRecord(): Record<string, ChunkRecord | undefined> {\n return Object.create(null) as Record<string, ChunkRecord | undefined>;\n}\n\nfunction createEdgeQueueEntry(\n leftClusterId: number,\n rightClusterId: number,\n value: number,\n): EdgeQueueEntry {\n return {\n leftClusterId,\n rightClusterId,\n value,\n };\n}\n\nfunction createEdgeRecord(): Record<string, ReadingEdgeRecord | undefined> {\n return Object.create(null) as Record<string, ReadingEdgeRecord | undefined>;\n}\n\nfunction createReadingEdgeListRecord(): Record<\n string,\n ReadingEdgeRecord[] | undefined\n> {\n return Object.create(null) as Record<string, ReadingEdgeRecord[] | undefined>;\n}\n\nfunction createNumberMatrixRecord(): Record<string, Record<string, number>> {\n return Object.create(null) as Record<string, Record<string, number>>;\n}\n\nfunction createNumberListRecord(): Record<string, number[] | undefined> {\n return Object.create(null) as Record<string, number[] | undefined>;\n}\n\nfunction createNumberRecord(): Record<string, number> {\n return Object.create(null) as Record<string, number>;\n}\n\nfunction createOptionalNumberRecord(): Record<string, number | undefined> {\n return Object.create(null) as Record<string, number | undefined>;\n}\n\nfunction getClusterPairKey(\n leftClusterId: number,\n rightClusterId: number,\n): string {\n return leftClusterId < rightClusterId\n ? getReadingEdgeKey(leftClusterId, rightClusterId)\n : getReadingEdgeKey(rightClusterId, leftClusterId);\n}\n\nfunction getLinkStrengthWeight(strength: string | undefined): number {\n switch (strength) {\n case \"critical\":\n return 9;\n case \"important\":\n return 3;\n case \"helpful\":\n return 1;\n default:\n return DEFAULT_LINK_STRENGTH_WEIGHT;\n }\n}\n\nfunction greedyMerge(\n graph: DetectorGraph,\n fingerprints: Readonly<Record<string, Readonly<Record<string, number>>>>,\n config: MergeConfig,\n initialClusters: ReadonlyMap<number, readonly number[]> | undefined,\n): Map<number, number[]> {\n const clusters =\n initialClusters === undefined\n ? new Map<number, number[]>(\n graph.sortedChunkIds.map((chunkId) => [chunkId, [chunkId]]),\n )\n : new Map(\n [...initialClusters.entries()].map(([clusterId, chunkIds]) => [\n clusterId,\n [...chunkIds],\n ]),\n );\n const nodeToCluster = createOptionalNumberRecord();\n const edgeQueue: EdgeQueueEntry[] = [];\n const queuedClusterPairs = createBooleanRecord();\n\n for (const [clusterId, chunkIds] of clusters.entries()) {\n for (const chunkId of chunkIds) {\n nodeToCluster[String(chunkId)] = clusterId;\n }\n }\n\n for (const leftClusterId of clusters.keys()) {\n for (const rightClusterId of clusters.keys()) {\n if (leftClusterId >= rightClusterId) {\n continue;\n }\n\n const leftCluster = clusters.get(leftClusterId);\n const rightCluster = clusters.get(rightClusterId);\n\n if (leftCluster === undefined || rightCluster === undefined) {\n continue;\n }\n if (!clustersConnected(graph, leftCluster, rightCluster)) {\n continue;\n }\n\n edgeQueue.push(\n createEdgeQueueEntry(\n leftClusterId,\n rightClusterId,\n computeMergeValue(graph, leftCluster, rightCluster, fingerprints),\n ),\n );\n queuedClusterPairs[getClusterPairKey(leftClusterId, rightClusterId)] =\n true;\n }\n }\n\n while (edgeQueue.length > 0) {\n edgeQueue.sort(compareEdgeQueueEntry);\n\n const nextEntry = edgeQueue.shift();\n\n if (nextEntry === undefined) {\n continue;\n }\n\n const currentLeftClusterId = nodeToCluster[String(nextEntry.leftClusterId)];\n const currentRightClusterId =\n nodeToCluster[String(nextEntry.rightClusterId)];\n\n if (\n currentLeftClusterId === undefined ||\n currentRightClusterId === undefined ||\n currentLeftClusterId === currentRightClusterId\n ) {\n continue;\n }\n\n const leftCluster = clusters.get(currentLeftClusterId);\n const rightCluster = clusters.get(currentRightClusterId);\n\n if (leftCluster === undefined || rightCluster === undefined) {\n continue;\n }\n if (!clustersConnected(graph, leftCluster, rightCluster)) {\n continue;\n }\n\n if (\n config.enableBonus &&\n leftCluster.length >= 2 &&\n rightCluster.length >= 2\n ) {\n continue;\n }\n\n if (\n computeClusterWordsCount(graph, leftCluster) +\n computeClusterWordsCount(graph, rightCluster) >\n config.snakeWordsCount\n ) {\n continue;\n }\n\n leftCluster.push(...rightCluster);\n clusters.delete(currentRightClusterId);\n\n for (const chunkId of rightCluster) {\n nodeToCluster[String(chunkId)] = currentLeftClusterId;\n }\n\n const neighborChunkIds = new Set<number>();\n\n for (const chunkId of leftCluster) {\n for (const neighborChunkId of graph.undirectedAdjacentChunkIdsByChunkId[\n String(chunkId)\n ] ?? []) {\n neighborChunkIds.add(neighborChunkId);\n }\n }\n\n for (const neighborChunkId of neighborChunkIds) {\n const neighborClusterId = nodeToCluster[String(neighborChunkId)];\n\n if (\n neighborClusterId === undefined ||\n neighborClusterId === currentLeftClusterId\n ) {\n continue;\n }\n\n const neighborCluster = clusters.get(neighborClusterId);\n\n if (neighborCluster === undefined) {\n continue;\n }\n\n const clusterPairKey = getClusterPairKey(\n currentLeftClusterId,\n neighborClusterId,\n );\n\n if (queuedClusterPairs[clusterPairKey] === true) {\n continue;\n }\n\n edgeQueue.push(\n createEdgeQueueEntry(\n Math.min(currentLeftClusterId, neighborClusterId),\n Math.max(currentLeftClusterId, neighborClusterId),\n computeMergeValue(graph, leftCluster, neighborCluster, fingerprints),\n ),\n );\n queuedClusterPairs[clusterPairKey] = true;\n }\n }\n\n return clusters;\n}\n\nfunction isWeaklyConnected(graph: DetectorGraph): boolean {\n const firstChunkId = graph.sortedChunkIds[0];\n\n if (firstChunkId === undefined) {\n return true;\n }\n\n const visitedChunkIds = createBooleanRecord();\n const stack = [firstChunkId];\n\n visitedChunkIds[String(firstChunkId)] = true;\n\n while (stack.length > 0) {\n const currentChunkId = stack.pop();\n\n if (currentChunkId === undefined) {\n continue;\n }\n\n for (const nextChunkId of graph.undirectedAdjacentChunkIdsByChunkId[\n String(currentChunkId)\n ] ?? []) {\n if (visitedChunkIds[String(nextChunkId)] === true) {\n continue;\n }\n\n visitedChunkIds[String(nextChunkId)] = true;\n stack.push(nextChunkId);\n }\n }\n\n return graph.sortedChunkIds.every(\n (chunkId) => visitedChunkIds[String(chunkId)] === true,\n );\n}\n\nfunction validateFingerprints(\n fingerprints: Readonly<Record<string, Readonly<Record<string, number>>>>,\n): void {\n const firstChunkId = Object.keys(fingerprints)[0];\n\n if (firstChunkId === undefined) {\n return;\n }\n\n const referenceChunkIds = Object.keys(\n fingerprints[firstChunkId] ?? {},\n ).sort();\n\n for (const [chunkId, fingerprint] of Object.entries(fingerprints)) {\n const fingerprintChunkIds = Object.keys(fingerprint).sort();\n\n if (\n fingerprintChunkIds.length !== referenceChunkIds.length ||\n fingerprintChunkIds.some(\n (targetChunkId, index) => targetChunkId !== referenceChunkIds[index],\n )\n ) {\n throw new Error(`Fingerprint structure mismatch for chunk ${chunkId}`);\n }\n }\n}\n\nfunction computeFingerprintEdgeWeights(\n chunksById: Readonly<Record<string, ChunkRecord | undefined>>,\n edges: readonly ReadingEdgeRecord[],\n): Readonly<Record<string, number>> {\n const totalStrengthsByChunkId = createNumberRecord();\n\n for (const edge of edges) {\n if (\n chunksById[String(edge.fromId)] === undefined ||\n chunksById[String(edge.toId)] === undefined\n ) {\n continue;\n }\n\n const strengthWeight = getLinkStrengthWeight(edge.strength);\n\n totalStrengthsByChunkId[String(edge.fromId)] =\n (totalStrengthsByChunkId[String(edge.fromId)] ?? 0) + strengthWeight;\n totalStrengthsByChunkId[String(edge.toId)] =\n (totalStrengthsByChunkId[String(edge.toId)] ?? 0) + strengthWeight;\n }\n\n const edgeWeightsByKey = createNumberRecord();\n\n for (const edge of edges) {\n const fromChunk = chunksById[String(edge.fromId)];\n const toChunk = chunksById[String(edge.toId)];\n\n if (fromChunk === undefined || toChunk === undefined) {\n continue;\n }\n\n const strengthWeight = getLinkStrengthWeight(edge.strength);\n const fromTotalStrength = totalStrengthsByChunkId[String(edge.fromId)] ?? 0;\n const toTotalStrength = totalStrengthsByChunkId[String(edge.toId)] ?? 0;\n const fromHalfWeight =\n fromTotalStrength === 0\n ? 0\n : fromChunk.weight * (strengthWeight / fromTotalStrength);\n const toHalfWeight =\n toTotalStrength === 0\n ? 0\n : toChunk.weight * (strengthWeight / toTotalStrength);\n const finalWeight = Math.max(\n fromHalfWeight + toHalfWeight,\n MIN_EDGE_WEIGHT,\n );\n\n edgeWeightsByKey[getReadingEdgeKey(edge.fromId, edge.toId)] = finalWeight;\n edgeWeightsByKey[getReadingEdgeKey(edge.toId, edge.fromId)] = finalWeight;\n }\n\n return edgeWeightsByKey;\n}\n\ninterface EdgeQueueEntry {\n readonly leftClusterId: number;\n readonly rightClusterId: number;\n readonly value: number;\n}\n","import type {\n ChunkRecord,\n Document,\n ReadingEdgeRecord,\n SentenceGroupRecord,\n} from \"../document/index.js\";\nimport type { ReaderChunk, ReaderGraphDelta } from \"../reader/index.js\";\nimport { groupSegments } from \"./grouping.js\";\nimport { buildSnakeGraph } from \"./snake-graph-builder.js\";\nimport {\n detectSnakesInComponent,\n splitConnectedComponents,\n} from \"./snake-detector.js\";\nimport {\n computeChunkWeights,\n computeReadingEdgeWeights,\n getReadingEdgeKey,\n} from \"./weights.js\";\n\nconst DEFAULT_SNAKE_WORDS_COUNT = 280;\n\nexport class Topology {\n readonly #chunkIds: number[] = [];\n readonly #chunksById = createChunkRecord();\n readonly #document: Document;\n readonly #edgeKeys: string[] = [];\n readonly #edgesByKey = createEdgeRecord();\n readonly #groupWordsCount: number;\n readonly #serialId: number;\n\n public constructor(\n document: Document,\n serialId: number,\n groupWordsCount: number,\n ) {\n this.#document = document;\n this.#groupWordsCount = groupWordsCount;\n this.#serialId = serialId;\n }\n\n public accept(delta: ReaderGraphDelta): void {\n for (const chunk of delta.chunks) {\n this.#saveChunk(chunk);\n }\n\n if (delta.importanceAnnotations !== undefined) {\n for (const annotation of delta.importanceAnnotations) {\n const chunk = this.#chunksById[String(annotation.chunkId)];\n\n if (chunk === undefined) {\n continue;\n }\n\n this.#chunksById[String(annotation.chunkId)] = {\n ...chunk,\n importance: annotation.importance,\n };\n }\n }\n\n for (const edge of delta.edges) {\n this.#saveEdge(edge);\n }\n }\n\n public async finalize(): Promise<void> {\n const chunks = this.#listChunks();\n const edges = this.#listEdges();\n const chunkWeights = computeChunkWeights(chunks);\n const edgeWeights = computeReadingEdgeWeights({\n chunkWeights,\n edges,\n });\n const weightedChunks = chunks.map((chunk) => ({\n ...chunk,\n weight: chunkWeights[String(chunk.id)] ?? 0,\n }));\n const weightedEdges = edges.map((edge) => ({\n ...edge,\n weight: edgeWeights[getReadingEdgeKey(edge.fromId, edge.toId)] ?? 0,\n }));\n const sentenceGroups = await groupSegments({\n chunks: weightedChunks,\n edges: weightedEdges,\n fragments: this.#document.getSerialFragments(this.#serialId),\n groupWordsCount: this.#groupWordsCount,\n serialId: this.#serialId,\n });\n const snakeTopology = buildSnakeTopology({\n chunks: weightedChunks,\n edges: weightedEdges,\n sentenceGroups,\n serialId: this.#serialId,\n });\n\n await this.#document.serials.ensure(this.#serialId);\n\n const chunkIdMap = new Map<number, number>();\n\n for (const chunk of weightedChunks) {\n const createdChunk = await this.#document.chunks.create({\n content: chunk.content,\n generation: chunk.generation,\n label: chunk.label,\n sentenceId: chunk.sentenceId,\n sentenceIds: chunk.sentenceIds,\n weight: chunk.weight,\n wordsCount: chunk.wordsCount,\n ...(chunk.importance === undefined\n ? {}\n : { importance: chunk.importance }),\n ...(chunk.retention === undefined\n ? {}\n : { retention: chunk.retention }),\n });\n\n chunkIdMap.set(chunk.id, createdChunk.id);\n }\n\n for (const edge of weightedEdges) {\n const fromId = chunkIdMap.get(edge.fromId);\n const toId = chunkIdMap.get(edge.toId);\n\n if (fromId === undefined || toId === undefined) {\n continue;\n }\n\n await this.#document.readingEdges.save({\n ...edge,\n fromId,\n toId,\n });\n }\n\n await this.#document.fragmentGroups.saveMany(sentenceGroups);\n\n const snakeIds: number[] = [];\n\n for (const snake of snakeTopology.snakes) {\n snakeIds.push(\n await this.#document.snakes.create({\n firstLabel: snake.firstLabel,\n groupId: snake.groupId,\n lastLabel: snake.lastLabel,\n localSnakeId: snake.localSnakeId,\n serialId: this.#serialId,\n size: snake.size,\n weight: snake.weight,\n wordsCount: snake.wordsCount,\n }),\n );\n }\n\n for (const snakeChunk of snakeTopology.snakeChunks) {\n const snakeId = snakeIds[snakeChunk.snakeIndex];\n\n if (snakeId === undefined) {\n continue;\n }\n\n await this.#document.snakeChunks.save({\n chunkId: chunkIdMap.get(snakeChunk.chunkId) ?? snakeChunk.chunkId,\n position: snakeChunk.position,\n snakeId,\n });\n }\n\n for (const snakeEdge of snakeTopology.snakeEdges) {\n const fromSnakeId = snakeIds[snakeEdge.fromSnakeIndex];\n const toSnakeId = snakeIds[snakeEdge.toSnakeIndex];\n\n if (fromSnakeId === undefined || toSnakeId === undefined) {\n continue;\n }\n\n await this.#document.snakeEdges.save({\n fromSnakeId,\n toSnakeId,\n weight: snakeEdge.weight,\n });\n }\n }\n\n #listChunks(): ChunkRecord[] {\n return [...this.#chunkIds]\n .sort(compareNumber)\n .map((chunkId) => this.#chunksById[String(chunkId)])\n .filter((chunk): chunk is ChunkRecord => chunk !== undefined);\n }\n\n #listEdges(): ReadingEdgeRecord[] {\n return [...this.#edgeKeys]\n .sort(compareEdgeKey)\n .map((edgeKey) => this.#edgesByKey[edgeKey])\n .filter((edge): edge is ReadingEdgeRecord => edge !== undefined);\n }\n\n #saveChunk(chunk: ReaderChunk): void {\n const chunkId = String(chunk.id);\n\n if (this.#chunksById[chunkId] === undefined) {\n this.#chunkIds.push(chunk.id);\n }\n\n const importance = chunk.importance;\n const retention = chunk.retention;\n\n this.#chunksById[chunkId] = {\n content: chunk.content,\n generation: chunk.generation,\n id: chunk.id,\n label: chunk.label,\n sentenceId: chunk.sentenceId,\n sentenceIds: [...chunk.sentenceIds],\n wordsCount: chunk.wordsCount,\n weight: 0,\n ...(importance === undefined ? {} : { importance }),\n ...(retention === undefined ? {} : { retention }),\n };\n }\n\n #saveEdge(edge: ReaderGraphDelta[\"edges\"][number]): void {\n const edgeKey = getReadingEdgeKey(edge.fromId, edge.toId);\n\n if (this.#edgesByKey[edgeKey] === undefined) {\n this.#edgeKeys.push(edgeKey);\n }\n\n const strength = edge.strength;\n\n this.#edgesByKey[edgeKey] = {\n fromId: edge.fromId,\n toId: edge.toId,\n weight: 0,\n ...(strength === undefined ? {} : { strength }),\n };\n }\n}\n\nfunction compareEdgeKey(left: string, right: string): number {\n const [leftFromIdText = \"\", leftToIdText = \"\"] = left.split(\":\");\n const [rightFromIdText = \"\", rightToIdText = \"\"] = right.split(\":\");\n const leftFromId = Number(leftFromIdText);\n const rightFromId = Number(rightFromIdText);\n\n if (leftFromId !== rightFromId) {\n return leftFromId - rightFromId;\n }\n\n return Number(leftToIdText) - Number(rightToIdText);\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction createChunkRecord(): Record<string, ChunkRecord | undefined> {\n return Object.create(null) as Record<string, ChunkRecord | undefined>;\n}\n\nfunction createEdgeRecord(): Record<string, ReadingEdgeRecord | undefined> {\n return Object.create(null) as Record<string, ReadingEdgeRecord | undefined>;\n}\n\ninterface SnakeDraft {\n readonly firstLabel: string;\n readonly groupId: number;\n readonly lastLabel: string;\n readonly localSnakeId: number;\n readonly size: number;\n readonly weight: number;\n readonly wordsCount: number;\n}\n\ninterface SnakeChunkDraft {\n readonly chunkId: number;\n readonly position: number;\n readonly snakeIndex: number;\n}\n\ninterface SnakeEdgeDraft {\n readonly fromSnakeIndex: number;\n readonly toSnakeIndex: number;\n readonly weight: number;\n}\n\nfunction buildSnakeTopology(input: {\n chunks: readonly ChunkRecord[];\n edges: readonly ReadingEdgeRecord[];\n sentenceGroups: readonly SentenceGroupRecord[];\n serialId: number;\n}): {\n readonly snakeChunks: readonly SnakeChunkDraft[];\n readonly snakeEdges: readonly SnakeEdgeDraft[];\n readonly snakes: readonly SnakeDraft[];\n} {\n const chunksById = createChunkRecord();\n const chunkIdsByGroupId = createNumberListRecord();\n const edgesByGroupId = createReadingEdgeListRecord();\n const sentenceRangesByGroupId: Array<{\n readonly endSentenceIndex: number;\n readonly groupId: number;\n readonly startSentenceIndex: number;\n }> = [];\n\n for (const sentenceGroup of input.sentenceGroups) {\n if (sentenceGroup.serialId !== input.serialId) {\n continue;\n }\n\n sentenceRangesByGroupId.push(sentenceGroup);\n }\n\n for (const chunk of input.chunks) {\n const groupId = findSentenceGroupId(\n sentenceRangesByGroupId,\n chunk.sentenceId[1],\n );\n\n chunksById[String(chunk.id)] = chunk;\n if (groupId === undefined) {\n continue;\n }\n if (chunkIdsByGroupId[String(groupId)] === undefined) {\n chunkIdsByGroupId[String(groupId)] = [];\n }\n chunkIdsByGroupId[String(groupId)]?.push(chunk.id);\n }\n\n for (const edge of input.edges) {\n const fromChunk = chunksById[String(edge.fromId)];\n const toChunk = chunksById[String(edge.toId)];\n\n if (fromChunk === undefined || toChunk === undefined) {\n continue;\n }\n\n const fromGroupId = findSentenceGroupId(\n sentenceRangesByGroupId,\n fromChunk.sentenceId[1],\n );\n const toGroupId = findSentenceGroupId(\n sentenceRangesByGroupId,\n toChunk.sentenceId[1],\n );\n\n if (fromGroupId === undefined || toGroupId === undefined) {\n continue;\n }\n if (fromGroupId !== toGroupId) {\n continue;\n }\n if (edgesByGroupId[String(fromGroupId)] === undefined) {\n edgesByGroupId[String(fromGroupId)] = [];\n }\n\n edgesByGroupId[String(fromGroupId)]?.push(edge);\n }\n\n const snakes: SnakeDraft[] = [];\n const snakeChunks: SnakeChunkDraft[] = [];\n const snakeEdges: SnakeEdgeDraft[] = [];\n\n for (const groupId of listSortedRecordNumbers(chunkIdsByGroupId)) {\n const groupChunkIds = [...(chunkIdsByGroupId[String(groupId)] ?? [])].sort(\n compareNumber,\n );\n const groupEdges = edgesByGroupId[String(groupId)] ?? [];\n const groupSnakeChunkIds: number[][] = [];\n\n for (const componentChunkIds of splitConnectedComponents({\n chunkIds: groupChunkIds,\n edges: groupEdges,\n })) {\n const componentChunkIdRecord = createBooleanRecord();\n\n for (const chunkId of componentChunkIds) {\n componentChunkIdRecord[String(chunkId)] = true;\n }\n\n groupSnakeChunkIds.push(\n ...detectSnakesInComponent({\n chunks: componentChunkIds\n .map((chunkId) => chunksById[String(chunkId)])\n .filter((chunk): chunk is ChunkRecord => chunk !== undefined),\n edges: groupEdges.filter(\n (edge) =>\n componentChunkIdRecord[String(edge.fromId)] === true &&\n componentChunkIdRecord[String(edge.toId)] === true,\n ),\n snakeWordsCount: DEFAULT_SNAKE_WORDS_COUNT,\n }),\n );\n }\n\n const snakeStartIndex = snakes.length;\n\n for (const [localSnakeId, snakeChunkIds] of groupSnakeChunkIds.entries()) {\n const firstChunkId = snakeChunkIds[0];\n const lastChunkId = snakeChunkIds[snakeChunkIds.length - 1];\n const firstChunk =\n firstChunkId === undefined\n ? undefined\n : chunksById[String(firstChunkId)];\n const lastChunk =\n lastChunkId === undefined ? undefined : chunksById[String(lastChunkId)];\n\n if (firstChunk === undefined || lastChunk === undefined) {\n continue;\n }\n\n const snakeIndex = snakeStartIndex + localSnakeId;\n\n snakes.push({\n firstLabel: firstChunk.label,\n groupId,\n lastLabel: lastChunk.label,\n localSnakeId,\n size: snakeChunkIds.length,\n weight: snakeChunkIds.reduce((sum, chunkId) => {\n return sum + (chunksById[String(chunkId)]?.weight ?? 0);\n }, 0),\n wordsCount: snakeChunkIds.reduce((sum, chunkId) => {\n return sum + (chunksById[String(chunkId)]?.wordsCount ?? 0);\n }, 0),\n });\n\n for (const [position, chunkId] of snakeChunkIds.entries()) {\n snakeChunks.push({\n chunkId,\n position,\n snakeIndex,\n });\n }\n }\n\n snakeEdges.push(\n ...buildSnakeGraph({\n chunksById,\n edges: groupEdges,\n snakes: groupSnakeChunkIds,\n }).map((edge) => ({\n fromSnakeIndex: snakeStartIndex + edge.fromSnakeIndex,\n toSnakeIndex: snakeStartIndex + edge.toSnakeIndex,\n weight: edge.weight,\n })),\n );\n }\n\n return {\n snakeChunks,\n snakeEdges,\n snakes,\n };\n}\n\nfunction findSentenceGroupId(\n groups: readonly {\n readonly endSentenceIndex: number;\n readonly groupId: number;\n readonly startSentenceIndex: number;\n }[],\n sentenceIndex: number,\n): number | undefined {\n return groups.find(\n (group) =>\n sentenceIndex >= group.startSentenceIndex &&\n sentenceIndex <= group.endSentenceIndex,\n )?.groupId;\n}\n\nfunction createBooleanRecord(): Record<string, boolean | undefined> {\n return Object.create(null) as Record<string, boolean | undefined>;\n}\n\nfunction createNumberListRecord(): Record<string, number[] | undefined> {\n return Object.create(null) as Record<string, number[] | undefined>;\n}\n\nfunction createReadingEdgeListRecord(): Record<\n string,\n ReadingEdgeRecord[] | undefined\n> {\n return Object.create(null) as Record<string, ReadingEdgeRecord[] | undefined>;\n}\n\nfunction listSortedRecordNumbers(\n record: Readonly<Record<string, number[] | undefined>>,\n): number[] {\n return Object.keys(record).map(Number).sort(compareNumber);\n}\n","export { Topology } from \"./topology.js\";\n","import { AsyncSemaphore } from \"./utils/async-semaphore.js\";\nimport type { Language } from \"./common/language.js\";\nimport {\n WIKI_GRAPH_EDITOR_SCOPES,\n WIKI_GRAPH_READER_SCOPES,\n type WikiGraphScope,\n} from \"./common/llm-scope.js\";\nimport type { LLM } from \"./llm/index.js\";\nimport type {\n ChunkRecord,\n Document,\n FragmentGroupStore,\n ReadingEdgeRecord,\n ReadonlyChunkStore,\n ReadonlyDocument,\n ReadonlyFragmentGroupStore,\n ReadonlyReadingEdgeStore,\n ReadonlySerialFragments,\n SentenceGroupRecord,\n SerialTextStream,\n ReadonlySnakeChunkStore,\n ReadonlySnakeEdgeStore,\n ReadonlySnakeStore,\n SerialRecord,\n SerialStore,\n SnakeChunkRecord,\n SnakeEdgeRecord,\n SnakeRecord,\n} from \"./document/index.js\";\nimport { Reader, segmentTextStream } from \"./reader/index.js\";\nimport type {\n ReaderChunk,\n ReaderGraphDelta,\n ReaderSegmenter,\n ReaderTextStream,\n} from \"./reader/index.js\";\nimport { compressText, type EditorOptions } from \"./editor/index.js\";\nimport { Topology } from \"./topology/index.js\";\n\nconst DEFAULT_COMPRESSION_RATIO = 0.2;\nconst DEFAULT_FRAGMENT_WORDS_COUNT = 320;\nconst DEFAULT_GENERATION_DECAY_FACTOR = 0.5;\nconst DEFAULT_GROUP_WORDS_COUNT = 3840;\nconst DEFAULT_MAX_CLUES = 10;\nconst DEFAULT_MAX_ITERATIONS = 5;\nconst DEFAULT_WORKING_MEMORY_CAPACITY = 7;\n\nexport interface GenerateSerialOptions {\n readonly extractionPrompt: string;\n readonly userLanguage?: Language;\n}\n\nexport type BuildSerialTopologyOptions = GenerateSerialOptions;\n\nexport interface BuildSerialSummaryOptions {\n readonly userLanguage?: Language;\n}\n\nexport interface WriteSerialSourceOptions {\n readonly segmenter?: ReaderSegmenter;\n}\n\nexport interface SerialDiscovery {\n readonly fragments: number;\n readonly words: number;\n}\n\nexport interface SerialProgressSink {\n begin?(input?: {\n readonly fragments: number;\n readonly words: number;\n }): Promise<void>;\n advance(wordsCount: number): Promise<void>;\n complete(finalWordsCount?: number): Promise<void>;\n}\n\ntype ReaderProgressScope =\n (typeof WIKI_GRAPH_READER_SCOPES)[keyof typeof WIKI_GRAPH_READER_SCOPES];\n\nexport type CreateSerialOptions = GenerateSerialOptions;\n\nexport interface SerialGenerationOptions {\n readonly document?: Document;\n readonly llm: LLM<WikiGraphScope>;\n readonly logDirPath?: string;\n readonly segmenter?: ReaderSegmenter;\n /** @deprecated Use `document` instead. */\n readonly workspace?: Document;\n}\n\nexport type SerialHubOptions = SerialGenerationOptions;\n\nexport async function discoverSerial(input: {\n readonly segmenter?: ReaderSegmenter;\n readonly stream: ReaderTextStream;\n}): Promise<SerialDiscovery> {\n let fragments = 0;\n let words = 0;\n\n for await (const fragment of streamFragments({\n maxWordsCount: DEFAULT_FRAGMENT_WORDS_COUNT,\n stream: segmentTextStream(input.stream, {\n ...(input.segmenter === undefined ? {} : { adapter: input.segmenter }),\n }),\n })) {\n fragments += 1;\n words += countFragmentWords(fragment.sentences);\n }\n\n return {\n fragments,\n words,\n };\n}\n\nexport async function writeSerialSource(\n document: Document,\n serialId: number,\n stream: ReaderTextStream,\n options: WriteSerialSourceOptions = {},\n): Promise<void> {\n const serialFragments = document.getSerialFragments(serialId);\n\n await serialFragments.writeTextStream(await collectTextStream(stream), {\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n await document.serials.bumpRevision(serialId);\n}\n\nexport class SerialGeneration {\n readonly #fragmentWordsCount = DEFAULT_FRAGMENT_WORDS_COUNT;\n readonly #fragmentGroups: FragmentGroupStore;\n readonly #idSemaphore = new AsyncSemaphore(1);\n readonly #llm: LLM<WikiGraphScope>;\n readonly #logDirPath: string | undefined;\n readonly #serials: SerialStore;\n readonly #segmenter: ReaderSegmenter | undefined;\n readonly #document: Document;\n readonly #writeSemaphore = new AsyncSemaphore(1);\n\n #nextChunkId = 1;\n\n public constructor(options: SerialGenerationOptions) {\n const document = resolveDocument(options);\n\n this.#fragmentGroups = document.fragmentGroups;\n this.#llm = options.llm;\n this.#logDirPath = options.logDirPath;\n this.#serials = document.serials;\n this.#segmenter = options.segmenter;\n this.#document = document;\n }\n\n public async generate(\n stream: ReaderTextStream,\n options: GenerateSerialOptions,\n progressTracker?: SerialProgressSink,\n ): Promise<Serial> {\n const serialId = await this.#createSerialId();\n await writeSerialSource(this.#document, serialId, stream, {\n ...(this.#segmenter === undefined ? {} : { segmenter: this.#segmenter }),\n });\n return await this.#generatePrepared(serialId, options, progressTracker);\n }\n\n public async create(\n stream: ReaderTextStream,\n options: GenerateSerialOptions,\n ): Promise<Serial> {\n return await this.generate(stream, options);\n }\n\n public async generateInto(\n serialId: number,\n stream: ReaderTextStream,\n options: GenerateSerialOptions,\n progressTracker?: SerialProgressSink,\n ): Promise<Serial> {\n await this.#createExplicitSerialId(serialId);\n await writeSerialSource(this.#document, serialId, stream, {\n ...(this.#segmenter === undefined ? {} : { segmenter: this.#segmenter }),\n });\n return await this.#generatePrepared(serialId, options, progressTracker);\n }\n\n public async buildTopologyInto(\n serialId: number,\n options: BuildSerialTopologyOptions,\n progressTracker?: SerialProgressSink,\n ): Promise<void> {\n await this.#buildTopology(\n serialId,\n options.extractionPrompt,\n options.userLanguage,\n progressTracker,\n );\n }\n\n public async buildSummary(\n serialId: number,\n options: BuildSerialSummaryOptions = {},\n ): Promise<Serial> {\n const summary = await this.#buildSummary(serialId, options.userLanguage);\n\n return new Serial(this.#document, serialId, summary);\n }\n\n async #generatePrepared(\n serialId: number,\n options: GenerateSerialOptions,\n progressTracker?: SerialProgressSink,\n ): Promise<Serial> {\n await this.#buildTopology(\n serialId,\n options.extractionPrompt,\n options.userLanguage,\n progressTracker,\n );\n\n const summary = await this.#buildSummary(serialId, options.userLanguage);\n await progressTracker?.complete();\n\n return new Serial(this.#document, serialId, summary);\n }\n\n async #allocateChunkId(): Promise<number> {\n return await this.#idSemaphore.use(() => {\n const chunkId = this.#nextChunkId;\n this.#nextChunkId += 1;\n return chunkId;\n });\n }\n\n async #buildSummary(\n serialId: number,\n userLanguage: Language | undefined,\n ): Promise<string> {\n const record = await getSerialRecord(this.#document, serialId);\n if (!record.topologyReady) {\n throw new Error(`Serial ${serialId} is not ready for summary`);\n }\n const existingSummary = await this.#document.readSummary(serialId);\n\n if (existingSummary !== undefined) {\n return existingSummary;\n }\n const serialFragments = this.#getSerialFragments(serialId);\n const fragmentIds = await serialFragments.listFragmentIds();\n\n if (fragmentIds.length <= 1) {\n const summary = await readSerialPassthroughSummary(\n serialFragments,\n fragmentIds,\n );\n\n await this.#writeSemaphore.use(\n async () => await this.#document.writeSummary(serialId, summary),\n );\n\n return summary;\n }\n const groupIds = await this.#fragmentGroups.listGroupIdsForSerial(serialId);\n const summaryParts: string[] = [];\n\n for (const groupId of groupIds) {\n const groupSummary = await compressText({\n ...this.#createEditorOptions({\n groupId,\n serialId,\n userLanguage,\n }),\n });\n if (groupSummary.trim() === \"\") {\n continue;\n }\n summaryParts.push(groupSummary);\n }\n const summary = summaryParts.join(\"\\n\\n\");\n\n await this.#writeSemaphore.use(\n async () => await this.#document.writeSummary(serialId, summary),\n );\n return summary;\n }\n\n async #buildTopology(\n serialId: number,\n extractionPrompt: string,\n userLanguage: Language | undefined,\n progressTracker?: SerialProgressSink,\n ): Promise<void> {\n const reader = new Reader({\n attention: {\n capacity: DEFAULT_WORKING_MEMORY_CAPACITY,\n generationDecayFactor: DEFAULT_GENERATION_DECAY_FACTOR,\n idGenerator: async () => await this.#allocateChunkId(),\n },\n extractionGuidance: extractionPrompt,\n llm: this.#llm,\n scopes: WIKI_GRAPH_READER_SCOPES,\n sentenceTextSource: this.#document,\n ...(this.#segmenter === undefined\n ? {}\n : {\n segmenter: this.#segmenter,\n }),\n ...(userLanguage === undefined\n ? {}\n : {\n userLanguage,\n }),\n });\n const topology = new Topology(\n this.#document,\n serialId,\n DEFAULT_GROUP_WORDS_COUNT,\n );\n const allChunks: ReaderChunk[] = [];\n const successorIdsByChunkId = createNumberListRecord();\n const serialFragments = this.#document.getSerialFragments(serialId);\n\n for (const fragment of await listSerialProcessingFragments(\n serialFragments,\n this.#fragmentWordsCount,\n )) {\n const wordsCount = countFragmentWords(fragment.sentences);\n\n await this.#processFragment({\n allChunks,\n fragment: {\n startSentenceIndex: fragment.startSentenceIndex,\n sentences: fragment.sentences,\n },\n reader,\n serialId,\n successorIdsByChunkId,\n topology,\n });\n // Progress only advances after the fragment is fully persisted and indexed.\n await progressTracker?.advance(wordsCount);\n }\n\n await this.#writeSemaphore.use(async () => {\n await topology.finalize();\n await this.#serials.setTopologyReady(serialId);\n await this.#serials.bumpRevision(serialId);\n });\n }\n\n async #processFragment(input: {\n readonly allChunks: ReaderChunk[];\n readonly fragment: {\n readonly startSentenceIndex: number;\n readonly sentences: ReadonlyArray<{\n readonly text: string;\n readonly wordsCount: number;\n }>;\n };\n readonly reader: Reader<ReaderProgressScope>;\n readonly serialId: number;\n readonly successorIdsByChunkId: Record<string, number[] | undefined>;\n readonly topology: Topology;\n }): Promise<void> {\n const sentences = input.fragment.sentences.map((sentence, index) => ({\n sentenceId: [\n input.serialId,\n input.fragment.startSentenceIndex + index,\n ] as const,\n text: sentence.text,\n wordsCount: sentence.wordsCount,\n }));\n const fragmentText = sentences.map((sentence) => sentence.text).join(\" \");\n const userFocused = await input.reader.extractUserFocused({\n sentences,\n text: fragmentText,\n });\n\n const bookCoherence = await input.reader.extractBookCoherence({\n sentences,\n text: fragmentText,\n userFocusedChunks: userFocused.delta.chunks,\n });\n\n saveDelta(\n input.allChunks,\n input.successorIdsByChunkId,\n input.topology,\n userFocused.delta,\n );\n saveDelta(\n input.allChunks,\n input.successorIdsByChunkId,\n input.topology,\n bookCoherence,\n );\n input.reader.completeFragment({\n allChunks: input.allChunks,\n getSuccessorChunkIds: (chunkId) =>\n input.successorIdsByChunkId[String(chunkId)] ?? [],\n });\n }\n\n async #createSerialId(): Promise<number> {\n return await this.#idSemaphore.use(\n async () => await this.#serials.create(),\n );\n }\n\n async #createExplicitSerialId(serialId: number): Promise<void> {\n await this.#idSemaphore.use(\n async () => await this.#serials.createWithId(serialId),\n );\n }\n\n #getSerialFragments(serialId: number): SerialTextStream {\n return this.#document.getSerialFragments(serialId);\n }\n\n #createEditorOptions(input: {\n groupId: number;\n serialId: number;\n userLanguage: Language | undefined;\n }): EditorOptions<WikiGraphScope> {\n return {\n compressionRatio: DEFAULT_COMPRESSION_RATIO,\n groupId: input.groupId,\n llm: this.#llm,\n maxClues: DEFAULT_MAX_CLUES,\n maxIterations: DEFAULT_MAX_ITERATIONS,\n scopes: WIKI_GRAPH_EDITOR_SCOPES,\n serialId: input.serialId,\n document: this.#document,\n ...(this.#logDirPath === undefined\n ? {}\n : {\n logDirPath: this.#logDirPath,\n }),\n ...(input.userLanguage === undefined\n ? {}\n : {\n userLanguage: input.userLanguage,\n }),\n };\n }\n}\n\nexport class Serial {\n readonly #id: number;\n readonly #summary: string;\n readonly #topology: SerialTopology;\n\n public constructor(\n document: ReadonlyDocument,\n serialId: number,\n summary: string,\n ) {\n this.#id = serialId;\n this.#summary = summary;\n this.#topology = new SerialTopology(document, serialId);\n }\n\n public get id(): number {\n return this.#id;\n }\n\n public getSummary(): string {\n return this.#summary;\n }\n\n public getTopology(): SerialTopology {\n return this.#topology;\n }\n}\n\nexport class SerialTopology {\n readonly #chunks: ReadonlyChunkStore;\n readonly #fragmentGroups: ReadonlyFragmentGroupStore;\n readonly #readingEdges: ReadonlyReadingEdgeStore;\n readonly #serialId: number;\n readonly #snakeChunks: ReadonlySnakeChunkStore;\n readonly #snakeEdges: ReadonlySnakeEdgeStore;\n readonly #snakes: ReadonlySnakeStore;\n\n public constructor(document: ReadonlyDocument, serialId: number) {\n this.#chunks = document.chunks;\n this.#fragmentGroups = document.fragmentGroups;\n this.#readingEdges = document.readingEdges;\n this.#serialId = serialId;\n this.#snakeChunks = document.snakeChunks;\n this.#snakeEdges = document.snakeEdges;\n this.#snakes = document.snakes;\n }\n\n public async listChunks(): Promise<readonly ChunkRecord[]> {\n return await this.#chunks.listBySerial(this.#serialId);\n }\n\n public async listEdges(): Promise<readonly ReadingEdgeRecord[]> {\n return await this.#readingEdges.listBySerial(this.#serialId);\n }\n\n public async listGroups(): Promise<readonly SentenceGroupRecord[]> {\n return await this.#fragmentGroups.listBySerial(this.#serialId);\n }\n\n public async listSnakeChunks(\n snakeId: number,\n ): Promise<readonly SnakeChunkRecord[]> {\n const snake = await this.#snakes.getById(snakeId);\n if (snake === undefined || snake.serialId !== this.#serialId) {\n throw new Error(`Snake ${snakeId} does not belong to this serial`);\n }\n return await this.#snakeChunks.listBySnake(snakeId);\n }\n\n public async listSnakeEdges(): Promise<readonly SnakeEdgeRecord[]> {\n return await this.#snakeEdges.listBySerial(this.#serialId);\n }\n\n public async listSnakes(): Promise<readonly SnakeRecord[]> {\n return await this.#snakes.listBySerial(this.#serialId);\n }\n}\n\nfunction appendSuccessor(\n successorIdsByChunkId: Record<string, number[] | undefined>,\n fromId: number,\n toId: number,\n): void {\n const existingSuccessors = successorIdsByChunkId[String(fromId)] ?? [];\n\n if (existingSuccessors.includes(toId)) {\n return;\n }\n\n successorIdsByChunkId[String(fromId)] = [...existingSuccessors, toId].sort(\n compareNumber,\n );\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction createNumberListRecord(): Record<string, number[] | undefined> {\n return Object.create(null) as Record<string, number[] | undefined>;\n}\n\nexport async function readSerial(\n document: ReadonlyDocument,\n serialId: number,\n): Promise<Serial> {\n const record = await getSerialRecord(document, serialId);\n\n if (!record.topologyReady) {\n throw new Error(`Serial ${serialId} is not ready`);\n }\n\n const summary = await document.readSummary(serialId);\n\n if (summary === undefined) {\n throw new Error(\n `Chapter ${serialId} summary is missing. Run \\`wg wikg://local/job add --input <chapter-uri> --task reading-summary --accept-cost\\` before export, or inspect the chapter with \\`wg <archive-uri>/chapter/${serialId}/source get\\`.`,\n );\n }\n\n return new Serial(document, serialId, summary);\n}\n\nfunction resolveDocument(options: SerialGenerationOptions): Document {\n const document = options.document ?? options.workspace;\n\n if (document === undefined) {\n throw new Error(\"SerialGeneration requires a document\");\n }\n\n return document;\n}\n\nasync function getSerialRecord(\n document: Pick<ReadonlyDocument, \"serials\">,\n serialId: number,\n): Promise<SerialRecord> {\n const record = await document.serials.getById(serialId);\n\n if (record === undefined) {\n throw new Error(\n `Chapter ${serialId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n return record;\n}\n\nfunction saveDelta(\n allChunks: ReaderChunk[],\n successorIdsByChunkId: Record<string, number[] | undefined>,\n topology: Topology,\n delta: ReaderGraphDelta,\n): void {\n topology.accept(delta);\n allChunks.push(...delta.chunks);\n\n for (const edge of delta.edges) {\n appendSuccessor(successorIdsByChunkId, edge.fromId, edge.toId);\n }\n}\n\nasync function* streamFragments(input: {\n maxWordsCount: number;\n stream: AsyncIterable<{\n readonly text: string;\n readonly wordsCount: number;\n }>;\n}): AsyncIterable<{\n readonly sentences: ReadonlyArray<{\n readonly text: string;\n readonly wordsCount: number;\n }>;\n}> {\n let currentSentences: Array<{\n readonly text: string;\n readonly wordsCount: number;\n }> = [];\n let currentWordsCount = 0;\n\n for await (const sentence of input.stream) {\n const sentenceText = sentence.text.trim();\n\n if (sentenceText === \"\") {\n continue;\n }\n if (\n currentSentences.length > 0 &&\n currentWordsCount + sentence.wordsCount > input.maxWordsCount\n ) {\n yield {\n sentences: currentSentences,\n };\n currentSentences = [];\n currentWordsCount = 0;\n }\n currentSentences.push({\n text: sentenceText,\n wordsCount: sentence.wordsCount,\n });\n currentWordsCount += sentence.wordsCount;\n }\n\n if (currentSentences.length > 0) {\n yield {\n sentences: currentSentences,\n };\n }\n}\n\nasync function listSerialProcessingFragments(\n fragments: ReadonlySerialFragments,\n maxWordsCount: number,\n): Promise<\n ReadonlyArray<{\n readonly startSentenceIndex: number;\n readonly sentences: ReadonlyArray<{\n readonly text: string;\n readonly wordsCount: number;\n }>;\n }>\n> {\n const sentences =\n fragments.listSentences === undefined\n ? await listFragmentSentences(fragments)\n : await fragments.listSentences();\n const batches: Array<{\n startSentenceIndex: number;\n sentences: Array<{\n readonly text: string;\n readonly wordsCount: number;\n }>;\n }> = [];\n let currentSentences: Array<{\n readonly text: string;\n readonly wordsCount: number;\n }> = [];\n let currentStartSentenceIndex = 0;\n let currentWordsCount = 0;\n\n for (let index = 0; index < sentences.length; index += 1) {\n const sentence = sentences[index];\n\n if (sentence === undefined || sentence.text.trim() === \"\") {\n continue;\n }\n if (\n currentSentences.length > 0 &&\n currentWordsCount + sentence.wordsCount > maxWordsCount\n ) {\n batches.push({\n startSentenceIndex: currentStartSentenceIndex,\n sentences: currentSentences,\n });\n currentSentences = [];\n currentWordsCount = 0;\n }\n if (currentSentences.length === 0) {\n currentStartSentenceIndex = index;\n }\n currentSentences.push(sentence);\n currentWordsCount += sentence.wordsCount;\n }\n\n if (currentSentences.length > 0) {\n batches.push({\n startSentenceIndex: currentStartSentenceIndex,\n sentences: currentSentences,\n });\n }\n\n return batches;\n}\n\nasync function listFragmentSentences(\n fragments: ReadonlySerialFragments,\n): Promise<\n ReadonlyArray<{\n readonly text: string;\n readonly wordsCount: number;\n }>\n> {\n const records = await Promise.all(\n (await fragments.listFragmentIds()).map(\n async (fragmentId) => await fragments.getFragment(fragmentId),\n ),\n );\n\n return records\n .sort((left, right) => left.fragmentId - right.fragmentId)\n .flatMap((fragment) => fragment.sentences);\n}\n\nasync function collectTextStream(stream: ReaderTextStream): Promise<string> {\n const parts: string[] = [];\n\n for await (const chunk of stream) {\n parts.push(chunk);\n }\n\n return parts.join(\"\");\n}\n\nfunction countFragmentWords(\n sentences: ReadonlyArray<{\n readonly text: string;\n readonly wordsCount: number;\n }>,\n): number {\n return sentences.reduce((sum, sentence) => sum + sentence.wordsCount, 0);\n}\n\nasync function readSerialPassthroughSummary(\n fragments: ReadonlySerialFragments,\n fragmentIds: readonly number[],\n): Promise<string> {\n if (fragmentIds.length === 0) {\n return \"\";\n }\n\n const records = await Promise.all(\n fragmentIds.map(\n async (fragmentId) => await fragments.getFragment(fragmentId),\n ),\n );\n\n return records\n .flatMap((fragment: Awaited<ReturnType<typeof fragments.getFragment>>) =>\n fragment.sentences.map((sentence) => sentence.text),\n )\n .join(\" \")\n .trim();\n}\n","import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { PassThrough, type Readable } from \"stream\";\nimport type { Entry, ZipFile } from \"yauzl\";\nimport { open } from \"yauzl\";\n\nexport class EpubArchive {\n readonly #path: string;\n readonly #zipFile: ZipFile;\n readonly #entries: ReadonlyMap<string, Entry>;\n #closed = false;\n\n public constructor(\n path: string,\n zipFile: ZipFile,\n entries: ReadonlyMap<string, Entry>,\n ) {\n this.#path = path;\n this.#zipFile = zipFile;\n this.#entries = entries;\n }\n\n public static async open(path: string): Promise<EpubArchive> {\n const zipFile = await openZipFile(path);\n const entries = await indexEntries(zipFile);\n\n return new EpubArchive(path, zipFile, entries);\n }\n\n public close(): Promise<void> {\n if (this.#closed) {\n return Promise.resolve();\n }\n\n this.#closed = true;\n this.#zipFile.close();\n\n return Promise.resolve();\n }\n\n public hasEntry(path: string): boolean {\n return this.#entries.has(normalizeArchivePath(path));\n }\n\n public listEntries(): readonly string[] {\n return [...this.#entries.keys()];\n }\n\n public async openReadStream(path: string): Promise<Readable> {\n const entry = this.#getEntry(path);\n\n return await openEntryStream(this.#zipFile, entry);\n }\n\n public async readText(path: string): Promise<string> {\n const stream = await this.openReadStream(path);\n return (await readStreamToBuffer(stream)).toString(\"utf8\");\n }\n\n public async readBuffer(path: string): Promise<Buffer> {\n const stream = await this.openReadStream(path);\n return await readStreamToBuffer(stream);\n }\n\n public resolveRelativePath(basePath: string, href: string): string {\n const normalizedHref = normalizeHref(href);\n if (normalizedHref === \"\") {\n throw new Error(`Invalid EPUB href: ${href}`);\n }\n\n return normalizeArchivePath(\n posix.join(posix.dirname(normalizeArchivePath(basePath)), normalizedHref),\n );\n }\n\n public createSectionId(path: string, fragment?: string): string {\n const normalizedPath = normalizeArchivePath(path);\n\n if (fragment === undefined || fragment === \"\") {\n return normalizedPath;\n }\n\n return `${normalizedPath}#${fragment}`;\n }\n\n public createSyntheticSectionId(path: string, title?: string): string {\n const normalizedPath = normalizeArchivePath(path);\n const hash = createHash(\"sha1\")\n .update(`${normalizedPath}:${title ?? \"\"}`)\n .digest(\"hex\")\n .slice(0, 10);\n\n return `toc:${hash}`;\n }\n\n public get path(): string {\n return this.#path;\n }\n\n #getEntry(path: string): Entry {\n const normalizedPath = normalizeArchivePath(path);\n const entry = this.#entries.get(normalizedPath);\n\n if (entry === undefined) {\n throw new Error(`EPUB entry does not exist: ${normalizedPath}`);\n }\n\n return entry;\n }\n}\n\nexport function normalizeArchivePath(path: string): string {\n const normalized = path.replaceAll(\"\\\\\", \"/\").trim();\n const withoutFragment = normalized.startsWith(\"/\")\n ? normalized.slice(1)\n : normalized;\n\n return posix\n .normalize(withoutFragment)\n .replace(/^(\\.\\/)+/, \"\")\n .replace(/^\\/+/, \"\");\n}\n\nexport function normalizeHref(href: string): string {\n const [path] = href.split(\"#\", 1);\n\n return normalizeArchivePath(path ?? \"\");\n}\n\nexport function normalizeFragment(\n fragment: string | undefined,\n): string | undefined {\n if (fragment === undefined) {\n return undefined;\n }\n\n const normalized = fragment.startsWith(\"#\") ? fragment.slice(1) : fragment;\n\n return normalized === \"\" ? undefined : normalized;\n}\n\nexport function splitHref(href: string): {\n readonly path: string;\n readonly fragment: string | undefined;\n} {\n const [pathPart, fragmentPart] = href.split(\"#\", 2);\n\n return {\n path: normalizeArchivePath(pathPart ?? \"\"),\n fragment: normalizeFragment(fragmentPart),\n };\n}\n\nasync function openZipFile(path: string): Promise<ZipFile> {\n return await new Promise((resolve, reject) => {\n open(path, { autoClose: false, lazyEntries: true }, (error, zipFile) => {\n if (error !== null || zipFile === undefined) {\n reject(error ?? new Error(`Cannot open EPUB archive: ${path}`));\n return;\n }\n\n resolve(zipFile);\n });\n });\n}\n\nasync function indexEntries(\n zipFile: ZipFile,\n): Promise<ReadonlyMap<string, Entry>> {\n return await new Promise((resolve, reject) => {\n const entries = new Map<string, Entry>();\n\n zipFile.on(\"entry\", (entry: Entry) => {\n if (entry.fileName.endsWith(\"/\")) {\n zipFile.readEntry();\n return;\n }\n\n entries.set(normalizeArchivePath(entry.fileName), entry);\n zipFile.readEntry();\n });\n zipFile.once(\"end\", () => {\n resolve(entries);\n });\n zipFile.once(\"error\", (error: Error) => {\n reject(error);\n });\n\n zipFile.readEntry();\n });\n}\n\nfunction toBuffer(chunk: unknown): Buffer {\n if (Buffer.isBuffer(chunk)) {\n return chunk;\n }\n\n if (typeof chunk === \"string\") {\n return Buffer.from(chunk, \"utf8\");\n }\n\n throw new Error(\"Unexpected ZIP stream chunk type\");\n}\n\nasync function readStreamToBuffer(stream: Readable): Promise<Buffer> {\n return await new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n\n stream.on(\"data\", (chunk: unknown) => {\n chunks.push(toBuffer(chunk));\n });\n stream.once(\"end\", () => {\n resolve(Buffer.concat(chunks));\n });\n stream.once(\"error\", (error: Error) => {\n reject(error);\n });\n stream.resume();\n });\n}\n\nasync function openEntryStream(\n zipFile: ZipFile,\n entry: Entry,\n): Promise<Readable> {\n return await new Promise((resolve, reject) => {\n zipFile.openReadStream(entry, (error, stream) => {\n if (error !== null || stream === undefined) {\n reject(error ?? new Error(`Cannot open EPUB entry: ${entry.fileName}`));\n return;\n }\n\n resolve(normalizeEntryStream(stream));\n });\n });\n}\n\nfunction normalizeEntryStream(stream: Readable): Readable {\n const normalized = new PassThrough();\n\n stream.once(\"error\", (error: Error) => {\n normalized.destroy(error);\n });\n normalized.once(\"close\", () => {\n stream.unpipe(normalized);\n\n if (!stream.destroyed) {\n stream.destroy();\n }\n });\n\n stream.pipe(normalized);\n\n return normalized;\n}\n","import type { Readable } from \"stream\";\n\nimport { Parser } from \"htmlparser2\";\n\nimport type { SourceTextStream } from \"../types.js\";\nimport type { EpubArchive } from \"./archive.js\";\n\nconst BLOCK_TAGS = new Set([\n \"address\",\n \"article\",\n \"aside\",\n \"blockquote\",\n \"br\",\n \"dd\",\n \"div\",\n \"dl\",\n \"dt\",\n \"figcaption\",\n \"figure\",\n \"footer\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"header\",\n \"hr\",\n \"li\",\n \"main\",\n \"nav\",\n \"ol\",\n \"p\",\n \"pre\",\n \"section\",\n \"table\",\n \"td\",\n \"th\",\n \"tr\",\n \"ul\",\n]);\n\nconst SKIPPED_TAGS = new Set([\"script\", \"style\"]);\n\nexport interface EpubSectionTarget {\n readonly id: string;\n readonly path: string;\n readonly fragment: string | undefined;\n}\n\nexport interface EpubSectionAnalysis {\n readonly hasContent: boolean;\n readonly wordsCount: number;\n}\n\nexport class EpubContentLoader {\n readonly #archive: EpubArchive;\n readonly #targetsBySectionId: ReadonlyMap<\n string,\n {\n readonly path: string;\n readonly targets: readonly EpubSectionTarget[];\n }\n >;\n\n public constructor(\n archive: EpubArchive,\n targetsByPath: ReadonlyMap<string, readonly EpubSectionTarget[]>,\n ) {\n this.#archive = archive;\n this.#targetsBySectionId = createTargetsBySectionId(targetsByPath);\n }\n\n public async openSection(sectionId: string): Promise<SourceTextStream> {\n const target = this.#targetsBySectionId.get(sectionId);\n\n if (target === undefined) {\n return [];\n }\n\n const sections = await this.#parseSections(target.path, target.targets);\n\n return [sections.get(sectionId) ?? \"\"];\n }\n\n async #parseSections(\n path: string,\n targets: readonly EpubSectionTarget[],\n ): Promise<ReadonlyMap<string, string>> {\n const stream = await this.#archive.openReadStream(path);\n stream.setEncoding(\"utf8\");\n\n return await parseHtmlSections(stream, targets);\n }\n}\n\nexport async function analyzeSectionTargets(\n archive: Pick<EpubArchive, \"openReadStream\">,\n targetsByPath: ReadonlyMap<string, readonly EpubSectionTarget[]>,\n): Promise<ReadonlyMap<string, EpubSectionAnalysis>> {\n const analyses = new Map<string, EpubSectionAnalysis>();\n\n for (const [path, targets] of targetsByPath.entries()) {\n const stream = await archive.openReadStream(path);\n stream.setEncoding(\"utf8\");\n const sections = await parseHtmlSectionTexts(stream, targets);\n\n for (const target of targets) {\n const text = sections.get(target.id) ?? \"\";\n\n analyses.set(target.id, {\n hasContent: text.trim() !== \"\",\n wordsCount: countWords(text),\n });\n }\n }\n\n return analyses;\n}\n\nfunction createTargetsBySectionId(\n targetsByPath: ReadonlyMap<string, readonly EpubSectionTarget[]>,\n): ReadonlyMap<\n string,\n {\n readonly path: string;\n readonly targets: readonly EpubSectionTarget[];\n }\n> {\n const targetsBySectionId = new Map<\n string,\n {\n readonly path: string;\n readonly targets: readonly EpubSectionTarget[];\n }\n >();\n\n for (const [path, targets] of targetsByPath.entries()) {\n for (const target of targets) {\n targetsBySectionId.set(target.id, { path, targets });\n }\n }\n\n return targetsBySectionId;\n}\n\nasync function parseHtmlSections(\n stream: Readable,\n targets: readonly EpubSectionTarget[],\n): Promise<ReadonlyMap<string, string>> {\n return await parseHtmlSectionTexts(stream, targets);\n}\n\nasync function parseHtmlSectionTexts(\n stream: Readable,\n targets: readonly EpubSectionTarget[],\n): Promise<ReadonlyMap<string, string>> {\n const fragments = new Map<string, number>();\n const sections = new Map<string, string>();\n const orderedTargets = [...targets];\n const buffers = orderedTargets.map(() => \"\");\n const rootSectionIndex =\n orderedTargets[0]?.fragment === undefined && orderedTargets[0] !== undefined\n ? 0\n : -1;\n let currentIndex = rootSectionIndex;\n let skippedTagDepth = 0;\n\n orderedTargets.forEach((target, index) => {\n sections.set(target.id, \"\");\n if (target.fragment !== undefined && !fragments.has(target.fragment)) {\n fragments.set(target.fragment, index);\n }\n });\n\n const parser = new Parser(\n {\n onopentag(name, attributes) {\n const tagName = name.toLowerCase();\n if (SKIPPED_TAGS.has(tagName)) {\n skippedTagDepth += 1;\n return;\n }\n\n const anchorId = attributes.id ?? attributes[\"xml:id\"];\n if (anchorId !== undefined) {\n const nextIndex = fragments.get(anchorId);\n if (nextIndex !== undefined && nextIndex > currentIndex) {\n currentIndex = nextIndex;\n }\n }\n\n if (tagName === \"br\") {\n appendText(buffers, currentIndex, \"\\n\");\n }\n },\n ontext(text) {\n if (skippedTagDepth > 0) {\n return;\n }\n\n appendText(buffers, currentIndex, text);\n },\n onclosetag(name) {\n const tagName = name.toLowerCase();\n if (SKIPPED_TAGS.has(tagName)) {\n skippedTagDepth = Math.max(0, skippedTagDepth - 1);\n return;\n }\n\n if (BLOCK_TAGS.has(tagName)) {\n appendText(buffers, currentIndex, \"\\n\\n\");\n }\n },\n },\n { decodeEntities: true },\n );\n\n for await (const chunk of stream as AsyncIterable<unknown>) {\n parser.write(toTextChunk(chunk));\n }\n\n parser.end();\n\n orderedTargets.forEach((target, index) => {\n sections.set(target.id, normalizeSectionText(buffers[index] ?? \"\"));\n });\n\n return sections;\n}\n\nfunction appendText(\n buffers: string[],\n currentIndex: number,\n text: string,\n): void {\n if (currentIndex < 0 || currentIndex >= buffers.length) {\n return;\n }\n\n buffers[currentIndex] += text;\n}\n\nfunction normalizeSectionText(text: string): string {\n return text\n .replace(/\\r\\n/gu, \"\\n\")\n .replace(/\\n{3,}/gu, \"\\n\\n\")\n .trim();\n}\n\nfunction toTextChunk(chunk: unknown): string {\n if (typeof chunk === \"string\") {\n return chunk;\n }\n\n if (Buffer.isBuffer(chunk)) {\n return chunk.toString(\"utf8\");\n }\n\n throw new Error(\"Unexpected HTML stream chunk type\");\n}\n\nfunction countWords(text: string): number {\n return [...createWordSegmenter().segment(text)].filter(\n (segment) => segment.isWordLike,\n ).length;\n}\n\nlet WORD_SEGMENTER: Intl.Segmenter | undefined;\n\nfunction createWordSegmenter(): Intl.Segmenter {\n if (WORD_SEGMENTER === undefined) {\n WORD_SEGMENTER = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n }\n\n return WORD_SEGMENTER;\n}\n","import { SaxesParser, type SaxesTagPlain } from \"saxes\";\n\nexport interface XmlElement {\n readonly name: string;\n readonly attributes: Readonly<Record<string, string>>;\n readonly children: readonly XmlElement[];\n readonly text: string;\n}\n\ninterface MutableXmlElement {\n readonly name: string;\n readonly attributes: Record<string, string>;\n readonly children: MutableXmlElement[];\n text: string;\n}\n\nexport function parseXml(content: string): XmlElement {\n const parser = new SaxesParser({ xmlns: false });\n const stack: MutableXmlElement[] = [];\n let root: MutableXmlElement | undefined;\n\n parser.on(\"opentag\", (tag: SaxesTagPlain) => {\n const element: MutableXmlElement = {\n name: tag.name,\n attributes: Object.fromEntries(\n Object.entries(tag.attributes).map(([name, value]) => [\n name,\n String(value),\n ]),\n ),\n children: [],\n text: \"\",\n };\n\n const parent = stack[stack.length - 1];\n if (parent === undefined) {\n root = element;\n } else {\n parent.children.push(element);\n }\n\n stack.push(element);\n });\n\n parser.on(\"text\", (text: string) => {\n const current = stack[stack.length - 1];\n if (current !== undefined) {\n current.text += text;\n }\n });\n\n parser.on(\"cdata\", (text: string) => {\n const current = stack[stack.length - 1];\n if (current !== undefined) {\n current.text += text;\n }\n });\n\n parser.on(\"closetag\", () => {\n stack.pop();\n });\n\n parser.write(content).close();\n\n if (root === undefined) {\n throw new Error(\"XML document is empty\");\n }\n\n return freezeXmlElement(root);\n}\n\nexport function findChild(\n element: XmlElement,\n localName: string,\n): XmlElement | undefined {\n return element.children.find(\n (child) => getLocalName(child.name) === localName,\n );\n}\n\nexport function findChildren(\n element: XmlElement,\n localName: string,\n): readonly XmlElement[] {\n return element.children.filter(\n (child) => getLocalName(child.name) === localName,\n );\n}\n\nexport function findDescendant(\n element: XmlElement,\n localName: string,\n): XmlElement | undefined {\n for (const child of element.children) {\n if (getLocalName(child.name) === localName) {\n return child;\n }\n\n const descendant = findDescendant(child, localName);\n if (descendant !== undefined) {\n return descendant;\n }\n }\n\n return undefined;\n}\n\nexport function findDescendants(\n element: XmlElement,\n localName: string,\n): readonly XmlElement[] {\n const descendants: XmlElement[] = [];\n\n for (const child of element.children) {\n if (getLocalName(child.name) === localName) {\n descendants.push(child);\n }\n\n descendants.push(...findDescendants(child, localName));\n }\n\n return descendants;\n}\n\nexport function getAttribute(\n element: XmlElement,\n name: string,\n): string | undefined {\n return element.attributes[name];\n}\n\nexport function getDescendantText(element: XmlElement): string {\n let text = element.text;\n\n for (const child of element.children) {\n text += getDescendantText(child);\n }\n\n return text;\n}\n\nexport function getLocalName(name: string): string {\n const index = name.indexOf(\":\");\n\n return index === -1 ? name : name.slice(index + 1);\n}\n\nfunction freezeXmlElement(element: MutableXmlElement): XmlElement {\n return {\n name: element.name,\n attributes: Object.freeze({ ...element.attributes }),\n children: Object.freeze(element.children.map(freezeXmlElement)),\n text: element.text,\n };\n}\n","import { posix } from \"path\";\n\nimport { BOOK_META_VERSION, type BookMeta } from \"../meta.js\";\nimport { splitHref } from \"./archive.js\";\nimport type { EpubArchive } from \"./archive.js\";\nimport {\n findChild,\n findChildren,\n getAttribute,\n getDescendantText,\n getLocalName,\n parseXml,\n type XmlElement,\n} from \"./xml.js\";\n\nconst COVER_ITEM_IDS = [\"cover-image\", \"coverimg\", \"cover-img\", \"cover\"];\nconst GUIDE_NON_CONTENT_TYPES = new Set([\n \"cover\",\n \"titlepage\",\n \"other.titlepage\",\n]);\n\nexport interface EpubManifestItem {\n readonly id: string;\n readonly path: string;\n readonly mediaType: string;\n readonly properties: ReadonlySet<string>;\n}\n\nexport interface EpubSpineItem {\n readonly idref: string;\n readonly path: string;\n readonly mediaType: string;\n}\n\nexport interface EpubPackageData {\n readonly version: 2 | 3;\n readonly opfPath: string;\n readonly metadata: BookMeta;\n readonly manifest: ReadonlyMap<string, EpubManifestItem>;\n readonly manifestByPath: ReadonlyMap<string, EpubManifestItem>;\n readonly spine: readonly EpubSpineItem[];\n readonly navPath: string | undefined;\n readonly ncxPath: string | undefined;\n readonly coverPath: string | undefined;\n readonly coverMediaType: string | undefined;\n readonly guideNonContentPaths: ReadonlySet<string>;\n}\n\nexport async function readEpubPackage(\n archive: EpubArchive,\n): Promise<EpubPackageData> {\n const opfPath = await findOpfPath(archive);\n const opf = parseXml(await archive.readText(opfPath));\n const version = parsePackageVersion(getAttribute(opf, \"version\"));\n const metadataElement = findChild(opf, \"metadata\");\n const manifestElement = findChild(opf, \"manifest\");\n const spineElement = findChild(opf, \"spine\");\n const guideElement = findChild(opf, \"guide\");\n\n const manifest = buildManifest(archive, opfPath, manifestElement);\n const manifestByPath = new Map(\n [...manifest.values()].map((item) => [item.path, item]),\n );\n const spine = buildSpine(spineElement, manifest);\n const guideNonContentPaths = buildGuideNonContentPaths(\n archive,\n opfPath,\n guideElement,\n );\n const metadata = buildMetadata(metadataElement);\n const navPath = findNavPath(manifest);\n const ncxPath = findNcxPath(manifest, spineElement);\n const coverItem = findCoverItem(manifest, metadataElement);\n\n return {\n version,\n opfPath,\n metadata: {\n ...metadata,\n version: BOOK_META_VERSION,\n sourceFormat: \"epub\",\n },\n manifest,\n manifestByPath,\n spine,\n navPath,\n ncxPath,\n coverPath: coverItem?.path,\n coverMediaType: coverItem?.mediaType,\n guideNonContentPaths,\n };\n}\n\nasync function findOpfPath(archive: EpubArchive): Promise<string> {\n const container = parseXml(await archive.readText(\"META-INF/container.xml\"));\n const rootfiles = findChild(container, \"rootfiles\");\n const rootfile = rootfiles?.children.find(\n (child) => getLocalName(child.name) === \"rootfile\",\n );\n const fullPath =\n rootfile === undefined ? undefined : getAttribute(rootfile, \"full-path\");\n\n if (fullPath === undefined || fullPath.trim() === \"\") {\n throw new Error(\"EPUB container.xml does not contain an OPF rootfile\");\n }\n\n return fullPath;\n}\n\nfunction parsePackageVersion(version: string | undefined): 2 | 3 {\n return version?.trim().startsWith(\"3\") === true ? 3 : 2;\n}\n\nfunction buildMetadata(metadataElement: XmlElement | undefined): BookMeta {\n if (metadataElement === undefined) {\n return createEmptyMetadata();\n }\n\n const titles = readMetadataValues(metadataElement, \"title\");\n const creators = readMetadataValues(metadataElement, \"creator\");\n const languages = readMetadataValues(metadataElement, \"language\");\n const identifiers = readMetadataValues(metadataElement, \"identifier\");\n const publishers = readMetadataValues(metadataElement, \"publisher\");\n const dates = readMetadataValues(metadataElement, \"date\");\n const descriptions = readMetadataValues(metadataElement, \"description\");\n\n return {\n ...createEmptyMetadata(),\n title: titles[0] ?? null,\n authors: [...creators],\n language: languages[0] ?? null,\n identifier: identifiers[0] ?? null,\n publisher: publishers[0] ?? null,\n publishedAt: dates[0] ?? null,\n description: descriptions[0] ?? null,\n };\n}\n\nfunction createEmptyMetadata(): BookMeta {\n return {\n version: BOOK_META_VERSION,\n sourceFormat: \"epub\",\n title: null,\n authors: [],\n language: null,\n identifier: null,\n publisher: null,\n publishedAt: null,\n description: null,\n };\n}\n\nfunction readMetadataValues(\n metadataElement: XmlElement,\n localName: string,\n): readonly string[] {\n return metadataElement.children\n .filter((child) => getLocalName(child.name) === localName)\n .map((child) => normalizeText(getDescendantText(child)))\n .filter((value): value is string => value !== undefined);\n}\n\nfunction buildManifest(\n archive: EpubArchive,\n opfPath: string,\n manifestElement: XmlElement | undefined,\n): ReadonlyMap<string, EpubManifestItem> {\n const manifest = new Map<string, EpubManifestItem>();\n\n if (manifestElement === undefined) {\n return manifest;\n }\n\n for (const item of findChildren(manifestElement, \"item\")) {\n const id = getAttribute(item, \"id\");\n const href = getAttribute(item, \"href\");\n const mediaType = getAttribute(item, \"media-type\") ?? \"\";\n\n if (id === undefined || href === undefined) {\n continue;\n }\n\n manifest.set(id, {\n id,\n path: archive.resolveRelativePath(opfPath, href),\n mediaType,\n properties: new Set(\n (getAttribute(item, \"properties\") ?? \"\")\n .split(/\\s+/u)\n .map((value) => value.trim().toLowerCase())\n .filter((value) => value !== \"\"),\n ),\n });\n }\n\n return manifest;\n}\n\nfunction buildSpine(\n spineElement: XmlElement | undefined,\n manifest: ReadonlyMap<string, EpubManifestItem>,\n): readonly EpubSpineItem[] {\n if (spineElement === undefined) {\n return [];\n }\n\n return findChildren(spineElement, \"itemref\")\n .map((itemref) => getAttribute(itemref, \"idref\"))\n .filter((idref): idref is string => idref !== undefined)\n .map((idref) => {\n const item = manifest.get(idref);\n\n if (\n item === undefined ||\n (item.mediaType !== \"application/xhtml+xml\" &&\n item.mediaType !== \"text/html\")\n ) {\n return undefined;\n }\n\n return {\n idref,\n path: item.path,\n mediaType: item.mediaType,\n } satisfies EpubSpineItem;\n })\n .filter((item): item is EpubSpineItem => item !== undefined);\n}\n\nfunction buildGuideNonContentPaths(\n archive: EpubArchive,\n opfPath: string,\n guideElement: XmlElement | undefined,\n): ReadonlySet<string> {\n const paths = new Set<string>();\n\n if (guideElement === undefined) {\n return paths;\n }\n\n for (const reference of findChildren(guideElement, \"reference\")) {\n const type = (getAttribute(reference, \"type\") ?? \"\").trim().toLowerCase();\n if (!GUIDE_NON_CONTENT_TYPES.has(type)) {\n continue;\n }\n\n const href = getAttribute(reference, \"href\");\n if (href === undefined || href.trim() === \"\") {\n continue;\n }\n\n paths.add(archive.resolveRelativePath(opfPath, splitHref(href).path));\n }\n\n return paths;\n}\n\nfunction findNavPath(\n manifest: ReadonlyMap<string, EpubManifestItem>,\n): string | undefined {\n for (const item of manifest.values()) {\n if (item.properties.has(\"nav\")) {\n return item.path;\n }\n }\n\n return undefined;\n}\n\nfunction findNcxPath(\n manifest: ReadonlyMap<string, EpubManifestItem>,\n spineElement: XmlElement | undefined,\n): string | undefined {\n const tocId =\n spineElement === undefined ? undefined : getAttribute(spineElement, \"toc\");\n\n if (tocId !== undefined) {\n return manifest.get(tocId)?.path;\n }\n\n for (const item of manifest.values()) {\n if (item.mediaType === \"application/x-dtbncx+xml\") {\n return item.path;\n }\n }\n\n return undefined;\n}\n\nfunction findCoverItem(\n manifest: ReadonlyMap<string, EpubManifestItem>,\n metadataElement: XmlElement | undefined,\n): EpubManifestItem | undefined {\n for (const item of manifest.values()) {\n if (\n item.properties.has(\"cover-image\") &&\n item.mediaType.startsWith(\"image/\")\n ) {\n return item;\n }\n }\n\n const coverId = readCoverIdFromMetadata(metadataElement);\n if (coverId !== undefined) {\n const coverItem = manifest.get(coverId);\n if (coverItem?.mediaType.startsWith(\"image/\") === true) {\n return coverItem;\n }\n }\n\n for (const coverItemId of COVER_ITEM_IDS) {\n const coverItem = manifest.get(coverItemId);\n if (coverItem?.mediaType.startsWith(\"image/\") === true) {\n return coverItem;\n }\n }\n\n for (const item of manifest.values()) {\n if (\n item.mediaType.startsWith(\"image/\") &&\n posix.basename(item.path).toLowerCase().includes(\"cover\")\n ) {\n return item;\n }\n }\n\n return undefined;\n}\n\nfunction readCoverIdFromMetadata(\n metadataElement: XmlElement | undefined,\n): string | undefined {\n if (metadataElement === undefined) {\n return undefined;\n }\n\n for (const meta of findChildren(metadataElement, \"meta\")) {\n const name = (getAttribute(meta, \"name\") ?? \"\").trim().toLowerCase();\n if (name !== \"cover\") {\n continue;\n }\n\n const content = getAttribute(meta, \"content\");\n if (content !== undefined && content.trim() !== \"\") {\n return content.trim();\n }\n }\n\n return undefined;\n}\n\nfunction normalizeText(text: string): string | undefined {\n const normalized = text.replace(/\\s+/gu, \" \").trim();\n\n return normalized === \"\" ? undefined : normalized;\n}\n","import { basename, extname, posix } from \"path\";\n\nimport { parseDocument } from \"htmlparser2\";\n\nimport { splitHref } from \"./archive.js\";\nimport type { EpubArchive } from \"./archive.js\";\nimport { type EpubPackageData } from \"./package.js\";\nimport {\n findChild,\n findChildren,\n getAttribute,\n getDescendantText,\n parseXml,\n type XmlElement,\n} from \"./xml.js\";\n\nconst NON_CONTENT_FILE_NAMES = new Set([\n \"toc.xhtml\",\n \"nav.xhtml\",\n \"contents.xhtml\",\n \"tableofcontents.xhtml\",\n \"cover.xhtml\",\n \"titlepage.xhtml\",\n]);\n\ninterface HtmlNode {\n readonly type?: string;\n readonly name?: string;\n readonly data?: string;\n readonly attribs?: Readonly<Record<string, string>>;\n readonly children?: readonly HtmlNode[];\n}\n\nexport interface EpubNavigationItem {\n readonly title: string | undefined;\n readonly path: string | undefined;\n readonly fragment: string | undefined;\n readonly children: readonly EpubNavigationItem[];\n}\n\nexport async function readEpubNavigation(\n archive: EpubArchive,\n packageData: EpubPackageData,\n): Promise<readonly EpubNavigationItem[]> {\n const navigation =\n (await readNavDocument(archive, packageData)) ??\n (await readNcxDocument(archive, packageData)) ??\n [];\n\n if (navigation.length === 0) {\n return buildSpineNavigation(packageData, new Set());\n }\n\n const referencedPaths = collectReferencedPaths(navigation);\n const extras = buildSpineNavigation(packageData, referencedPaths);\n\n return extras.length === 0 ? navigation : [...navigation, ...extras];\n}\n\nasync function readNavDocument(\n archive: EpubArchive,\n packageData: EpubPackageData,\n): Promise<readonly EpubNavigationItem[] | undefined> {\n if (packageData.navPath === undefined) {\n return undefined;\n }\n\n const document = parseDocument(await archive.readText(packageData.navPath), {\n decodeEntities: true,\n }) as HtmlNode;\n const nav = findFirstHtmlElement(document, (element) => {\n if (getHtmlTagName(element) !== \"nav\") {\n return false;\n }\n\n const type = (element.attribs?.[\"epub:type\"] ?? element.attribs?.type ?? \"\")\n .split(/\\s+/u)\n .map((value) => value.trim().toLowerCase());\n\n return type.includes(\"toc\");\n });\n\n if (nav === undefined) {\n return undefined;\n }\n\n const ol = findFirstHtmlElement(\n nav,\n (element) => getHtmlTagName(element) === \"ol\",\n );\n if (ol === undefined) {\n return undefined;\n }\n\n const items = getHtmlElementChildren(ol)\n .filter((child) => getHtmlTagName(child) === \"li\")\n .map((li) => parseNavListItem(archive, packageData.navPath!, li))\n .filter((item): item is EpubNavigationItem => item !== undefined);\n\n return items.length === 0 ? undefined : items;\n}\n\nasync function readNcxDocument(\n archive: EpubArchive,\n packageData: EpubPackageData,\n): Promise<readonly EpubNavigationItem[] | undefined> {\n if (packageData.ncxPath === undefined) {\n return undefined;\n }\n\n const root = parseXml(await archive.readText(packageData.ncxPath));\n const navMap = findChild(root, \"navMap\");\n if (navMap === undefined) {\n return undefined;\n }\n\n const items = findChildren(navMap, \"navPoint\")\n .map((navPoint) =>\n parseNcxNavPoint(archive, packageData.ncxPath!, navPoint),\n )\n .filter((item): item is EpubNavigationItem => item !== undefined);\n\n return items.length === 0 ? undefined : items;\n}\n\nfunction parseNavListItem(\n archive: EpubArchive,\n navPath: string,\n item: HtmlNode,\n): EpubNavigationItem | undefined {\n const label = findHtmlLabelNode(item);\n const title =\n label === undefined ? undefined : normalizeText(collectHtmlText(label));\n const href =\n label !== undefined && getHtmlTagName(label) === \"a\"\n ? label.attribs?.href\n : undefined;\n const resolved = resolveHtmlHref(archive, navPath, href);\n const children = getHtmlElementChildren(item)\n .filter((child) => getHtmlTagName(child) === \"ol\")\n .flatMap((ol) => getHtmlElementChildren(ol))\n .filter((child) => getHtmlTagName(child) === \"li\")\n .map((li) => parseNavListItem(archive, navPath, li))\n .filter((child): child is EpubNavigationItem => child !== undefined);\n\n if (\n title === undefined &&\n resolved.path === undefined &&\n children.length === 0\n ) {\n return undefined;\n }\n\n return {\n title,\n path: resolved.path,\n fragment: resolved.fragment,\n children,\n };\n}\n\nfunction parseNcxNavPoint(\n archive: EpubArchive,\n ncxPath: string,\n navPoint: XmlElement,\n): EpubNavigationItem | undefined {\n const navLabel = findChild(navPoint, \"navLabel\");\n const textElement =\n navLabel === undefined ? undefined : findChild(navLabel, \"text\");\n const title =\n textElement === undefined\n ? undefined\n : normalizeText(getDescendantText(textElement));\n const contentElement = findChild(navPoint, \"content\");\n const resolved = resolveXmlHref(\n archive,\n ncxPath,\n getAttribute(contentElement ?? navPoint, \"src\"),\n );\n const children = findChildren(navPoint, \"navPoint\")\n .map((child) => parseNcxNavPoint(archive, ncxPath, child))\n .filter((child): child is EpubNavigationItem => child !== undefined);\n\n if (\n title === undefined &&\n resolved.path === undefined &&\n children.length === 0\n ) {\n return undefined;\n }\n\n return {\n title,\n path: resolved.path,\n fragment: resolved.fragment,\n children,\n };\n}\n\nfunction buildSpineNavigation(\n packageData: EpubPackageData,\n referencedPaths: ReadonlySet<string>,\n): readonly EpubNavigationItem[] {\n return packageData.spine\n .filter(\n (item) => !isNonContentSpineItem(packageData, item.path, referencedPaths),\n )\n .filter((item) => !referencedPaths.has(item.path))\n .map((item) => ({\n title: getFallbackTitle(item.path),\n path: item.path,\n fragment: undefined,\n children: [],\n }));\n}\n\nfunction isNonContentSpineItem(\n packageData: EpubPackageData,\n path: string,\n referencedPaths: ReadonlySet<string>,\n): boolean {\n if (packageData.guideNonContentPaths.has(path)) {\n return true;\n }\n\n const lowerName = posix.basename(path).toLowerCase();\n if (packageData.navPath === path) {\n return true;\n }\n\n return NON_CONTENT_FILE_NAMES.has(lowerName) && !referencedPaths.has(path);\n}\n\nfunction collectReferencedPaths(\n items: readonly EpubNavigationItem[],\n): ReadonlySet<string> {\n const paths = new Set<string>();\n\n for (const item of items) {\n if (item.path !== undefined) {\n paths.add(item.path);\n }\n\n for (const childPath of collectReferencedPaths(item.children)) {\n paths.add(childPath);\n }\n }\n\n return paths;\n}\n\nfunction resolveHtmlHref(\n archive: EpubArchive,\n basePath: string,\n href: string | undefined,\n): {\n readonly path: string | undefined;\n readonly fragment: string | undefined;\n} {\n if (href === undefined || href.trim() === \"\") {\n return { path: undefined, fragment: undefined };\n }\n\n const { path, fragment } = splitHref(href);\n\n return {\n path: path === \"\" ? undefined : archive.resolveRelativePath(basePath, path),\n fragment,\n };\n}\n\nfunction resolveXmlHref(\n archive: EpubArchive,\n basePath: string,\n href: string | undefined,\n): {\n readonly path: string | undefined;\n readonly fragment: string | undefined;\n} {\n if (href === undefined || href.trim() === \"\") {\n return { path: undefined, fragment: undefined };\n }\n\n const { path, fragment } = splitHref(href);\n\n return {\n path: path === \"\" ? undefined : archive.resolveRelativePath(basePath, path),\n fragment,\n };\n}\n\nfunction findFirstHtmlElement(\n node: HtmlNode,\n predicate: (element: HtmlNode) => boolean,\n): HtmlNode | undefined {\n if (isHtmlElement(node) && predicate(node)) {\n return node;\n }\n\n for (const child of node.children ?? []) {\n const found = findFirstHtmlElement(child, predicate);\n if (found !== undefined) {\n return found;\n }\n }\n\n return undefined;\n}\n\nfunction findHtmlLabelNode(node: HtmlNode): HtmlNode | undefined {\n for (const child of getHtmlElementChildren(node)) {\n const tagName = getHtmlTagName(child);\n if (tagName === \"a\" || tagName === \"span\") {\n return child;\n }\n }\n\n for (const child of getHtmlElementChildren(node)) {\n if (getHtmlTagName(child) === \"ol\") {\n continue;\n }\n\n const nested = findHtmlLabelNode(child);\n if (nested !== undefined) {\n return nested;\n }\n }\n\n return undefined;\n}\n\nfunction collectHtmlText(node: HtmlNode): string {\n if (node.type === \"text\") {\n return node.data ?? \"\";\n }\n\n return (node.children ?? []).map(collectHtmlText).join(\"\");\n}\n\nfunction getHtmlElementChildren(node: HtmlNode): readonly HtmlNode[] {\n return (node.children ?? []).filter(isHtmlElement);\n}\n\nfunction isHtmlElement(node: HtmlNode): boolean {\n return node.type === \"tag\" || node.type === \"script\" || node.type === \"style\";\n}\n\nfunction getHtmlTagName(node: HtmlNode): string | undefined {\n return node.name?.toLowerCase();\n}\n\nfunction getFallbackTitle(path: string): string | undefined {\n const stem = basename(path, extname(path)).trim();\n\n return stem === \"\" ? undefined : stem;\n}\n\nfunction normalizeText(text: string): string | undefined {\n const normalized = text.replace(/\\s+/gu, \" \").trim();\n\n return normalized === \"\" ? undefined : normalized;\n}\n","import type { SourceAdapter, SourceDocument } from \"../adapter.js\";\nimport type { SourceAsset, SourceSection, SourceTextStream } from \"../types.js\";\nimport { normalizeFragment } from \"./archive.js\";\nimport { EpubArchive } from \"./archive.js\";\nimport {\n analyzeSectionTargets,\n EpubContentLoader,\n type EpubSectionAnalysis,\n type EpubSectionTarget,\n} from \"./content.js\";\nimport { readEpubNavigation, type EpubNavigationItem } from \"./navigation.js\";\nimport { readEpubPackage, type EpubPackageData } from \"./package.js\";\n\ninterface SectionDefinition {\n readonly hasContent: boolean;\n readonly id: string;\n readonly title: string | undefined;\n readonly path: string | undefined;\n readonly fragment: string | undefined;\n readonly wordsCount: number;\n readonly children: readonly SectionDefinition[];\n}\n\nclass EpubSourceSection implements SourceSection {\n readonly #document: EpubSourceDocument;\n readonly #definition: SectionDefinition;\n\n public constructor(\n document: EpubSourceDocument,\n definition: SectionDefinition,\n ) {\n this.#document = document;\n this.#definition = definition;\n }\n\n public get id(): string {\n return this.#definition.id;\n }\n\n public get hasContent(): boolean {\n return this.#definition.hasContent;\n }\n\n public get title(): string | undefined {\n return this.#definition.title;\n }\n\n public get wordsCount(): number {\n return this.#definition.wordsCount;\n }\n\n public get children(): readonly SourceSection[] {\n return this.#definition.children.map(\n (child) => new EpubSourceSection(this.#document, child),\n );\n }\n\n public async open(): Promise<SourceTextStream> {\n return await this.#document.openSection(this.#definition.id);\n }\n}\n\nexport class EpubSourceDocument implements SourceDocument {\n readonly #packageData: EpubPackageData;\n readonly #cover: SourceAsset | undefined;\n readonly #sections: readonly SectionDefinition[];\n readonly #contentLoader: EpubContentLoader;\n\n public constructor(\n packageData: EpubPackageData,\n cover: SourceAsset | undefined,\n sections: readonly SectionDefinition[],\n contentLoader: EpubContentLoader,\n ) {\n this.#packageData = packageData;\n this.#cover = cover;\n this.#sections = sections;\n this.#contentLoader = contentLoader;\n }\n\n public static async open(archive: EpubArchive): Promise<EpubSourceDocument> {\n assertArchiveIsSupported(archive);\n const packageData = await readEpubPackage(archive);\n const navigation = await readEpubNavigation(archive, packageData);\n const rawSections = buildSections(archive, navigation);\n const targetsByPath = groupTargetsByPath(rawSections);\n const sectionAnalyses = await analyzeSectionTargets(archive, targetsByPath);\n const sections = hydrateSectionAnalyses(rawSections, sectionAnalyses);\n const cover = await readCoverAsset(archive, packageData);\n\n return new EpubSourceDocument(\n packageData,\n cover,\n sections,\n new EpubContentLoader(archive, targetsByPath),\n );\n }\n\n public readMeta() {\n return Promise.resolve(this.#packageData.metadata);\n }\n\n public readCover() {\n return Promise.resolve(this.#cover);\n }\n\n public readSections() {\n return Promise.resolve(\n this.#sections.map((section) => new EpubSourceSection(this, section)),\n );\n }\n\n public async openSection(sectionId: string): Promise<SourceTextStream> {\n return await this.#contentLoader.openSection(sectionId);\n }\n}\n\nexport class EpubSourceAdapter implements SourceAdapter {\n public get format() {\n return \"epub\" as const;\n }\n\n public async openSession<T>(\n path: string,\n operation: (document: SourceDocument) => Promise<T>,\n ): Promise<T> {\n const archive = await EpubArchive.open(path);\n\n try {\n const document = await EpubSourceDocument.open(archive);\n\n return await operation(document);\n } finally {\n await archive.close();\n }\n }\n}\n\nexport const EPUB_SOURCE_ADAPTER = new EpubSourceAdapter();\n\nfunction assertArchiveIsSupported(archive: EpubArchive): void {\n if (!archive.hasEntry(\"META-INF/encryption.xml\")) {\n return;\n }\n\n throw new Error(\n \"Encrypted EPUB is not supported: found META-INF/encryption.xml.\",\n );\n}\n\nasync function readCoverAsset(\n archive: EpubArchive,\n packageData: EpubPackageData,\n): Promise<SourceAsset | undefined> {\n if (\n packageData.coverPath === undefined ||\n packageData.coverMediaType === undefined\n ) {\n return undefined;\n }\n\n return {\n path: packageData.coverPath,\n mediaType: packageData.coverMediaType,\n data: await archive.readBuffer(packageData.coverPath),\n };\n}\n\nfunction buildSections(\n archive: EpubArchive,\n items: readonly EpubNavigationItem[],\n): readonly SectionDefinition[] {\n const idCounts = new Map<string, number>();\n\n return items.map((item, index) =>\n buildSection(archive, item, [index], idCounts),\n );\n}\n\nfunction buildSection(\n archive: EpubArchive,\n item: EpubNavigationItem,\n indexPath: readonly number[],\n idCounts: Map<string, number>,\n): SectionDefinition {\n const fragment = normalizeFragment(item.fragment);\n const baseId =\n item.path === undefined\n ? `toc:${indexPath.join(\".\")}`\n : archive.createSectionId(item.path, fragment);\n const id = createUniqueId(baseId, idCounts);\n\n return {\n hasContent: item.path !== undefined,\n id,\n title: item.title,\n path: item.path,\n fragment,\n wordsCount: 0,\n children: item.children.map((child, index) =>\n buildSection(archive, child, [...indexPath, index], idCounts),\n ),\n };\n}\n\nfunction hydrateSectionAnalyses(\n sections: readonly SectionDefinition[],\n sectionAnalyses: ReadonlyMap<string, EpubSectionAnalysis>,\n): readonly SectionDefinition[] {\n return sections.map((section) => {\n const analysis = sectionAnalyses.get(section.id);\n\n return {\n ...section,\n hasContent: analysis?.hasContent ?? false,\n wordsCount: analysis?.wordsCount ?? 0,\n children: hydrateSectionAnalyses(section.children, sectionAnalyses),\n };\n });\n}\n\nfunction createUniqueId(baseId: string, idCounts: Map<string, number>): string {\n const currentCount = idCounts.get(baseId) ?? 0;\n idCounts.set(baseId, currentCount + 1);\n\n return currentCount === 0 ? baseId : `${baseId}~${currentCount + 1}`;\n}\n\nfunction groupTargetsByPath(\n sections: readonly SectionDefinition[],\n): ReadonlyMap<string, readonly EpubSectionTarget[]> {\n const targetsByPath = new Map<string, EpubSectionTarget[]>();\n\n for (const section of flattenSections(sections)) {\n if (section.path === undefined) {\n continue;\n }\n\n const targets = targetsByPath.get(section.path) ?? [];\n targets.push({\n id: section.id,\n path: section.path,\n fragment: section.fragment,\n });\n targetsByPath.set(section.path, targets);\n }\n\n return targetsByPath;\n}\n\nfunction flattenSections(\n sections: readonly SectionDefinition[],\n): readonly SectionDefinition[] {\n const flattened: SectionDefinition[] = [];\n\n for (const section of sections) {\n flattened.push(section);\n flattened.push(...flattenSections(section.children));\n }\n\n return flattened;\n}\n","export { EpubSourceAdapter, EPUB_SOURCE_ADAPTER } from \"./document.js\";\n","import { createReadStream } from \"fs\";\nimport { stat } from \"fs/promises\";\nimport { basename, extname, resolve } from \"path\";\n\nimport { BOOK_META_VERSION, type BookMeta } from \"./meta.js\";\nimport type { SourceAdapter, SourceDocument } from \"./adapter.js\";\nimport type { SourceAsset, SourceSection, SourceTextStream } from \"./types.js\";\n\ntype PlainTextSourceFormat = \"markdown\" | \"txt\";\n\nconst ROOT_SECTION_ID = \"root\";\n\nclass PlainTextSection implements SourceSection {\n readonly #path: string;\n readonly #id: string;\n\n public constructor(path: string, id = ROOT_SECTION_ID) {\n this.#path = resolve(path);\n this.#id = id;\n }\n\n public get id(): string {\n return this.#id;\n }\n\n public get hasContent(): boolean {\n return true;\n }\n\n public get title(): string | undefined {\n return undefined;\n }\n\n public get children(): readonly SourceSection[] {\n return [];\n }\n\n public open(): Promise<SourceTextStream> {\n return Promise.resolve(createReadStream(this.#path, { encoding: \"utf8\" }));\n }\n}\n\nclass PlainTextDocument implements SourceDocument {\n readonly #path: string;\n readonly #format: PlainTextSourceFormat;\n readonly #section: PlainTextSection;\n\n public constructor(path: string, format: PlainTextSourceFormat) {\n this.#path = resolve(path);\n this.#format = format;\n this.#section = new PlainTextSection(this.#path);\n }\n\n public readMeta(): Promise<BookMeta> {\n return Promise.resolve({\n version: BOOK_META_VERSION,\n sourceFormat: this.#format,\n title: getFileStem(this.#path),\n authors: [],\n language: null,\n identifier: null,\n publisher: null,\n publishedAt: null,\n description: null,\n });\n }\n\n public readCover(): Promise<SourceAsset | undefined> {\n return Promise.resolve(undefined);\n }\n\n public readSections(): Promise<readonly SourceSection[]> {\n return Promise.resolve([this.#section]);\n }\n}\n\nexport class PlainTextSourceAdapter implements SourceAdapter {\n readonly #format: PlainTextSourceFormat;\n\n public constructor(format: PlainTextSourceFormat) {\n this.#format = format;\n }\n\n public get format(): PlainTextSourceFormat {\n return this.#format;\n }\n\n public async openSession<T>(\n path: string,\n operation: (document: SourceDocument) => Promise<T>,\n ): Promise<T> {\n const resolvedPath = resolve(path);\n const fileStat = await stat(resolvedPath);\n\n if (!fileStat.isFile()) {\n throw new Error(`Source file is not a regular file: ${resolvedPath}`);\n }\n\n return await operation(new PlainTextDocument(resolvedPath, this.#format));\n }\n}\n\nexport const TXT_SOURCE_ADAPTER = new PlainTextSourceAdapter(\"txt\");\nexport const MARKDOWN_SOURCE_ADAPTER = new PlainTextSourceAdapter(\"markdown\");\n\nfunction getFileStem(path: string): string | null {\n const stem = basename(path, extname(path)).trim();\n\n return stem === \"\" ? null : stem;\n}\n","export {\n BOOK_META_VERSION,\n SOURCE_FORMATS,\n bookMetaSchema,\n sourceFormatSchema,\n type BookMeta,\n type SourceFormat,\n} from \"./meta.js\";\nexport type { SourceAdapter, SourceDocument } from \"./adapter.js\";\nexport { EpubSourceAdapter, EPUB_SOURCE_ADAPTER } from \"./epub/index.js\";\nexport {\n MARKDOWN_SOURCE_ADAPTER,\n PlainTextSourceAdapter,\n TXT_SOURCE_ADAPTER,\n} from \"./plain-text.js\";\nexport {\n TOC_FILE_VERSION,\n tocFileSchema,\n tocItemSchema,\n type TocFile,\n type TocItem,\n} from \"./toc.js\";\nexport type { SourceAsset, SourceSection, SourceTextStream } from \"./types.js\";\n","import type { Document, ReadonlyDocument } from \"../document/index.js\";\nimport { normalizeLanguageCode, type Language } from \"../common/language.js\";\nimport type { WikiGraphScope } from \"../common/llm-scope.js\";\nimport type { LLM } from \"../llm/index.js\";\nimport type { ReaderTextStream } from \"../reader/index.js\";\nimport { z } from \"zod\";\nimport {\n SerialGeneration,\n writeSerialSource,\n type SerialProgressSink,\n type BuildSerialTopologyOptions,\n} from \"../serial.js\";\nimport { TOC_FILE_VERSION, type TocItem } from \"../source/index.js\";\n\nexport const CHAPTER_STAGES = [\n \"planned\",\n \"sourced\",\n \"graphed\",\n \"summarized\",\n] as const;\n\nexport type ChapterStage = (typeof CHAPTER_STAGES)[number];\n\nexport interface ChapterEntry {\n readonly chapterId: number;\n readonly childCount: number;\n readonly depth: number;\n readonly documentOrder: number;\n readonly fragmentCount: number;\n readonly stage: ChapterStage;\n readonly title: string | null;\n readonly tocPath: readonly string[];\n readonly words: number;\n}\n\nexport interface ChapterDetails extends ChapterEntry {\n readonly graphReady: boolean;\n readonly hasSummary: boolean;\n readonly words: number;\n}\n\nexport interface ChapterTree {\n readonly chapters: readonly ChapterTreeNode[];\n}\n\nexport interface ChapterTreeNode {\n readonly children: readonly ChapterTreeNode[];\n readonly id: number;\n readonly title: string | null;\n}\n\nexport interface ChapterTreeInput {\n readonly chapters: readonly ChapterTreeInputNode[];\n}\n\nexport interface ChapterTreeInputNode {\n readonly children: readonly ChapterTreeInputNode[];\n readonly id: number;\n readonly title?: string | null | undefined;\n}\n\nexport interface ChapterTreeApplyResult {\n readonly changed: boolean;\n readonly moved: readonly ChapterTreeMoveChange[];\n readonly renamed: readonly ChapterTreeTitleChange[];\n readonly unchanged: number;\n}\n\nexport interface ChapterTreeMoveChange {\n readonly chapterId: number;\n readonly newIndex: number;\n readonly newParentChapterId: number | null;\n readonly newPath: readonly string[];\n readonly oldIndex: number;\n readonly oldParentChapterId: number | null;\n readonly oldPath: readonly string[];\n}\n\nexport interface ChapterTreeTitleChange {\n readonly chapterId: number;\n readonly newTitle: string | null;\n readonly oldTitle: string | null;\n}\n\nexport interface MoveChapterOptions {\n readonly afterChapterId?: number;\n readonly beforeChapterId?: number;\n readonly first?: boolean;\n readonly last?: boolean;\n readonly parentChapterId?: number;\n readonly root?: boolean;\n}\n\nexport interface AdvanceChapterStagesOptions {\n readonly chapterId?: number;\n readonly extractionPrompt: string;\n readonly llm: LLM<WikiGraphScope>;\n readonly logDirPath?: string;\n readonly onProgress?: AdvanceChapterStagesProgressCallback;\n readonly targetStage: ChapterStage;\n readonly userLanguage?: Language;\n}\n\nexport interface AdvanceChapterStagesProgressState {\n readonly graphWords: number;\n readonly summaryWords: number;\n readonly totalGraphWords: number;\n readonly totalSummaryWords: number;\n}\n\nexport type AdvanceChapterStagesProgressCallback = (\n event: AdvanceChapterStagesProgressEvent,\n) => void | Promise<void>;\n\nexport type AdvanceChapterStagesProgressEvent =\n | {\n readonly type: \"selected\";\n readonly state: AdvanceChapterStagesProgressState;\n readonly targetStage: ChapterStage;\n readonly totalChapters: number;\n }\n | {\n readonly type: \"skipped\";\n readonly chapter: ChapterEntry;\n readonly reason: \"planned\";\n readonly targetStage: ChapterStage;\n }\n | {\n readonly type: \"progress\";\n readonly state: AdvanceChapterStagesProgressState;\n readonly targetStage: ChapterStage;\n }\n | {\n readonly type: \"started\";\n readonly chapter: ChapterEntry;\n readonly step: \"graph\" | \"summary\";\n readonly targetStage: ChapterStage;\n }\n | {\n readonly type: \"completed\";\n readonly chapter: ChapterEntry;\n readonly step: \"graph\" | \"summary\";\n readonly targetStage: ChapterStage;\n };\n\nexport interface AdvanceChapterStagesResult {\n readonly advanced: readonly ChapterEntry[];\n readonly pending: readonly ChapterEntry[];\n readonly skipped: readonly ChapterEntry[];\n}\n\ninterface MutableAdvanceProgressState {\n addGraphWords(words: number): void;\n addSummaryWords(words: number): void;\n snapshot(): AdvanceChapterStagesProgressState;\n}\n\nexport interface AddChapterOptions {\n readonly parentChapterId?: number;\n readonly title?: string | null | undefined;\n}\n\nconst chapterTreeInputNodeSchema: z.ZodType<ChapterTreeInputNode> = z\n .object({\n children: z.lazy(() => z.array(chapterTreeInputNodeSchema)),\n id: z.number().int().nonnegative(),\n title: z.string().nullable().optional(),\n })\n .strict();\n\nconst chapterTreeInputSchema: z.ZodType<ChapterTreeInput> = z\n .object({\n chapters: z.array(chapterTreeInputNodeSchema),\n })\n .strict();\n\nexport function parseChapterTreeInput(input: unknown): ChapterTreeInput {\n return chapterTreeInputSchema.parse(input);\n}\n\nexport interface GenerateChapterGraphOptions {\n readonly extractionPrompt: string;\n readonly llm: LLM<WikiGraphScope>;\n readonly logDirPath?: string;\n readonly progressTracker?: SerialProgressSink;\n readonly userLanguage?: Language;\n}\n\nexport interface GenerateChapterSummaryOptions {\n readonly llm: LLM<WikiGraphScope>;\n readonly logDirPath?: string;\n readonly userLanguage?: Language;\n}\n\nexport async function advanceChapterStages(\n document: Document,\n options: AdvanceChapterStagesOptions,\n): Promise<AdvanceChapterStagesResult> {\n if (options.targetStage === \"planned\") {\n return {\n advanced: [],\n pending: await selectChapterEntries(document, options.chapterId),\n skipped: [],\n };\n }\n\n const advancedIds = new Set<number>();\n const entries = await selectChapterEntries(document, options.chapterId);\n const progressState = createAdvanceProgressState(\n entries,\n options.targetStage,\n );\n await emitAdvanceProgress(options, {\n state: progressState.snapshot(),\n targetStage: options.targetStage,\n totalChapters: entries.length,\n type: \"selected\",\n });\n await emitPlannedSkips(entries, options);\n\n if (isStageBefore(\"sourced\", options.targetStage)) {\n await advanceEntriesToGraphed(document, entries, options, {\n advancedIds,\n progressState,\n });\n }\n if (isStageBefore(\"graphed\", options.targetStage)) {\n const graphedEntries = await selectChapterEntries(\n document,\n options.chapterId,\n );\n\n await advanceEntriesToSummarized(document, graphedEntries, options, {\n advancedIds,\n progressState,\n });\n }\n\n const nextEntries = await selectChapterEntries(document, options.chapterId);\n\n return {\n advanced: nextEntries.filter((entry) => advancedIds.has(entry.chapterId)),\n pending: nextEntries.filter((entry) =>\n isStageBefore(entry.stage, options.targetStage),\n ),\n skipped: nextEntries.filter((entry) => entry.stage === \"planned\"),\n };\n}\n\nexport async function addChapter(\n document: Document,\n options: AddChapterOptions,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const toc = await normalizeChapterToc(openedDocument);\n const normalizedTitle = normalizeTitle(options.title);\n\n const chapterId = await openedDocument.createSerial();\n const chapterItem = {\n children: [],\n serialId: chapterId,\n ...(normalizedTitle === undefined ? {} : { title: normalizedTitle }),\n } satisfies TocItem;\n\n if (options.parentChapterId === undefined) {\n toc.items = [...toc.items, chapterItem];\n } else if (\n !appendChildToChapter(toc.items, options.parentChapterId, chapterItem)\n ) {\n throw new Error(\n `Chapter ${options.parentChapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n await openedDocument.replaceToc(toc);\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function generateChapterGraph(\n document: Document,\n chapterId: number,\n options: GenerateChapterGraphOptions,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const details = await requireChapterDetails(openedDocument, chapterId);\n\n if (details.stage !== \"sourced\") {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Generate a graph only for sourced chapters.`,\n );\n }\n\n const generation = new SerialGeneration({\n document: openedDocument,\n llm: options.llm,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n });\n await openedDocument.clearSerialGraph(chapterId);\n\n await generation.buildTopologyInto(\n chapterId,\n createTopologyOptions(options),\n options.progressTracker,\n );\n const language = normalizeLanguageCode(options.userLanguage);\n const parameter = await openedDocument.graphBuildParameters.save({\n prompt: options.extractionPrompt,\n ...(language === undefined ? {} : { language }),\n });\n await openedDocument.serials.setTopologyReady(\n chapterId,\n true,\n parameter.hash,\n );\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function generateChapterSummary(\n document: Document,\n chapterId: number,\n options: GenerateChapterSummaryOptions,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const details = await requireChapterDetails(openedDocument, chapterId);\n\n if (details.stage !== \"graphed\") {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Generate a summary only for graphed chapters.`,\n );\n }\n\n const generation = new SerialGeneration({\n document: openedDocument,\n llm: options.llm,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n });\n\n await generation.buildSummary(chapterId, {\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function getChapterDetails(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<ChapterDetails> {\n const toc = await readChapterToc(document);\n const entry = await findChapterEntry(document, toc.items, chapterId);\n\n if (entry === undefined) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n const serial = await document.serials.getById(chapterId);\n const summary = await document.readSummary(chapterId);\n\n return {\n ...entry,\n graphReady: serial?.topologyReady === true,\n hasSummary: summary !== undefined,\n };\n}\n\nexport async function listChapters(\n document: ReadonlyDocument,\n): Promise<readonly ChapterEntry[]> {\n const toc = await readChapterToc(document);\n\n return await collectChapterEntries(document, toc.items);\n}\n\nexport async function getChapterTree(\n document: ReadonlyDocument,\n): Promise<ChapterTree> {\n const toc = await readChapterToc(document);\n\n return {\n chapters: toc.items.flatMap(toChapterTreeNodes),\n };\n}\n\nexport async function applyChapterTree(\n document: Document,\n tree: ChapterTreeInput,\n options: { readonly dryRun?: boolean } = {},\n): Promise<ChapterTreeApplyResult> {\n return await document.openSession(async (openedDocument) => {\n const toc = await normalizeChapterToc(openedDocument);\n const oldItems = toc.items.map(cloneTocItem);\n const oldMetas = collectTocItemMetas(oldItems);\n const { items } = createTocItemsFromChapterTree(tree, oldItems);\n const newMetas = collectTocItemMetas(items);\n const result = createChapterTreeApplyResult(oldMetas, newMetas);\n\n if (options.dryRun !== true && result.changed) {\n await openedDocument.replaceToc({\n items,\n version: toc.version,\n });\n }\n\n return result;\n });\n}\n\nexport async function moveChapter(\n document: Document,\n chapterId: number,\n options: MoveChapterOptions,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const toc = await normalizeChapterToc(openedDocument);\n const originalLocation = findChapterLocation(toc.items, chapterId);\n const extracted = extractChapterItem(toc.items, chapterId);\n\n if (extracted.item === undefined) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n rejectMoveIntoOwnSubtree(chapterId, extracted.item, options);\n\n toc.items = extracted.items;\n insertMovedChapter(toc.items, extracted.item, {\n originalParentChapterId: originalLocation?.parentChapterId,\n ...options,\n });\n await openedDocument.replaceToc(toc);\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function removeChapter(\n document: Document,\n chapterId: number,\n options: { readonly recursive?: boolean } = {},\n): Promise<void> {\n await document.openSession(async (openedDocument) => {\n const toc = await normalizeChapterToc(openedDocument);\n const removedChapterIds: number[] = [];\n const result = removeChapterFromItems(toc.items, chapterId, {\n recursive: options.recursive ?? false,\n removedChapterIds,\n });\n\n if (!result.removed) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n toc.items = result.items;\n await openedDocument.replaceToc(toc);\n\n for (const removedChapterId of removedChapterIds) {\n await openedDocument.deleteSerial(removedChapterId);\n }\n });\n}\n\nexport async function resetChapter(\n document: Document,\n chapterId: number,\n stage: Exclude<ChapterStage, \"summarized\">,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n await requireChapterDetails(openedDocument, chapterId);\n\n switch (stage) {\n case \"planned\":\n await openedDocument.clearSerialSource(chapterId);\n break;\n case \"sourced\":\n await openedDocument.clearSerialGraph(chapterId);\n break;\n case \"graphed\":\n await openedDocument.deleteSummary(chapterId);\n break;\n }\n\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function setChapterSource(\n document: Document,\n chapterId: number,\n stream: ReaderTextStream,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const details = await requireChapterDetails(openedDocument, chapterId);\n\n if (details.stage !== \"planned\") {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Reset it to planned before setting source.`,\n );\n }\n\n await writeSerialSource(openedDocument, chapterId, stream);\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function setChapterSummary(\n document: Document,\n chapterId: number,\n summary: string,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const details = await requireChapterDetails(openedDocument, chapterId);\n\n if (details.stage !== \"graphed\") {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Set a summary only for graphed chapters.`,\n );\n }\n\n await openedDocument.writeSummary(chapterId, summary);\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nexport async function setChapterTitle(\n document: Document,\n chapterId: number,\n title: string | null | undefined,\n): Promise<ChapterDetails> {\n return await document.openSession(async (openedDocument) => {\n const toc = await normalizeChapterToc(openedDocument);\n const normalizedTitle = normalizeTitle(title);\n\n if (!setChapterTitleInItems(toc.items, chapterId, normalizedTitle)) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n await openedDocument.replaceToc(toc);\n return await getChapterDetails(openedDocument, chapterId);\n });\n}\n\nasync function normalizeChapterToc(\n document: Document,\n): Promise<MutableTocFile> {\n const existingToc = await document.readToc();\n const toc = await readChapterToc(document);\n let changed = false;\n\n const normalizeItems = async (items: MutableTocItem[]): Promise<void> => {\n for (const item of items) {\n if (item.serialId === undefined) {\n item.serialId = await document.createSerial();\n changed = true;\n } else {\n await document.serials.ensure(item.serialId);\n }\n\n await normalizeItems(item.children);\n }\n };\n\n await normalizeItems(toc.items);\n\n if (existingToc === undefined || changed) {\n await document.replaceToc(toc);\n }\n\n return toc;\n}\n\nasync function readChapterToc(\n document: ReadonlyDocument,\n): Promise<MutableTocFile> {\n const toc = await document.readToc();\n\n return toc === undefined\n ? { items: [], version: TOC_FILE_VERSION }\n : {\n items: toc.items.map(cloneTocItem),\n version: toc.version,\n };\n}\n\nasync function findChapterEntry(\n document: ReadonlyDocument,\n items: readonly TocItem[],\n chapterId: number,\n ancestorTitles: readonly string[] = [],\n depth = 0,\n): Promise<ChapterEntry | undefined> {\n for (const item of items) {\n const title = normalizeTitle(item.title) ?? null;\n const tocPath =\n item.serialId === undefined\n ? [...ancestorTitles, ...(title === null ? [] : [title])]\n : [...ancestorTitles, title ?? `Chapter ${item.serialId}`];\n\n if (item.serialId === chapterId) {\n return await createChapterEntry(document, item, item.serialId, {\n depth,\n title,\n tocPath,\n });\n }\n\n const childEntry = await findChapterEntry(\n document,\n item.children,\n chapterId,\n tocPath,\n depth + 1,\n );\n\n if (childEntry !== undefined) {\n return childEntry;\n }\n }\n\n return undefined;\n}\n\nasync function collectChapterEntries(\n document: ReadonlyDocument,\n items: readonly TocItem[],\n ancestorTitles: readonly string[] = [],\n depth = 0,\n): Promise<ChapterEntry[]> {\n const entries: ChapterEntry[] = [];\n\n for (const item of items) {\n const title = normalizeTitle(item.title) ?? null;\n const tocPath =\n item.serialId === undefined\n ? [...ancestorTitles, ...(title === null ? [] : [title])]\n : [...ancestorTitles, title ?? `Chapter ${item.serialId}`];\n\n if (item.serialId === undefined) {\n entries.push(\n ...(await collectChapterEntries(\n document,\n item.children,\n tocPath,\n depth + 1,\n )),\n );\n continue;\n }\n\n entries.push(\n await createChapterEntry(document, item, item.serialId, {\n depth,\n title,\n tocPath,\n }),\n );\n entries.push(\n ...(await collectChapterEntries(\n document,\n item.children,\n tocPath,\n depth + 1,\n )),\n );\n }\n\n return entries;\n}\n\nasync function createChapterEntry(\n document: ReadonlyDocument,\n item: TocItem,\n serialId: number,\n input: {\n readonly depth: number;\n readonly title: string | null;\n readonly tocPath: readonly string[];\n },\n): Promise<ChapterEntry> {\n const [serial, sourceSummary] = await Promise.all([\n document.serials.getById(serialId),\n summarizeSerialSource(document, serialId),\n ]);\n\n return {\n chapterId: serialId,\n childCount: item.children.length,\n depth: input.depth,\n documentOrder: serial?.documentOrder ?? serialId,\n fragmentCount: sourceSummary.fragmentCount,\n stage: await resolveChapterStage(\n document,\n serialId,\n sourceSummary.fragmentCount,\n ),\n title: input.title,\n tocPath: input.tocPath,\n words: sourceSummary.words,\n };\n}\n\nasync function summarizeSerialSource(\n document: ReadonlyDocument,\n serialId: number,\n): Promise<{ readonly fragmentCount: number; readonly words: number }> {\n const sentenceWords = await document.readDatabase(\n async (database) =>\n await database.queryAll(\n `\n SELECT words_count\n FROM text_sentence_records\n WHERE kind = 1 AND chapter_id = ?\n ORDER BY sentence_index\n `,\n [serialId],\n (row) => Number(row.words_count),\n ),\n );\n let fragmentCount = 0;\n let fragmentWords = 0;\n let words = 0;\n\n for (const sentenceWordCount of sentenceWords) {\n if (fragmentWords > 0 && fragmentWords + sentenceWordCount > 600) {\n fragmentCount += 1;\n fragmentWords = 0;\n }\n\n fragmentWords += sentenceWordCount;\n words += sentenceWordCount;\n }\n\n if (fragmentWords > 0) {\n fragmentCount += 1;\n }\n\n return {\n fragmentCount,\n words,\n };\n}\n\nasync function advanceEntriesToGraphed(\n document: Document,\n entries: readonly ChapterEntry[],\n options: AdvanceChapterStagesOptions,\n state: {\n readonly advancedIds: Set<number>;\n readonly progressState: MutableAdvanceProgressState;\n },\n): Promise<readonly ChapterEntry[]> {\n for (const entry of entries) {\n if (entry.stage !== \"sourced\") {\n continue;\n }\n\n await emitAdvanceProgress(options, {\n chapter: entry,\n step: \"graph\",\n targetStage: options.targetStage,\n type: \"started\",\n });\n await generateChapterGraph(document, entry.chapterId, {\n extractionPrompt: options.extractionPrompt,\n llm: options.llm,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n progressTracker: {\n async advance(wordsCount) {\n state.progressState.addGraphWords(wordsCount);\n await emitAdvanceProgress(options, {\n state: state.progressState.snapshot(),\n targetStage: options.targetStage,\n type: \"progress\",\n });\n },\n async complete() {},\n },\n });\n await emitAdvanceProgress(options, {\n chapter: entry,\n step: \"graph\",\n targetStage: options.targetStage,\n type: \"completed\",\n });\n state.advancedIds.add(entry.chapterId);\n }\n\n return await selectChapterEntries(document, options.chapterId);\n}\n\nasync function advanceEntriesToSummarized(\n document: Document,\n entries: readonly ChapterEntry[],\n options: AdvanceChapterStagesOptions,\n state: {\n readonly advancedIds: Set<number>;\n readonly progressState: MutableAdvanceProgressState;\n },\n): Promise<readonly ChapterEntry[]> {\n for (const entry of entries) {\n if (entry.stage !== \"graphed\") {\n continue;\n }\n\n await emitAdvanceProgress(options, {\n chapter: entry,\n step: \"summary\",\n targetStage: options.targetStage,\n type: \"started\",\n });\n await generateChapterSummary(document, entry.chapterId, {\n llm: options.llm,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n state.progressState.addSummaryWords(entry.words);\n await emitAdvanceProgress(options, {\n state: state.progressState.snapshot(),\n targetStage: options.targetStage,\n type: \"progress\",\n });\n await emitAdvanceProgress(options, {\n chapter: entry,\n step: \"summary\",\n targetStage: options.targetStage,\n type: \"completed\",\n });\n state.advancedIds.add(entry.chapterId);\n }\n\n return await selectChapterEntries(document, options.chapterId);\n}\n\nasync function selectChapterEntries(\n document: ReadonlyDocument,\n chapterId: number | undefined,\n): Promise<readonly ChapterEntry[]> {\n const entries = await listChapters(document);\n\n if (chapterId === undefined) {\n return entries;\n }\n\n const selectedIds = await collectChapterSubtreeIds(document, chapterId);\n\n if (selectedIds.size === 0) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n return entries.filter((entry) => selectedIds.has(entry.chapterId));\n}\n\nfunction createAdvanceProgressState(\n entries: readonly ChapterEntry[],\n targetStage: ChapterStage,\n): MutableAdvanceProgressState {\n const totalGraphWords = isStageBefore(\"sourced\", targetStage)\n ? entries\n .filter((entry) => entry.stage === \"sourced\")\n .reduce((sum, entry) => sum + entry.words, 0)\n : 0;\n const totalSummaryWords = isStageBefore(\"graphed\", targetStage)\n ? entries\n .filter(\n (entry) => entry.stage === \"graphed\" || entry.stage === \"sourced\",\n )\n .reduce((sum, entry) => sum + entry.words, 0)\n : 0;\n let graphWords = 0;\n let summaryWords = 0;\n\n return {\n addGraphWords(words) {\n graphWords = Math.min(totalGraphWords, graphWords + Math.max(0, words));\n },\n addSummaryWords(words) {\n summaryWords = Math.min(\n totalSummaryWords,\n summaryWords + Math.max(0, words),\n );\n },\n snapshot() {\n return {\n graphWords,\n summaryWords,\n totalGraphWords,\n totalSummaryWords,\n };\n },\n };\n}\n\nasync function collectChapterSubtreeIds(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<ReadonlySet<number>> {\n const toc = await readChapterToc(document);\n const selectedIds = new Set<number>();\n\n for (const item of toc.items) {\n if (collectChapterSubtreeIdsFromItem(item, chapterId, selectedIds)) {\n break;\n }\n }\n\n return selectedIds;\n}\n\nfunction collectChapterSubtreeIdsFromItem(\n item: TocItem,\n chapterId: number,\n selectedIds: Set<number>,\n): boolean {\n if (item.serialId === chapterId) {\n collectChapterIds(item, selectedIds);\n return true;\n }\n\n for (const child of item.children) {\n if (collectChapterSubtreeIdsFromItem(child, chapterId, selectedIds)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isStageBefore(\n stage: ChapterStage,\n targetStage: ChapterStage,\n): boolean {\n return CHAPTER_STAGES.indexOf(stage) < CHAPTER_STAGES.indexOf(targetStage);\n}\n\nasync function emitPlannedSkips(\n entries: readonly ChapterEntry[],\n options: AdvanceChapterStagesOptions,\n): Promise<void> {\n for (const entry of entries) {\n if (entry.stage !== \"planned\") {\n continue;\n }\n\n await emitAdvanceProgress(options, {\n chapter: entry,\n reason: \"planned\",\n targetStage: options.targetStage,\n type: \"skipped\",\n });\n }\n}\n\nasync function emitAdvanceProgress(\n options: AdvanceChapterStagesOptions,\n event: AdvanceChapterStagesProgressEvent,\n): Promise<void> {\n try {\n await options.onProgress?.(event);\n } catch {\n return;\n }\n}\n\nasync function resolveChapterStage(\n document: ReadonlyDocument,\n chapterId: number,\n fragmentCount: number,\n): Promise<ChapterStage> {\n const summarySentenceCount = await document.readDatabase(\n async (database) =>\n (await database.queryOne(\n `\n SELECT COUNT(*) AS count\n FROM text_sentence_records\n WHERE kind = 2 AND chapter_id = ?\n `,\n [chapterId],\n (row) => Number(row.count),\n )) ?? 0,\n );\n\n if (summarySentenceCount > 0) {\n return \"summarized\";\n }\n\n const serial = await document.serials.getById(chapterId);\n\n if (serial?.topologyReady === true) {\n return \"graphed\";\n }\n\n if (fragmentCount > 0) {\n return \"sourced\";\n }\n\n return \"planned\";\n}\n\nasync function requireChapterDetails(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<ChapterDetails> {\n return await getChapterDetails(document, chapterId);\n}\n\nfunction appendChildToChapter(\n items: MutableTocItem[],\n parentChapterId: number,\n child: MutableTocItem,\n): boolean {\n for (const item of items) {\n if (item.serialId === parentChapterId) {\n item.children = [...item.children, child];\n return true;\n }\n\n if (appendChildToChapter(item.children, parentChapterId, child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction createChapterTreeApplyResult(\n oldMetas: Map<number, TocItemMeta>,\n newMetas: Map<number, TocItemMeta>,\n): ChapterTreeApplyResult {\n const moved: ChapterTreeMoveChange[] = [];\n const renamed: ChapterTreeTitleChange[] = [];\n let unchanged = 0;\n\n for (const [chapterId, oldMeta] of oldMetas) {\n const newMeta = newMetas.get(chapterId);\n\n if (newMeta === undefined) {\n continue;\n }\n\n const movedChapter =\n oldMeta.parentChapterId !== newMeta.parentChapterId ||\n oldMeta.index !== newMeta.index;\n const renamedChapter = oldMeta.title !== newMeta.title;\n\n if (movedChapter) {\n moved.push({\n chapterId,\n newIndex: newMeta.index,\n newParentChapterId: newMeta.parentChapterId,\n newPath: newMeta.path,\n oldIndex: oldMeta.index,\n oldParentChapterId: oldMeta.parentChapterId,\n oldPath: oldMeta.path,\n });\n }\n if (renamedChapter) {\n renamed.push({\n chapterId,\n newTitle: newMeta.title,\n oldTitle: oldMeta.title,\n });\n }\n if (!movedChapter && !renamedChapter) {\n unchanged += 1;\n }\n }\n\n return {\n changed: moved.length > 0 || renamed.length > 0,\n moved,\n renamed,\n unchanged,\n };\n}\n\nfunction createTocItemsFromChapterTree(\n tree: ChapterTreeInput,\n oldItems: readonly MutableTocItem[],\n): { readonly items: MutableTocItem[] } {\n const oldItemsById = new Map<number, MutableTocItem>();\n const oldIds = new Set<number>();\n\n for (const oldItem of oldItems) {\n collectTocItemsById(oldItem, oldItemsById, oldIds);\n }\n\n const seenIds = new Set<number>();\n const items = tree.chapters.map((node) =>\n createTocItemFromChapterTreeNode(node, oldItemsById, seenIds),\n );\n const missingIds = [...oldIds].filter((id) => !seenIds.has(id));\n\n if (missingIds.length > 0) {\n throw new Error(\n `Chapter tree is missing chapter ids: ${missingIds.join(\", \")}.`,\n );\n }\n\n return { items };\n}\n\nfunction createTocItemFromChapterTreeNode(\n node: ChapterTreeInputNode,\n oldItemsById: Map<number, MutableTocItem>,\n seenIds: Set<number>,\n): MutableTocItem {\n const oldItem = oldItemsById.get(node.id);\n\n if (oldItem === undefined) {\n throw new Error(`Chapter tree references unknown chapter id: ${node.id}.`);\n }\n if (seenIds.has(node.id)) {\n throw new Error(`Chapter tree repeats chapter id: ${node.id}.`);\n }\n seenIds.add(node.id);\n\n const title = Object.prototype.hasOwnProperty.call(node, \"title\")\n ? normalizeTitle(node.title)\n : normalizeTitle(oldItem.title);\n const item: MutableTocItem = {\n children: node.children.map((child) =>\n createTocItemFromChapterTreeNode(child, oldItemsById, seenIds),\n ),\n serialId: node.id,\n };\n\n if (title !== undefined) {\n item.title = title;\n }\n\n return item;\n}\n\nfunction collectTocItemsById(\n item: MutableTocItem,\n itemsById: Map<number, MutableTocItem>,\n ids: Set<number>,\n): void {\n if (item.serialId !== undefined) {\n itemsById.set(item.serialId, item);\n ids.add(item.serialId);\n }\n\n for (const child of item.children) {\n collectTocItemsById(child, itemsById, ids);\n }\n}\n\nfunction collectTocItemMetas(\n items: readonly MutableTocItem[],\n parentChapterId: number | null = null,\n parentPath: readonly string[] = [],\n): Map<number, TocItemMeta> {\n const metas = new Map<number, TocItemMeta>();\n\n items.forEach((item, index) => {\n if (item.serialId === undefined) {\n return;\n }\n\n const title = normalizeTitle(item.title) ?? null;\n const path = [...parentPath, title ?? `Chapter ${item.serialId}`];\n\n metas.set(item.serialId, {\n index,\n parentChapterId,\n path,\n title,\n });\n\n for (const [childId, childMeta] of collectTocItemMetas(\n item.children,\n item.serialId,\n path,\n )) {\n metas.set(childId, childMeta);\n }\n });\n\n return metas;\n}\n\nfunction extractChapterItem(\n items: readonly MutableTocItem[],\n chapterId: number,\n): {\n readonly item?: MutableTocItem;\n readonly items: MutableTocItem[];\n} {\n const nextItems: MutableTocItem[] = [];\n\n for (const item of items) {\n if (item.serialId === chapterId) {\n return {\n item,\n items: [...nextItems, ...items.slice(nextItems.length + 1)],\n };\n }\n\n const childResult = extractChapterItem(item.children, chapterId);\n\n if (childResult.item !== undefined) {\n nextItems.push({\n ...item,\n children: childResult.items,\n });\n nextItems.push(...items.slice(nextItems.length));\n return {\n item: childResult.item,\n items: nextItems,\n };\n }\n\n nextItems.push(item);\n }\n\n return {\n items: [...items],\n };\n}\n\nfunction findChildContainer(\n items: MutableTocItem[],\n parentChapterId: number | undefined,\n): MutableTocItem[] | undefined {\n if (parentChapterId === undefined) {\n return items;\n }\n\n for (const item of items) {\n if (item.serialId === parentChapterId) {\n return item.children;\n }\n\n const childContainer = findChildContainer(item.children, parentChapterId);\n\n if (childContainer !== undefined) {\n return childContainer;\n }\n }\n\n return undefined;\n}\n\nfunction findChapterLocation(\n items: MutableTocItem[],\n chapterId: number,\n):\n | {\n readonly index: number;\n readonly parentChapterId?: number | undefined;\n readonly siblings: MutableTocItem[];\n }\n | undefined {\n return findChapterLocationInItems(items, chapterId);\n}\n\nfunction findChapterLocationInItems(\n items: MutableTocItem[],\n chapterId: number,\n parentChapterId?: number,\n):\n | {\n readonly index: number;\n readonly parentChapterId?: number | undefined;\n readonly siblings: MutableTocItem[];\n }\n | undefined {\n for (const [index, item] of items.entries()) {\n if (item.serialId === chapterId) {\n return {\n index,\n parentChapterId,\n siblings: items,\n };\n }\n\n const location = findChapterLocationInItems(\n item.children,\n chapterId,\n item.serialId,\n );\n\n if (location !== undefined) {\n return location;\n }\n }\n\n return undefined;\n}\n\nfunction insertMovedChapter(\n items: MutableTocItem[],\n item: MutableTocItem,\n options: MoveChapterOptions & {\n readonly originalParentChapterId?: number | undefined;\n },\n): void {\n if (\n options.beforeChapterId !== undefined ||\n options.afterChapterId !== undefined\n ) {\n const targetChapterId = options.beforeChapterId ?? options.afterChapterId!;\n const location = findChapterLocation(items, targetChapterId);\n\n if (location === undefined) {\n throw new Error(`Target chapter ${targetChapterId} does not exist.`);\n }\n\n location.siblings.splice(\n options.beforeChapterId === undefined\n ? location.index + 1\n : location.index,\n 0,\n item,\n );\n return;\n }\n\n const parentChapterId =\n options.root === true\n ? undefined\n : options.parentChapterId !== undefined\n ? options.parentChapterId\n : options.originalParentChapterId;\n const container = findChildContainer(items, parentChapterId);\n\n if (container === undefined) {\n throw new Error(`Chapter ${parentChapterId} does not exist.`);\n }\n\n if (options.first === true) {\n container.splice(0, 0, item);\n } else {\n container.push(item);\n }\n}\n\nfunction rejectMoveIntoOwnSubtree(\n chapterId: number,\n item: MutableTocItem,\n options: MoveChapterOptions,\n): void {\n const targetIds = [\n options.parentChapterId,\n options.beforeChapterId,\n options.afterChapterId,\n ].filter((id): id is number => id !== undefined);\n\n for (const targetId of targetIds) {\n if (targetId === chapterId || containsChapterId(item.children, targetId)) {\n throw new Error(\n `Cannot move chapter ${chapterId} into or next to its own descendant ${targetId}.`,\n );\n }\n }\n}\n\nfunction containsChapterId(\n items: readonly MutableTocItem[],\n chapterId: number,\n): boolean {\n for (const item of items) {\n if (\n item.serialId === chapterId ||\n containsChapterId(item.children, chapterId)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction cloneTocItem(item: TocItem): MutableTocItem {\n return {\n children: item.children.map(cloneTocItem),\n ...(item.serialId === undefined ? {} : { serialId: item.serialId }),\n title: item.title,\n };\n}\n\nfunction toChapterTreeNode(item: MutableTocItem): ChapterTreeNode {\n if (item.serialId === undefined) {\n throw new Error(\"Internal error: normalized chapter tree has no id.\");\n }\n\n return {\n children: item.children.flatMap(toChapterTreeNodes),\n id: item.serialId,\n title: normalizeTitle(item.title) ?? null,\n };\n}\n\nfunction toChapterTreeNodes(item: MutableTocItem): ChapterTreeNode[] {\n return item.serialId === undefined\n ? item.children.flatMap(toChapterTreeNodes)\n : [toChapterTreeNode(item)];\n}\n\nfunction createTopologyOptions(\n options: GenerateChapterGraphOptions,\n): BuildSerialTopologyOptions {\n return {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n };\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | undefined {\n const normalized = title?.trim();\n\n return normalized === undefined || normalized === \"\" ? undefined : normalized;\n}\n\nfunction removeChapterFromItems(\n items: readonly MutableTocItem[],\n chapterId: number,\n options: {\n readonly recursive: boolean;\n readonly removedChapterIds: number[];\n },\n): { readonly items: MutableTocItem[]; readonly removed: boolean } {\n const nextItems: MutableTocItem[] = [];\n let removed = false;\n\n for (const item of items) {\n if (item.serialId === chapterId) {\n if (!options.recursive && item.children.length > 0) {\n throw new Error(\n `Chapter ${chapterId} has child chapters. Use --recursive to remove it and its descendants.`,\n );\n }\n\n collectChapterIds(item, options.removedChapterIds);\n removed = true;\n continue;\n }\n\n const childResult = removeChapterFromItems(\n item.children,\n chapterId,\n options,\n );\n\n nextItems.push({\n ...item,\n children: childResult.items,\n });\n removed ||= childResult.removed;\n }\n\n return {\n items: nextItems,\n removed,\n };\n}\n\nfunction setChapterTitleInItems(\n items: readonly MutableTocItem[],\n chapterId: number,\n title: string | undefined,\n): boolean {\n for (const item of items) {\n if (item.serialId === chapterId) {\n if (title === undefined) {\n delete item.title;\n } else {\n item.title = title;\n }\n return true;\n }\n\n if (setChapterTitleInItems(item.children, chapterId, title)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction collectChapterIds(\n item: TocItem,\n chapterIds: number[] | Set<number>,\n): void {\n if (item.serialId !== undefined) {\n if (Array.isArray(chapterIds)) {\n chapterIds.push(item.serialId);\n } else {\n chapterIds.add(item.serialId);\n }\n }\n\n for (const child of item.children) {\n collectChapterIds(child, chapterIds);\n }\n}\n\ninterface MutableTocFile {\n items: MutableTocItem[];\n version: typeof TOC_FILE_VERSION;\n}\n\ninterface MutableTocItem {\n children: MutableTocItem[];\n serialId?: number | undefined;\n title?: string | null | undefined;\n}\n\ninterface TocItemMeta {\n readonly index: number;\n readonly parentChapterId: number | null;\n readonly path: readonly string[];\n readonly title: string | null;\n}\n","export interface LexicalQuery {\n readonly phrases: readonly string[];\n readonly terms: readonly string[];\n}\n\nexport interface LexicalScore {\n readonly matchCount: number;\n readonly matchedTerms: readonly string[];\n readonly missingTerms: readonly string[];\n readonly score: number;\n readonly snippetNeedle: string;\n}\n\nconst HAN_RE = /\\p{Script=Han}/u;\nconst LATIN_TOKEN_RE = /[\\p{Script=Latin}\\p{Number}]+/gu;\nconst PHRASE_WEIGHT = 2;\nconst TERM_WEIGHT = 1;\n\nexport function createLexicalQuery(query: string): LexicalQuery | undefined {\n const normalizedQuery = query.trim();\n\n if (normalizedQuery === \"\") {\n return undefined;\n }\n\n const phraseTerms = splitQueryPhrases(normalizedQuery);\n const characterTerms = [...normalizedQuery]\n .filter((character) => HAN_RE.test(character))\n .map((character) => character.toLowerCase());\n const latinTerms = normalizeLatinTokens(normalizedQuery);\n const terms = dedupeStrings([\n ...phraseTerms,\n ...latinTerms,\n ...characterTerms,\n ]);\n\n if (terms.length === 0) {\n return undefined;\n }\n\n return {\n phrases: phraseTerms,\n terms,\n };\n}\n\nexport function listLexicalQueryCandidateTerms(\n query: string,\n): readonly string[] {\n const normalizedQuery = query.trim();\n\n return dedupeStrings([\n ...splitQueryPhrases(normalizedQuery),\n ...normalizeLatinTokens(normalizedQuery),\n ...[...normalizedQuery]\n .filter((character) => HAN_RE.test(character))\n .map((character) => character.toLowerCase()),\n ]);\n}\n\nexport function scoreLexicalText(\n value: string,\n query: LexicalQuery,\n): LexicalScore | undefined {\n const normalizedText = value.toLowerCase();\n const matchedTerms: string[] = [];\n const missingTerms: string[] = [];\n let score = 0;\n\n for (const phrase of query.phrases) {\n const normalizedPhrase = phrase.toLowerCase();\n\n if (normalizedText.includes(normalizedPhrase)) {\n matchedTerms.push(phrase);\n score +=\n PHRASE_WEIGHT + countOccurrences(normalizedText, normalizedPhrase);\n } else if (!missingTerms.includes(phrase)) {\n missingTerms.push(phrase);\n }\n }\n\n const latinTokens = new Set(normalizeLatinTokens(value));\n\n for (const term of query.terms) {\n if (matchedTerms.includes(term)) {\n continue;\n }\n\n const isHan = [...term].every((character) => HAN_RE.test(character));\n const matched = isHan\n ? normalizedText.includes(term)\n : latinTokens.has(term.toLowerCase());\n\n if (matched) {\n matchedTerms.push(term);\n score += TERM_WEIGHT;\n } else if (!missingTerms.includes(term)) {\n missingTerms.push(term);\n }\n }\n\n if (matchedTerms.length === 0 || score <= 0) {\n return undefined;\n }\n\n score += calculateCoverageBonus(matchedTerms, query);\n score += calculateProximityBonus(normalizedText, matchedTerms);\n\n return {\n matchCount: matchedTerms.length,\n matchedTerms: dedupeStrings(matchedTerms),\n missingTerms: dedupeStrings(missingTerms).filter(\n (term) => !matchedTerms.includes(term),\n ),\n score,\n snippetNeedle: selectSnippetNeedle(matchedTerms),\n };\n}\n\nexport function createMentionLexicalHits<\n T extends { readonly surface: string },\n>(\n mentions: readonly T[],\n query: LexicalQuery,\n): readonly {\n readonly match: LexicalScore;\n readonly mention: T;\n}[] {\n return mentions\n .map((mention) => {\n const match = scoreLexicalText(mention.surface, query);\n\n return match === undefined ? undefined : { match, mention };\n })\n .filter(isDefined);\n}\n\nfunction splitQueryPhrases(query: string): readonly string[] {\n return dedupeStrings(\n query\n .split(/\\s+/u)\n .map((part) => part.trim())\n .filter((part) => part.length > 1),\n );\n}\n\nfunction normalizeLatinTokens(value: string): readonly string[] {\n return [...value.toLowerCase().matchAll(LATIN_TOKEN_RE)].map(\n (match) => match[0],\n );\n}\n\nfunction countOccurrences(value: string, needle: string): number {\n if (needle === \"\") {\n return 0;\n }\n\n let count = 0;\n let index = value.indexOf(needle);\n\n while (index >= 0) {\n count += 1;\n index = value.indexOf(needle, index + needle.length);\n }\n\n return count;\n}\n\nfunction calculateCoverageBonus(\n matchedTerms: readonly string[],\n query: LexicalQuery,\n): number {\n const total = query.terms.length;\n\n if (total === 0) {\n return 0;\n }\n\n return matchedTerms.length / total;\n}\n\nfunction calculateProximityBonus(\n normalizedText: string,\n matchedTerms: readonly string[],\n): number {\n const positions = matchedTerms\n .map((term) => normalizedText.indexOf(term.toLowerCase()))\n .filter((position) => position >= 0)\n .sort((left, right) => left - right);\n\n if (positions.length < 2) {\n return 0;\n }\n\n const span = positions.at(-1)! - positions[0]!;\n\n return span <= 80 ? 1 : span <= 240 ? 0.5 : 0;\n}\n\nfunction selectSnippetNeedle(matchedTerms: readonly string[]): string {\n return [...matchedTerms].sort(\n (left, right) => right.length - left.length,\n )[0]!;\n}\n\nfunction dedupeStrings(values: readonly string[]): readonly string[] {\n return [...new Set(values.filter((value) => value !== \"\"))];\n}\n\nfunction isDefined<T>(value: T | undefined): value is T {\n return value !== undefined;\n}\n","import { createHash } from \"crypto\";\nimport { stat } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphCacheDirectoryPath } from \"../../common/wiki-graph-dir.js\";\nimport { getNumber, getString } from \"../../document/database.js\";\nimport type { SqlBindValue } from \"../../document/database.js\";\nimport { openSharedStateDatabase } from \"../../document/index.js\";\nimport type { Database } from \"../../document/index.js\";\nimport type { GcContext, GcJobResult } from \"../../gc/index.js\";\nimport { isNodeError } from \"../../utils/node-error.js\";\n\nimport type { ArchiveFindHit } from \"./archive-view.js\";\n\nexport interface SearchSessionInput {\n readonly archiveKey: string;\n readonly chapters: readonly number[] | null;\n readonly chunkHits?: readonly SearchChunkHitInput[];\n readonly entityHits?: readonly SearchEntityHitInput[];\n readonly evidenceEvents?: readonly SearchEvidenceHitEventInput[];\n readonly items?: readonly ArchiveFindHit[];\n readonly lens: string;\n readonly match: string;\n readonly order: string;\n readonly query: string;\n readonly revisionScope: string;\n readonly terms: readonly string[];\n readonly tripleHits?: readonly SearchTripleHitInput[];\n readonly types: readonly string[] | null;\n}\n\nexport const SEARCH_EVIDENCE_KIND = {\n mention: 1,\n mentionLink: 2,\n chunk: 3,\n} as const;\n\nexport type SearchEvidenceKind =\n (typeof SEARCH_EVIDENCE_KIND)[keyof typeof SEARCH_EVIDENCE_KIND];\n\nexport interface SearchEvidenceHitEventInput {\n readonly chapterId: number;\n readonly evidenceId: string;\n readonly evidenceKind: SearchEvidenceKind;\n readonly score: number;\n readonly sentenceIndex: number;\n}\n\nexport interface SearchEntityHitInput {\n readonly evidenceTopScores?: readonly number[];\n readonly propertyTopScores?: readonly number[];\n readonly qid: string;\n}\n\nexport interface SearchTripleHitInput {\n readonly evidenceTopScores: readonly number[];\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n}\n\nexport interface SearchChunkHitInput {\n readonly chunkId: number;\n readonly evidenceTopScores?: readonly number[];\n readonly propertyTopScores?: readonly number[];\n}\n\nexport interface EntitySearchSessionInput {\n readonly archiveKey: string;\n readonly chapters: readonly number[] | null;\n readonly chunkHits?: readonly SearchChunkHitInput[];\n readonly entityHits?: readonly SearchEntityHitInput[];\n readonly evidenceEvents?: readonly SearchEvidenceHitEventInput[];\n readonly lens: string;\n readonly match: string;\n readonly order: string;\n readonly query: string;\n readonly revisionScope: string;\n readonly terms: readonly string[];\n readonly tripleHits?: readonly SearchTripleHitInput[];\n readonly types: readonly string[] | null;\n}\n\nexport interface SearchSessionPage {\n readonly chapters: readonly number[] | null;\n readonly items: readonly ArchiveFindHit[];\n readonly lens: string;\n readonly match: string;\n readonly nextCursor: string | null;\n readonly query: string;\n readonly sessionId: string;\n readonly terms: readonly string[];\n readonly types: readonly string[] | null;\n}\n\nexport interface EntitySearchSessionPage {\n readonly chapters: readonly number[] | null;\n readonly items: readonly ArchiveFindHit[];\n readonly lens: string;\n readonly match: string;\n readonly nextCursor: string | null;\n readonly query: string;\n readonly sessionId: string;\n readonly terms: readonly string[];\n readonly types: readonly string[] | null;\n}\n\nexport interface SearchSessionDescriptor {\n readonly chapters: readonly number[] | null;\n readonly createdAt: number;\n readonly lens: string;\n readonly match: string;\n readonly objectCachesPopulated: boolean;\n readonly query: string;\n readonly sessionId: string;\n readonly terms: readonly string[];\n readonly types: readonly string[] | null;\n}\n\nexport type BucketSearchCursor =\n | {\n readonly bucket: 0;\n readonly key?: SearchChapterTitleCursorKey;\n }\n | {\n readonly bucket: 1;\n readonly key?: SearchObjectCursorKey;\n }\n | {\n readonly bucket: 2;\n readonly key?: SearchChunkCursorKey;\n }\n | {\n readonly bucket: 3;\n readonly key?: SearchTextCursorKey;\n };\n\nexport interface SearchChapterTitleCursorKey {\n readonly chapterId: number;\n readonly score: number;\n}\n\nexport interface SearchObjectCursorKey {\n readonly id: string;\n readonly kind: \"entity\" | \"triple\";\n readonly score: number;\n}\n\nexport interface SearchChunkCursorKey {\n readonly chunkId: number;\n readonly score: number;\n}\n\nexport interface SearchTextCursorKey {\n readonly chapterId: number;\n readonly kind: number;\n readonly rank: number;\n readonly sentenceIndex: number;\n}\n\nconst SEARCH_SESSION_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS search_sessions (\n session_id TEXT PRIMARY KEY,\n archive_key TEXT NOT NULL,\n query TEXT NOT NULL,\n options_json TEXT NOT NULL,\n terms_json TEXT NOT NULL,\n lens TEXT NOT NULL,\n match TEXT NOT NULL,\n object_caches_populated INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n accessed_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS search_results (\n session_id TEXT NOT NULL,\n rank INTEGER NOT NULL,\n item_json TEXT NOT NULL,\n PRIMARY KEY (session_id, rank)\n);\n\nCREATE TABLE IF NOT EXISTS predicate_dictionary (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n value TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS search_evidence_hit_events (\n session_id TEXT NOT NULL,\n evidence_kind INTEGER NOT NULL,\n evidence_id TEXT NOT NULL,\n chapter_id INTEGER NOT NULL,\n sentence_index INTEGER NOT NULL,\n score REAL NOT NULL,\n PRIMARY KEY (\n session_id,\n evidence_kind,\n evidence_id,\n chapter_id,\n sentence_index\n )\n);\n\nCREATE INDEX IF NOT EXISTS idx_search_evidence_hit_events_evidence_rank\nON search_evidence_hit_events(session_id, evidence_kind, evidence_id, score DESC, chapter_id, sentence_index);\n\nCREATE INDEX IF NOT EXISTS idx_search_evidence_hit_events_sentence\nON search_evidence_hit_events(session_id, chapter_id, sentence_index, evidence_kind, evidence_id);\n\nCREATE TABLE IF NOT EXISTS search_entity_hits (\n session_id TEXT NOT NULL,\n qid TEXT NOT NULL,\n property_top_scores_json TEXT NOT NULL DEFAULT '[]',\n evidence_top_scores_json TEXT NOT NULL DEFAULT '[]',\n property_score REAL NOT NULL DEFAULT 0,\n evidence_score REAL NOT NULL DEFAULT 0,\n result_score REAL NOT NULL DEFAULT 0,\n PRIMARY KEY (session_id, qid)\n);\n\nCREATE INDEX IF NOT EXISTS idx_search_entity_hits_rank\nON search_entity_hits(session_id, result_score DESC, qid);\n\nCREATE TABLE IF NOT EXISTS search_triple_hits (\n session_id TEXT NOT NULL,\n subject_qid TEXT NOT NULL,\n predicate_id INTEGER NOT NULL,\n object_qid TEXT NOT NULL,\n evidence_top_scores_json TEXT NOT NULL DEFAULT '[]',\n result_score REAL NOT NULL DEFAULT 0,\n PRIMARY KEY (session_id, subject_qid, predicate_id, object_qid),\n FOREIGN KEY (predicate_id) REFERENCES predicate_dictionary(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_search_triple_hits_rank\nON search_triple_hits(session_id, result_score DESC, subject_qid, predicate_id, object_qid);\n\nCREATE TABLE IF NOT EXISTS search_chunk_hits (\n session_id TEXT NOT NULL,\n chunk_id INTEGER NOT NULL,\n property_top_scores_json TEXT NOT NULL DEFAULT '[]',\n evidence_top_scores_json TEXT NOT NULL DEFAULT '[]',\n property_score REAL NOT NULL DEFAULT 0,\n evidence_score REAL NOT NULL DEFAULT 0,\n result_score REAL NOT NULL DEFAULT 0,\n PRIMARY KEY (session_id, chunk_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_search_chunk_hits_rank\nON search_chunk_hits(session_id, result_score DESC, chunk_id);\n\nCREATE INDEX IF NOT EXISTS idx_search_sessions_archive\nON search_sessions(archive_key, session_id);\n\nCREATE INDEX IF NOT EXISTS idx_search_sessions_expires\nON search_sessions(expires_at);\n\nCREATE INDEX IF NOT EXISTS idx_search_sessions_prune\nON search_sessions(accessed_at DESC, created_at DESC, session_id);\n`;\n\nconst SEARCH_RANKING_VERSION = 6;\nconst SEARCH_SESSION_MAX_COUNT = 500;\nconst SEARCH_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;\nconst SEARCH_TOP_SCORE_COUNT = 10;\n\ntype SearchSessionCacheInput = Omit<SearchSessionInput, \"items\">;\ntype EntitySearchSessionCacheInput = EntitySearchSessionInput;\n\nexport async function readCachedSearchSessionPage(\n input: SearchSessionCacheInput,\n offset: number,\n limit: number,\n): Promise<SearchSessionPage | undefined> {\n const sessionId = createSearchSessionId(input);\n\n if (!(await hasSearchSession(sessionId, input.archiveKey))) {\n return undefined;\n }\n\n return await readSearchSessionPage(\n sessionId,\n offset,\n limit,\n input.archiveKey,\n );\n}\n\nexport async function readCachedEntitySearchSessionPage(\n input: EntitySearchSessionCacheInput,\n offset: number,\n limit: number,\n): Promise<EntitySearchSessionPage | undefined> {\n const sessionId = createEntitySearchSessionId(input);\n\n if (!(await hasSearchSession(sessionId, input.archiveKey))) {\n return undefined;\n }\n\n return await readEntitySearchSessionPage(\n sessionId,\n offset,\n limit,\n input.archiveKey,\n );\n}\n\nexport async function createSearchSession(\n input: SearchSessionInput,\n): Promise<string> {\n const database = await openSearchSessionDatabase();\n\n try {\n const now = Date.now();\n const sessionId = createSearchSessionId(input);\n const optionsJSON = JSON.stringify({\n chapters: input.chapters,\n order: input.order,\n types: input.types,\n });\n\n await database.transaction(async () => {\n await deleteSearchSession(database, sessionId);\n await database.run(\n `\n INSERT INTO search_sessions (\n session_id, archive_key, query, options_json, terms_json, lens,\n match, created_at, expires_at, accessed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n sessionId,\n input.archiveKey,\n input.query,\n optionsJSON,\n JSON.stringify(input.terms),\n input.lens,\n input.match,\n now,\n now + SEARCH_SESSION_TTL_MS,\n now,\n ],\n );\n\n for (const [index, item] of (input.items ?? []).entries()) {\n await database.run(\n `\n INSERT INTO search_results (session_id, rank, item_json)\n VALUES (?, ?, ?)\n `,\n [sessionId, index, JSON.stringify(item)],\n );\n }\n for (const event of input.evidenceEvents ?? []) {\n await insertSearchEvidenceHitEvent(database, sessionId, event);\n }\n for (const hit of input.entityHits ?? []) {\n await upsertSearchEntityHit(database, sessionId, hit);\n }\n for (const hit of input.tripleHits ?? []) {\n await upsertSearchTripleHit(database, sessionId, hit);\n }\n for (const hit of input.chunkHits ?? []) {\n await upsertSearchChunkHit(database, sessionId, hit);\n }\n });\n\n return sessionId;\n } finally {\n await database.close();\n }\n}\n\nexport async function readSearchSessionObjectBucketPage(\n sessionId: string,\n bucket: 1,\n after: SearchObjectCursorKey | undefined,\n limit: number,\n): Promise<readonly ArchiveFindHit[]> {\n const database = await openSearchSessionDatabase();\n\n try {\n const entityRows = await readSearchSessionEntityBucketRows(\n database,\n sessionId,\n after,\n limit + 1,\n );\n const tripleRows = await readSearchSessionTripleBucketRows(\n database,\n sessionId,\n after,\n limit + 1,\n );\n\n return [...entityRows, ...tripleRows]\n .sort(compareObjectBucketHits)\n .slice(0, limit + 1);\n } finally {\n await database.close();\n }\n}\n\nexport async function readSearchSessionChunkBucketPage(\n sessionId: string,\n after: SearchChunkCursorKey | undefined,\n limit: number,\n): Promise<readonly ArchiveFindHit[]> {\n const database = await openSearchSessionDatabase();\n\n try {\n return await database.queryAll(\n `\n SELECT\n chunk_id,\n result_score\n FROM search_chunk_hits\n WHERE session_id = ?\n ${\n after === undefined\n ? \"\"\n : `\n AND (\n result_score < ?\n OR (result_score = ? AND chunk_id > ?)\n )\n `\n }\n ORDER BY result_score DESC, chunk_id\n LIMIT ?\n `,\n [\n sessionId,\n ...(after === undefined\n ? []\n : [after.score, after.score, after.chunkId]),\n limit + 1,\n ],\n (row) => {\n const chunkId = getNumber(row, \"chunk_id\");\n\n return {\n field: \"title\",\n id: `wikg://chunk/${chunkId}`,\n score: getNumber(row, \"result_score\"),\n snippet: `chunk ${chunkId}`,\n title: `chunk ${chunkId}`,\n type: \"node\",\n };\n },\n );\n } finally {\n await database.close();\n }\n}\n\nexport async function createEntitySearchSession(\n input: EntitySearchSessionInput,\n): Promise<string> {\n const database = await openSearchSessionDatabase();\n\n try {\n const now = Date.now();\n const sessionId = createEntitySearchSessionId(input);\n const optionsJSON = JSON.stringify({\n chapters: input.chapters,\n order: input.order,\n types: input.types,\n });\n\n await database.transaction(async () => {\n await deleteSearchSession(database, sessionId);\n await database.run(\n `\n INSERT INTO search_sessions (\n session_id, archive_key, query, options_json, terms_json, lens,\n match, created_at, expires_at, accessed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n sessionId,\n input.archiveKey,\n input.query,\n optionsJSON,\n JSON.stringify(input.terms),\n input.lens,\n input.match,\n now,\n now + SEARCH_SESSION_TTL_MS,\n now,\n ],\n );\n\n for (const event of input.evidenceEvents ?? []) {\n await insertSearchEvidenceHitEvent(database, sessionId, event);\n }\n for (const hit of input.entityHits ?? []) {\n await upsertSearchEntityHit(database, sessionId, hit);\n }\n for (const hit of input.tripleHits ?? []) {\n await upsertSearchTripleHit(database, sessionId, hit);\n }\n for (const hit of input.chunkHits ?? []) {\n await upsertSearchChunkHit(database, sessionId, hit);\n }\n });\n\n return sessionId;\n } finally {\n await database.close();\n }\n}\n\nexport async function populateSearchSessionObjectCaches(input: {\n readonly chunkHits?: readonly SearchChunkHitInput[];\n readonly entityHits?: readonly SearchEntityHitInput[];\n readonly evidenceEvents?: readonly SearchEvidenceHitEventInput[];\n readonly sessionId: string;\n readonly tripleHits?: readonly SearchTripleHitInput[];\n}): Promise<void> {\n const database = await openSearchSessionDatabase();\n\n try {\n await database.transaction(async () => {\n for (const event of input.evidenceEvents ?? []) {\n await insertSearchEvidenceHitEvent(database, input.sessionId, event);\n }\n for (const hit of input.entityHits ?? []) {\n await upsertSearchEntityHit(database, input.sessionId, hit);\n }\n for (const hit of input.tripleHits ?? []) {\n await upsertSearchTripleHit(database, input.sessionId, hit);\n }\n for (const hit of input.chunkHits ?? []) {\n await upsertSearchChunkHit(database, input.sessionId, hit);\n }\n await database.run(\n `\n UPDATE search_sessions\n SET object_caches_populated = 1\n WHERE session_id = ?\n `,\n [input.sessionId],\n );\n });\n } finally {\n await database.close();\n }\n}\n\nexport async function deleteArchiveSearchSessions(\n archiveKey: string,\n): Promise<void> {\n const database = await openSearchSessionDatabase();\n\n try {\n const sessionIds = await database.queryAll(\n `\n SELECT session_id\n FROM search_sessions\n WHERE archive_key = ?\n `,\n [archiveKey],\n (row) => getString(row, \"session_id\"),\n );\n\n await database.transaction(async () => {\n for (const sessionId of sessionIds) {\n await deleteSearchSession(database, sessionId);\n }\n await deleteUnusedPredicates(database);\n });\n } finally {\n await database.close();\n }\n}\n\nexport async function runSearchCacheGc(\n context: GcContext,\n): Promise<GcJobResult> {\n const databasePath = getSearchSessionDatabasePath();\n const beforeBytes = await readFileSize(databasePath);\n const database = await openSearchSessionDatabase();\n\n try {\n const scanned = await database.queryOne(\n \"SELECT COUNT(*) AS count FROM search_sessions\",\n undefined,\n (row) => getNumber(row, \"count\"),\n );\n const expiredSessionIds = context.force\n ? await listAllSearchSessionIds(database)\n : await listExpiredSearchSessionIds(database, context.now);\n const prunedSessionIds = context.force\n ? []\n : await listPrunedSearchSessionIds(database);\n const sessionIds = [\n ...new Set([...expiredSessionIds, ...prunedSessionIds]),\n ].sort();\n\n if (!context.dryRun && sessionIds.length > 0) {\n await database.transaction(async () => {\n for (const sessionId of sessionIds) {\n await deleteSearchSession(database, sessionId);\n }\n await deleteUnusedPredicates(database);\n });\n await database.run(\"VACUUM\");\n }\n\n const afterBytes = await readFileSize(databasePath);\n\n return {\n freedBytes: Math.max(0, beforeBytes - afterBytes),\n removed: sessionIds.length,\n scanned: scanned ?? 0,\n };\n } finally {\n await database.close();\n }\n}\n\nexport async function readSearchSessionPage(\n sessionId: string,\n offset: number,\n limit: number,\n expectedArchiveKey?: string,\n expectedCreatedAt?: number,\n): Promise<SearchSessionPage> {\n const database = await openSearchSessionDatabase();\n\n try {\n const session = await readSearchSessionMetadata(\n database,\n sessionId,\n expectedArchiveKey,\n expectedCreatedAt,\n );\n\n await touchSearchSession(database, sessionId);\n\n const rows = await database.queryAll(\n `\n SELECT rank, item_json\n FROM search_results\n WHERE session_id = ? AND rank >= ?\n ORDER BY rank\n LIMIT ?\n `,\n [sessionId, offset, limit + 1],\n (row) => ({\n item: parseSearchResultItem(getString(row, \"item_json\")),\n rank: getNumber(row, \"rank\"),\n }),\n );\n const items = rows.slice(0, limit).map((row) => row.item);\n const last = rows.at(limit - 1);\n\n return {\n chapters: session.options.chapters,\n items,\n lens: session.lens,\n match: session.match,\n nextCursor:\n rows.length > limit && last !== undefined\n ? encodeSearchSessionCursor(\n sessionId,\n last.rank + 1,\n session.createdAt,\n )\n : null,\n query: session.query,\n sessionId,\n terms: session.terms,\n types: session.options.types,\n };\n } finally {\n await database.close();\n }\n}\n\nexport async function readEntitySearchSessionPage(\n sessionId: string,\n offset: number,\n limit: number,\n expectedArchiveKey?: string,\n expectedCreatedAt?: number,\n): Promise<EntitySearchSessionPage> {\n const database = await openSearchSessionDatabase();\n\n try {\n const session = await readSearchSessionMetadata(\n database,\n sessionId,\n expectedArchiveKey,\n expectedCreatedAt,\n );\n\n await touchSearchSession(database, sessionId);\n\n const rows = await database.queryAll(\n `\n SELECT\n qid,\n result_score\n FROM search_entity_hits\n WHERE session_id = ?\n ORDER BY result_score DESC, qid\n LIMIT ? OFFSET ?\n `,\n [sessionId, limit + 1, offset],\n mapEntitySearchObjectRow,\n );\n const items = rows.slice(0, limit);\n const nextOffset = offset + items.length;\n\n return {\n chapters: session.options.chapters,\n items,\n lens: session.lens,\n match: session.match,\n nextCursor:\n rows.length > limit\n ? encodeSearchSessionCursor(sessionId, nextOffset, session.createdAt)\n : null,\n query: session.query,\n sessionId,\n terms: session.terms,\n types: session.options.types,\n };\n } finally {\n await database.close();\n }\n}\n\nexport async function readSearchSessionDescriptor(\n sessionId: string,\n expectedArchiveKey?: string,\n): Promise<SearchSessionDescriptor> {\n const database = await openSearchSessionDatabase();\n\n try {\n const session = await readSearchSessionMetadata(\n database,\n sessionId,\n expectedArchiveKey,\n );\n\n await touchSearchSession(database, sessionId);\n\n return {\n chapters: session.options.chapters,\n createdAt: session.createdAt,\n lens: session.lens,\n match: session.match,\n objectCachesPopulated: session.objectCachesPopulated,\n query: session.query,\n sessionId,\n terms: session.terms,\n types: session.options.types,\n };\n } finally {\n await database.close();\n }\n}\n\nexport async function readSearchSessionMetadataForCursor(\n sessionId: string,\n expectedArchiveKey?: string,\n expectedCreatedAt?: number,\n): Promise<SearchSessionDescriptor> {\n const database = await openSearchSessionDatabase();\n\n try {\n const session = await readSearchSessionMetadata(\n database,\n sessionId,\n expectedArchiveKey,\n expectedCreatedAt,\n );\n\n await touchSearchSession(database, sessionId);\n\n return {\n chapters: session.options.chapters,\n createdAt: session.createdAt,\n lens: session.lens,\n match: session.match,\n objectCachesPopulated: session.objectCachesPopulated,\n query: session.query,\n sessionId,\n terms: session.terms,\n types: session.options.types,\n };\n } finally {\n await database.close();\n }\n}\n\nexport async function readEntitySearchEvidenceMentions(\n sessionId: string,\n mentionIds: readonly string[],\n limit: number,\n): Promise<readonly { readonly mentionId: string; readonly score: number }[]> {\n if (mentionIds.length === 0) {\n return [];\n }\n\n const database = await openSearchSessionDatabase();\n\n try {\n const placeholders = mentionIds.map(() => \"?\").join(\", \");\n\n return await database.queryAll(\n `\n SELECT\n event.evidence_id AS mention_id,\n event.score AS score\n FROM search_evidence_hit_events AS event\n WHERE event.session_id = ?\n AND event.evidence_kind = ?\n AND event.evidence_id IN (${placeholders})\n ORDER BY event.score DESC, event.chapter_id, event.sentence_index,\n event.evidence_id\n LIMIT ?\n `,\n [sessionId, SEARCH_EVIDENCE_KIND.mention, ...mentionIds, limit],\n (row) => ({\n mentionId: getString(row, \"mention_id\"),\n score: getNumber(row, \"score\"),\n }),\n );\n } finally {\n await database.close();\n }\n}\n\nexport function encodeSearchSessionCursor(\n sessionId: string,\n offset: number,\n createdAt: number,\n): string {\n return Buffer.from(\n JSON.stringify({ createdAt, offset, sessionId, v: 3 }),\n ).toString(\"base64url\");\n}\n\nexport function encodeBucketSearchSessionCursor(\n sessionId: string,\n cursor: BucketSearchCursor,\n createdAt: number,\n): string {\n return Buffer.from(\n JSON.stringify({ createdAt, cursor, sessionId, v: 4 }),\n ).toString(\"base64url\");\n}\n\nexport function decodeBucketSearchSessionCursor(cursor: string): {\n readonly createdAt: number;\n readonly cursor: BucketSearchCursor;\n readonly sessionId: string;\n} {\n try {\n const parsed: unknown = JSON.parse(\n Buffer.from(cursor, \"base64url\").toString(\"utf8\"),\n );\n\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"createdAt\" in parsed &&\n \"cursor\" in parsed &&\n \"sessionId\" in parsed &&\n \"v\" in parsed &&\n parsed.v === 4 &&\n typeof parsed.createdAt === \"number\" &&\n Number.isInteger(parsed.createdAt) &&\n parsed.createdAt >= 0 &&\n typeof parsed.sessionId === \"string\" &&\n parsed.sessionId !== \"\" &&\n isBucketSearchCursor(parsed.cursor)\n ) {\n return {\n createdAt: parsed.createdAt,\n cursor: parsed.cursor,\n sessionId: parsed.sessionId,\n };\n }\n } catch {\n throw new Error(\"Invalid search cursor.\");\n }\n\n throw new Error(\"Invalid search cursor.\");\n}\n\nexport function decodeSearchSessionCursor(cursor: string): {\n readonly createdAt?: number;\n readonly offset: number;\n readonly sessionId: string;\n} {\n try {\n const parsed: unknown = JSON.parse(\n Buffer.from(cursor, \"base64url\").toString(\"utf8\"),\n );\n\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"offset\" in parsed &&\n \"sessionId\" in parsed &&\n \"createdAt\" in parsed &&\n \"v\" in parsed &&\n parsed.v === 3 &&\n typeof parsed.sessionId === \"string\" &&\n parsed.sessionId !== \"\" &&\n typeof parsed.createdAt === \"number\" &&\n Number.isInteger(parsed.createdAt) &&\n parsed.createdAt >= 0 &&\n typeof parsed.offset === \"number\" &&\n Number.isInteger(parsed.offset) &&\n parsed.offset >= 0\n ) {\n return {\n createdAt: parsed.createdAt,\n offset: parsed.offset,\n sessionId: parsed.sessionId,\n };\n }\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"offset\" in parsed &&\n \"sessionId\" in parsed &&\n \"v\" in parsed &&\n parsed.v === 2 &&\n typeof parsed.sessionId === \"string\" &&\n parsed.sessionId !== \"\" &&\n typeof parsed.offset === \"number\" &&\n Number.isInteger(parsed.offset) &&\n parsed.offset >= 0\n ) {\n return { offset: parsed.offset, sessionId: parsed.sessionId };\n }\n } catch {\n throw new Error(\"Invalid search cursor.\");\n }\n\n throw new Error(\"Invalid search cursor.\");\n}\n\nasync function openSearchSessionDatabase(): Promise<Database> {\n return await openSharedStateDatabase(\n getSearchSessionDatabasePath(),\n SEARCH_SESSION_SCHEMA_SQL,\n );\n}\n\nfunction getSearchSessionDatabasePath(): string {\n return join(getSearchSessionStateDirectoryPath(), \"search-sessions.sqlite\");\n}\n\nfunction getSearchSessionStateDirectoryPath(): string {\n return resolveWikiGraphCacheDirectoryPath();\n}\n\nasync function listExpiredSearchSessionIds(\n database: Database,\n now: number,\n): Promise<string[]> {\n return await database.queryAll(\n `\n SELECT session_id\n FROM search_sessions\n WHERE expires_at < ?\n `,\n [now],\n (row) => getString(row, \"session_id\"),\n );\n}\n\nasync function listAllSearchSessionIds(database: Database): Promise<string[]> {\n return await database.queryAll(\n \"SELECT session_id FROM search_sessions\",\n undefined,\n (row) => getString(row, \"session_id\"),\n );\n}\n\nasync function hasSearchSession(\n sessionId: string,\n archiveKey: string,\n): Promise<boolean> {\n const database = await openSearchSessionDatabase();\n\n try {\n const row = await database.queryOne(\n `\n SELECT session_id\n FROM search_sessions\n WHERE session_id = ? AND archive_key = ?\n AND expires_at >= ?\n `,\n [sessionId, archiveKey, Date.now()],\n () => true,\n );\n\n return row === true;\n } finally {\n await database.close();\n }\n}\n\nasync function deleteSearchSession(\n database: Database,\n sessionId: string,\n): Promise<void> {\n await database.run(\"DELETE FROM search_results WHERE session_id = ?\", [\n sessionId,\n ]);\n await database.run(\n \"DELETE FROM search_evidence_hit_events WHERE session_id = ?\",\n [sessionId],\n );\n await database.run(\"DELETE FROM search_entity_hits WHERE session_id = ?\", [\n sessionId,\n ]);\n await database.run(\"DELETE FROM search_triple_hits WHERE session_id = ?\", [\n sessionId,\n ]);\n await database.run(\"DELETE FROM search_chunk_hits WHERE session_id = ?\", [\n sessionId,\n ]);\n await database.run(\"DELETE FROM search_sessions WHERE session_id = ?\", [\n sessionId,\n ]);\n}\n\nasync function deleteUnusedPredicates(database: Database): Promise<void> {\n await database.run(`\n DELETE FROM predicate_dictionary\n WHERE NOT EXISTS (\n SELECT 1\n FROM search_triple_hits\n WHERE search_triple_hits.predicate_id = predicate_dictionary.id\n )\n `);\n}\n\nasync function insertSearchEvidenceHitEvent(\n database: Database,\n sessionId: string,\n event: SearchEvidenceHitEventInput,\n): Promise<void> {\n await database.run(\n `\n INSERT OR REPLACE INTO search_evidence_hit_events (\n session_id,\n evidence_kind,\n evidence_id,\n chapter_id,\n sentence_index,\n score\n )\n VALUES (?, ?, ?, ?, ?, ?)\n `,\n [\n sessionId,\n event.evidenceKind,\n event.evidenceId,\n event.chapterId,\n event.sentenceIndex,\n event.score,\n ],\n );\n}\n\nasync function upsertSearchEntityHit(\n database: Database,\n sessionId: string,\n hit: SearchEntityHitInput,\n): Promise<void> {\n const current = await database.queryOne(\n `\n SELECT property_top_scores_json, evidence_top_scores_json\n FROM search_entity_hits\n WHERE session_id = ? AND qid = ?\n `,\n [sessionId, hit.qid],\n (row) => ({\n evidenceTopScores: parseNumberArray(\n getString(row, \"evidence_top_scores_json\"),\n ),\n propertyTopScores: parseNumberArray(\n getString(row, \"property_top_scores_json\"),\n ),\n }),\n );\n const propertyTopScores = mergeTopScores(\n current?.propertyTopScores ?? [],\n hit.propertyTopScores ?? [],\n );\n const evidenceTopScores = mergeTopScores(\n current?.evidenceTopScores ?? [],\n hit.evidenceTopScores ?? [],\n );\n const propertyScore = aggregateCachedScores(propertyTopScores);\n const evidenceScore = aggregateCachedScores(evidenceTopScores);\n const resultScore = propertyScore + evidenceScore;\n\n await database.run(\n `\n INSERT INTO search_entity_hits (\n session_id,\n qid,\n property_top_scores_json,\n evidence_top_scores_json,\n property_score,\n evidence_score,\n result_score\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id, qid) DO UPDATE SET\n property_top_scores_json = excluded.property_top_scores_json,\n evidence_top_scores_json = excluded.evidence_top_scores_json,\n property_score = excluded.property_score,\n evidence_score = excluded.evidence_score,\n result_score = excluded.result_score\n `,\n [\n sessionId,\n hit.qid,\n JSON.stringify(propertyTopScores),\n JSON.stringify(evidenceTopScores),\n propertyScore,\n evidenceScore,\n resultScore,\n ],\n );\n}\n\nasync function upsertSearchTripleHit(\n database: Database,\n sessionId: string,\n hit: SearchTripleHitInput,\n): Promise<void> {\n const predicateId = await getOrCreatePredicateId(database, hit.predicate);\n const current = await database.queryOne(\n `\n SELECT evidence_top_scores_json\n FROM search_triple_hits\n WHERE session_id = ?\n AND subject_qid = ?\n AND predicate_id = ?\n AND object_qid = ?\n `,\n [sessionId, hit.subjectQid, predicateId, hit.objectQid],\n (row) => ({\n evidenceTopScores: parseNumberArray(\n getString(row, \"evidence_top_scores_json\"),\n ),\n }),\n );\n const evidenceTopScores = mergeTopScores(\n current?.evidenceTopScores ?? [],\n hit.evidenceTopScores,\n );\n const resultScore = aggregateCachedScores(evidenceTopScores);\n\n await database.run(\n `\n INSERT INTO search_triple_hits (\n session_id,\n subject_qid,\n predicate_id,\n object_qid,\n evidence_top_scores_json,\n result_score\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id, subject_qid, predicate_id, object_qid)\n DO UPDATE SET\n evidence_top_scores_json = excluded.evidence_top_scores_json,\n result_score = excluded.result_score\n `,\n [\n sessionId,\n hit.subjectQid,\n predicateId,\n hit.objectQid,\n JSON.stringify(evidenceTopScores),\n resultScore,\n ],\n );\n}\n\nasync function upsertSearchChunkHit(\n database: Database,\n sessionId: string,\n hit: SearchChunkHitInput,\n): Promise<void> {\n const current = await database.queryOne(\n `\n SELECT property_top_scores_json, evidence_top_scores_json\n FROM search_chunk_hits\n WHERE session_id = ? AND chunk_id = ?\n `,\n [sessionId, hit.chunkId],\n (row) => ({\n evidenceTopScores: parseNumberArray(\n getString(row, \"evidence_top_scores_json\"),\n ),\n propertyTopScores: parseNumberArray(\n getString(row, \"property_top_scores_json\"),\n ),\n }),\n );\n const propertyTopScores = mergeTopScores(\n current?.propertyTopScores ?? [],\n hit.propertyTopScores ?? [],\n );\n const evidenceTopScores = mergeTopScores(\n current?.evidenceTopScores ?? [],\n hit.evidenceTopScores ?? [],\n );\n const propertyScore = aggregateCachedScores(propertyTopScores);\n const evidenceScore = aggregateCachedScores(evidenceTopScores);\n const resultScore = propertyScore + evidenceScore;\n\n await database.run(\n `\n INSERT INTO search_chunk_hits (\n session_id,\n chunk_id,\n property_top_scores_json,\n evidence_top_scores_json,\n property_score,\n evidence_score,\n result_score\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id, chunk_id) DO UPDATE SET\n property_top_scores_json = excluded.property_top_scores_json,\n evidence_top_scores_json = excluded.evidence_top_scores_json,\n property_score = excluded.property_score,\n evidence_score = excluded.evidence_score,\n result_score = excluded.result_score\n `,\n [\n sessionId,\n hit.chunkId,\n JSON.stringify(propertyTopScores),\n JSON.stringify(evidenceTopScores),\n propertyScore,\n evidenceScore,\n resultScore,\n ],\n );\n}\n\nasync function getOrCreatePredicateId(\n database: Database,\n predicate: string,\n): Promise<number> {\n await database.run(\n `\n INSERT OR IGNORE INTO predicate_dictionary(value)\n VALUES (?)\n `,\n [predicate],\n );\n const id = await database.queryOne(\n `\n SELECT id\n FROM predicate_dictionary\n WHERE value = ?\n `,\n [predicate],\n (row) => getNumber(row, \"id\"),\n );\n\n if (id === undefined) {\n throw new Error(\"Failed to create predicate dictionary entry.\");\n }\n\n return id;\n}\n\nasync function listPrunedSearchSessionIds(\n database: Database,\n): Promise<string[]> {\n return await database.queryAll(\n `\n SELECT session_id\n FROM search_sessions\n ORDER BY accessed_at DESC, created_at DESC, session_id\n LIMIT -1 OFFSET ?\n `,\n [SEARCH_SESSION_MAX_COUNT],\n (row) => getString(row, \"session_id\"),\n );\n}\n\nasync function readFileSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return 0;\n }\n\n throw error;\n }\n}\n\nasync function readSearchSessionMetadata(\n database: Database,\n sessionId: string,\n expectedArchiveKey: string | undefined,\n expectedCreatedAt?: number,\n): Promise<{\n readonly createdAt: number;\n readonly lens: string;\n readonly match: string;\n readonly options: {\n readonly chapters: readonly number[] | null;\n readonly types: readonly string[] | null;\n };\n readonly query: string;\n readonly sessionId: string;\n readonly terms: readonly string[];\n readonly expiresAt: number;\n readonly objectCachesPopulated: boolean;\n}> {\n const session = await database.queryOne(\n `\n SELECT\n session_id,\n query,\n options_json,\n terms_json,\n lens,\n match,\n object_caches_populated,\n created_at,\n expires_at\n FROM search_sessions\n WHERE session_id = ?\n ${expectedArchiveKey === undefined ? \"\" : \"AND archive_key = ?\"}\n ${expectedCreatedAt === undefined ? \"\" : \"AND created_at = ?\"}\n `,\n [\n sessionId,\n ...(expectedArchiveKey === undefined ? [] : [expectedArchiveKey]),\n ...(expectedCreatedAt === undefined ? [] : [expectedCreatedAt]),\n ],\n (row) => ({\n createdAt: getNumber(row, \"created_at\"),\n expiresAt: getNumber(row, \"expires_at\"),\n lens: getString(row, \"lens\"),\n match: getString(row, \"match\"),\n objectCachesPopulated: getNumber(row, \"object_caches_populated\") !== 0,\n options: parseSessionOptions(getString(row, \"options_json\")),\n query: getString(row, \"query\"),\n sessionId: getString(row, \"session_id\"),\n terms: parseStringArray(getString(row, \"terms_json\")),\n }),\n );\n\n if (session === undefined || session.expiresAt < Date.now()) {\n throw new Error(\"Search cursor expired. Run the search again.\");\n }\n\n return session;\n}\n\nasync function touchSearchSession(\n database: Database,\n sessionId: string,\n): Promise<void> {\n await database.run(\n `\n UPDATE search_sessions\n SET accessed_at = ?, expires_at = ?\n WHERE session_id = ?\n `,\n [Date.now(), Date.now() + SEARCH_SESSION_TTL_MS, sessionId],\n );\n}\n\nfunction createEntitySearchSessionId(\n input: EntitySearchSessionCacheInput,\n): string {\n return createHash(\"sha256\")\n .update(\n JSON.stringify({\n archiveKey: input.archiveKey,\n entity: true,\n lens: input.lens,\n match: input.match,\n order: input.order,\n rankingVersion: SEARCH_RANKING_VERSION,\n revisionScope: input.revisionScope,\n scope: normalizeSearchSessionScope(input.chapters),\n terms: input.terms,\n types: normalizeSearchSessionTypes(input.types),\n }),\n )\n .digest(\"hex\");\n}\n\nfunction mapEntitySearchObjectRow(\n row: Record<string, SqlBindValue>,\n): ArchiveFindHit {\n const qid = getString(row, \"qid\");\n\n return {\n evidence: {\n nextCursor: null,\n shown: 0,\n sources: [],\n total: 0,\n },\n field: \"title\",\n id: `wikg://entity/${qid}`,\n score: getNumber(row, \"result_score\"),\n snippet: qid,\n title: qid,\n type: \"entity\",\n };\n}\n\nasync function readSearchSessionEntityBucketRows(\n database: Database,\n sessionId: string,\n after: SearchObjectCursorKey | undefined,\n limit: number,\n): Promise<readonly ArchiveFindHit[]> {\n return await database.queryAll(\n `\n SELECT\n qid,\n result_score\n FROM search_entity_hits\n WHERE session_id = ?\n ${\n after === undefined\n ? \"\"\n : `\n AND (\n result_score < ?\n OR (\n result_score = ?\n AND (\n ? < ?\n OR (? = ? AND qid > ?)\n )\n )\n )\n `\n }\n ORDER BY result_score DESC, qid\n LIMIT ?\n `,\n [\n sessionId,\n ...(after === undefined\n ? []\n : [\n after.score,\n after.score,\n getObjectBucketKindOrder(after.kind),\n SEARCH_OBJECT_BUCKET_KIND.entity,\n getObjectBucketKindOrder(after.kind),\n SEARCH_OBJECT_BUCKET_KIND.entity,\n after.id,\n ]),\n limit,\n ],\n mapEntitySearchObjectRow,\n );\n}\n\nasync function readSearchSessionTripleBucketRows(\n database: Database,\n sessionId: string,\n after: SearchObjectCursorKey | undefined,\n limit: number,\n): Promise<readonly ArchiveFindHit[]> {\n return await database.queryAll(\n `\n SELECT\n search_triple_hits.subject_qid AS subject_qid,\n predicate_dictionary.value AS predicate,\n search_triple_hits.object_qid AS object_qid,\n search_triple_hits.result_score AS result_score\n FROM search_triple_hits\n JOIN predicate_dictionary\n ON predicate_dictionary.id = search_triple_hits.predicate_id\n WHERE search_triple_hits.session_id = ?\n ${\n after === undefined\n ? \"\"\n : `\n AND (\n search_triple_hits.result_score < ?\n OR (\n search_triple_hits.result_score = ?\n AND (\n ? < ?\n OR (\n ? = ?\n AND (\n search_triple_hits.subject_qid || char(31) ||\n predicate_dictionary.value || char(31) ||\n search_triple_hits.object_qid\n ) > ?\n )\n )\n )\n )\n `\n }\n ORDER BY search_triple_hits.result_score DESC,\n search_triple_hits.subject_qid,\n predicate_dictionary.value,\n search_triple_hits.object_qid\n LIMIT ?\n `,\n [\n sessionId,\n ...(after === undefined\n ? []\n : [\n after.score,\n after.score,\n getObjectBucketKindOrder(after.kind),\n SEARCH_OBJECT_BUCKET_KIND.triple,\n getObjectBucketKindOrder(after.kind),\n SEARCH_OBJECT_BUCKET_KIND.triple,\n after.id,\n ]),\n limit,\n ],\n (row) => {\n const subjectQid = getString(row, \"subject_qid\");\n const predicate = getString(row, \"predicate\");\n const objectQid = getString(row, \"object_qid\");\n const title = `${subjectQid} ${predicate} ${objectQid}`;\n\n return {\n field: \"content\",\n id: `wikg://triple/${subjectQid}/${encodeURIComponent(predicate)}/${objectQid}`,\n score: getNumber(row, \"result_score\"),\n snippet: title,\n title,\n triple: {\n objectLabel: objectQid,\n predicate,\n subjectLabel: subjectQid,\n },\n type: \"triple\",\n };\n },\n );\n}\n\nfunction compareObjectBucketHits(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n): number {\n return (\n (right.score ?? 0) - (left.score ?? 0) ||\n getObjectBucketKindOrder(getObjectBucketHitKind(left)) -\n getObjectBucketKindOrder(getObjectBucketHitKind(right)) ||\n getObjectBucketHitKey(left).localeCompare(getObjectBucketHitKey(right))\n );\n}\n\nconst SEARCH_OBJECT_BUCKET_KIND = {\n entity: 1,\n triple: 2,\n} as const;\n\nfunction getObjectBucketHitKind(\n hit: ArchiveFindHit,\n): SearchObjectCursorKey[\"kind\"] {\n return hit.type === \"triple\" ? \"triple\" : \"entity\";\n}\n\nfunction getObjectBucketHitKey(hit: ArchiveFindHit): string {\n if (hit.type !== \"triple\") {\n return hit.id.slice(\"wikg://entity/\".length);\n }\n\n return [\n hit.triple?.subjectLabel ?? \"\",\n hit.triple?.predicate ?? \"\",\n hit.triple?.objectLabel ?? \"\",\n ].join(\"\\u001f\");\n}\n\nfunction getObjectBucketKindOrder(kind: SearchObjectCursorKey[\"kind\"]): number {\n return SEARCH_OBJECT_BUCKET_KIND[kind];\n}\n\nfunction isBucketSearchCursor(value: unknown): value is BucketSearchCursor {\n if (typeof value !== \"object\" || value === null || !(\"bucket\" in value)) {\n return false;\n }\n const cursor = value as { readonly bucket: unknown; readonly key?: unknown };\n\n switch (cursor.bucket) {\n case 0:\n return cursor.key === undefined || isChapterTitleCursorKey(cursor.key);\n case 1:\n return cursor.key === undefined || isObjectCursorKey(cursor.key);\n case 2:\n return cursor.key === undefined || isChunkCursorKey(cursor.key);\n case 3:\n return cursor.key === undefined || isTextCursorKey(cursor.key);\n default:\n return false;\n }\n}\n\nfunction isChapterTitleCursorKey(\n value: unknown,\n): value is SearchChapterTitleCursorKey {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as SearchChapterTitleCursorKey).chapterId === \"number\" &&\n typeof (value as SearchChapterTitleCursorKey).score === \"number\"\n );\n}\n\nfunction isObjectCursorKey(value: unknown): value is SearchObjectCursorKey {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as SearchObjectCursorKey).id === \"string\" &&\n ((value as SearchObjectCursorKey).kind === \"entity\" ||\n (value as SearchObjectCursorKey).kind === \"triple\") &&\n typeof (value as SearchObjectCursorKey).score === \"number\"\n );\n}\n\nfunction isChunkCursorKey(value: unknown): value is SearchChunkCursorKey {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as SearchChunkCursorKey).chunkId === \"number\" &&\n typeof (value as SearchChunkCursorKey).score === \"number\"\n );\n}\n\nfunction isTextCursorKey(value: unknown): value is SearchTextCursorKey {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as SearchTextCursorKey).chapterId === \"number\" &&\n typeof (value as SearchTextCursorKey).kind === \"number\" &&\n typeof (value as SearchTextCursorKey).rank === \"number\" &&\n typeof (value as SearchTextCursorKey).sentenceIndex === \"number\"\n );\n}\n\nfunction createSearchSessionId(input: SearchSessionCacheInput): string {\n return createHash(\"sha256\")\n .update(\n JSON.stringify({\n archiveKey: input.archiveKey,\n entity: false,\n lens: input.lens,\n match: input.match,\n order: input.order,\n rankingVersion: SEARCH_RANKING_VERSION,\n revisionScope: input.revisionScope,\n scope: normalizeSearchSessionScope(input.chapters),\n terms: input.terms,\n types: normalizeSearchSessionTypes(input.types),\n }),\n )\n .digest(\"hex\");\n}\n\nfunction normalizeSearchSessionScope(\n chapters: readonly number[] | null,\n): readonly number[] | null {\n return chapters === null ? null : [...new Set(chapters)].sort(compareNumbers);\n}\n\nfunction normalizeSearchSessionTypes(\n types: readonly string[] | null,\n): readonly string[] | null {\n return types === null ? null : [...new Set(types)].sort();\n}\n\nfunction compareNumbers(left: number, right: number): number {\n return left - right;\n}\n\nfunction parseSearchResultItem(value: string): ArchiveFindHit {\n const parsed: unknown = JSON.parse(value);\n\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new Error(\"Invalid cached search result.\");\n }\n\n return parsed as ArchiveFindHit;\n}\n\nfunction parseSessionOptions(value: string): {\n readonly chapters: readonly number[] | null;\n readonly types: readonly string[] | null;\n} {\n const parsed: unknown = JSON.parse(value);\n\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"chapters\" in parsed &&\n \"types\" in parsed &&\n (parsed.chapters === null ||\n (Array.isArray(parsed.chapters) &&\n parsed.chapters.every((chapter) => typeof chapter === \"number\"))) &&\n (parsed.types === null ||\n (Array.isArray(parsed.types) &&\n parsed.types.every((type) => typeof type === \"string\")))\n ) {\n return {\n chapters: parsed.chapters,\n types: parsed.types,\n };\n }\n\n throw new Error(\"Invalid cached search session.\");\n}\n\nfunction parseStringArray(value: string): readonly string[] {\n const parsed: unknown = JSON.parse(value);\n\n if (\n Array.isArray(parsed) &&\n parsed.every((item) => typeof item === \"string\")\n ) {\n return parsed;\n }\n\n throw new Error(\"Invalid cached search session.\");\n}\n\nfunction parseNumberArray(value: string): readonly number[] {\n const parsed: unknown = JSON.parse(value);\n\n if (\n Array.isArray(parsed) &&\n parsed.every((item) => typeof item === \"number\" && Number.isFinite(item))\n ) {\n return parsed.map((item) => Number(item));\n }\n\n return [];\n}\n\nfunction mergeTopScores(\n current: readonly number[],\n incoming: readonly number[],\n): readonly number[] {\n return [...current, ...incoming]\n .filter((score) => Number.isFinite(score))\n .sort((left, right) => right - left)\n .slice(0, SEARCH_TOP_SCORE_COUNT);\n}\n\nfunction aggregateCachedScores(scores: readonly number[]): number {\n return scores\n .slice(0, SEARCH_TOP_SCORE_COUNT)\n .reduce((total, score, index) => total + score / Math.log2(index + 2), 0);\n}\n","import { createHash } from \"crypto\";\n\nexport interface SearchToken {\n readonly encoded: string;\n readonly raw: string;\n readonly tier: 1 | 2 | 3;\n}\n\nexport interface SearchTokenPlan {\n readonly tier1: readonly SearchToken[];\n readonly tier2: readonly SearchToken[];\n readonly tier3: readonly SearchToken[];\n}\n\nconst HAN_RUN_RE = /\\p{Script=Han}+/gu;\nconst LATIN_OR_NUMBER_RE = /[\\p{Script=Latin}\\p{Number}]+/gu;\nconst ZERO_WIDTH_RE = /[\\u200B-\\u200D\\u2060\\uFEFF]/gu;\nconst CONTROL_RE = /[\\p{Cc}\\p{Cf}]/gu;\nconst WHITESPACE_RE = /\\s+/gu;\nconst HAN_RE = /^\\p{Script=Han}+$/u;\nconst ASCII_ALNUM_RE = /^[a-z0-9]+$/u;\n\nconst CHINESE_STOPWORDS = new Set([\n \"的\",\n \"地\",\n \"得\",\n \"了\",\n \"着\",\n \"过\",\n \"和\",\n \"与\",\n \"及\",\n \"或\",\n \"而\",\n \"在\",\n \"是\",\n \"为\",\n \"以\",\n \"于\",\n \"对\",\n \"中\",\n \"上\",\n \"下\",\n \"等\",\n]);\n\nconst ENGLISH_STOPWORDS = new Set([\n \"a\",\n \"an\",\n \"and\",\n \"are\",\n \"as\",\n \"at\",\n \"be\",\n \"been\",\n \"being\",\n \"by\",\n \"can\",\n \"could\",\n \"did\",\n \"do\",\n \"does\",\n \"for\",\n \"from\",\n \"had\",\n \"has\",\n \"have\",\n \"in\",\n \"is\",\n \"may\",\n \"might\",\n \"of\",\n \"on\",\n \"or\",\n \"should\",\n \"the\",\n \"to\",\n \"was\",\n \"were\",\n \"will\",\n \"with\",\n \"would\",\n]);\n\nconst IRREGULAR_LEMMAS = new Map([\n [\"am\", \"be\"],\n [\"are\", \"be\"],\n [\"is\", \"be\"],\n [\"was\", \"be\"],\n [\"were\", \"be\"],\n [\"been\", \"be\"],\n [\"being\", \"be\"],\n [\"has\", \"have\"],\n [\"had\", \"have\"],\n [\"does\", \"do\"],\n [\"did\", \"do\"],\n [\"done\", \"do\"],\n]);\n\nexport function normalizeSearchText(value: string): string {\n return value\n .normalize(\"NFKC\")\n .replace(ZERO_WIDTH_RE, \"\")\n .replace(CONTROL_RE, \" \")\n .replace(WHITESPACE_RE, \" \")\n .trim();\n}\n\nexport function createSearchTokenPlan(value: string): SearchTokenPlan {\n const normalized = normalizeSearchText(value);\n const tier1: SearchToken[] = [];\n const tier2: SearchToken[] = [];\n const tier3: SearchToken[] = [];\n\n for (const raw of createHanTokens(normalized, \"phrase\")) {\n tier1.push(createToken(raw, \"hp\", 1));\n }\n for (const raw of createHanTokens(normalized, \"bigram\")) {\n tier1.push(createToken(raw, \"h2\", 1));\n }\n for (const raw of createHanTokens(normalized, \"trigram\")) {\n tier1.push(createToken(raw, \"h3\", 1));\n }\n for (const raw of createSegmenterTokens(normalized)) {\n tier1.push(createToken(raw, \"hw\", 1));\n }\n for (const raw of createLatinTokens(normalized)) {\n tier1.push(createToken(raw, \"le\", 1));\n const stem = stemEnglish(raw);\n\n if (stem !== raw && !ENGLISH_STOPWORDS.has(stem)) {\n tier2.push(createToken(stem, \"ls\", 2));\n }\n }\n for (const raw of createHanTokens(normalized, \"char\")) {\n tier3.push(createToken(raw, \"hc\", 3));\n }\n\n return {\n tier1: dedupeTokens(tier1),\n tier2: dedupeTokens(tier2),\n tier3: dedupeTokens(tier3),\n };\n}\n\nexport function listSearchPlanTerms(plan: SearchTokenPlan): readonly string[] {\n return [\n ...new Set(\n [...plan.tier1, ...plan.tier2, ...plan.tier3].map((token) => token.raw),\n ),\n ];\n}\n\nexport function hasSearchTokens(plan: SearchTokenPlan): boolean {\n return (\n plan.tier1.length > 0 || plan.tier2.length > 0 || plan.tier3.length > 0\n );\n}\n\nfunction createHanTokens(\n value: string,\n kind: \"bigram\" | \"char\" | \"phrase\" | \"trigram\",\n): readonly string[] {\n const tokens: string[] = [];\n\n for (const match of value.matchAll(HAN_RUN_RE)) {\n const run = [...match[0]];\n\n if (kind === \"phrase\") {\n if (run.length >= 2) {\n tokens.push(run.join(\"\"));\n }\n continue;\n }\n if (kind === \"char\") {\n tokens.push(...run.filter((token) => !CHINESE_STOPWORDS.has(token)));\n continue;\n }\n\n const size = kind === \"bigram\" ? 2 : 3;\n\n if (run.length < size) {\n continue;\n }\n for (let index = 0; index <= run.length - size; index += 1) {\n tokens.push(run.slice(index, index + size).join(\"\"));\n }\n }\n\n return tokens;\n}\n\nfunction createLatinTokens(value: string): readonly string[] {\n return [...value.toLowerCase().matchAll(LATIN_OR_NUMBER_RE)]\n .map((match) => match[0])\n .filter((token) => !ENGLISH_STOPWORDS.has(token));\n}\n\nfunction createSegmenterTokens(value: string): readonly string[] {\n if (typeof Intl.Segmenter !== \"function\") {\n return [];\n }\n\n const segmenter = new Intl.Segmenter(\"zh-CN\", { granularity: \"word\" });\n const tokens: string[] = [];\n\n for (const segment of segmenter.segment(value)) {\n const token = normalizeSearchText(segment.segment).toLowerCase();\n\n if (\n segment.isWordLike === true &&\n [...token].length >= 2 &&\n HAN_RE.test(token)\n ) {\n tokens.push(token);\n }\n }\n\n return tokens;\n}\n\nfunction createToken(\n raw: string,\n prefix: string,\n tier: 1 | 2 | 3,\n): SearchToken {\n const normalized = normalizeSearchText(raw).toLowerCase();\n const digest = createHash(\"sha256\")\n .update(normalized)\n .digest(\"hex\")\n .slice(0, 20);\n\n return {\n encoded: `${prefix}${digest}`,\n raw: normalized,\n tier,\n };\n}\n\nfunction dedupeTokens(tokens: readonly SearchToken[]): readonly SearchToken[] {\n const seen = new Set<string>();\n const result: SearchToken[] = [];\n\n for (const token of tokens) {\n if (seen.has(token.encoded)) {\n continue;\n }\n\n seen.add(token.encoded);\n result.push(token);\n }\n\n return result;\n}\n\nfunction stemEnglish(token: string): string {\n const lemma = IRREGULAR_LEMMAS.get(token);\n\n if (lemma !== undefined) {\n return lemma;\n }\n if (!ASCII_ALNUM_RE.test(token) || /\\d/u.test(token) || token.length < 4) {\n return token;\n }\n\n return porterStem(token);\n}\n\nfunction porterStem(value: string): string {\n // A deliberately small Porter-like stemmer: enough for weak recall tokens,\n // not intended to replace exact Latin tokens or final ranking.\n let token = value;\n\n if (token.endsWith(\"ies\") && token.length > 4) {\n token = `${token.slice(0, -3)}y`;\n } else if (token.endsWith(\"ing\") && token.length > 5) {\n token = token.slice(0, -3);\n } else if (token.endsWith(\"ed\") && token.length > 4) {\n token = token.slice(0, -2);\n } else if (token.endsWith(\"es\") && token.length > 4) {\n token = token.slice(0, -2);\n } else if (token.endsWith(\"s\") && token.length > 4) {\n token = token.slice(0, -1);\n }\n\n if (\n token.length > 3 &&\n token.at(-1) === token.at(-2) &&\n /[bcdfghjklmnpqrstvwxyz]/u.test(token.at(-1)!)\n ) {\n token = token.slice(0, -1);\n }\n\n return token;\n}\n","import { createHash } from \"crypto\";\n\nimport {\n getNumber,\n type Database,\n type SqlBindValue,\n} from \"../../document/database.js\";\nimport type { Document, ReadonlyDocument } from \"../../document/index.js\";\n\nimport {\n createSearchTokenPlan,\n hasSearchTokens,\n listSearchPlanTerms,\n normalizeSearchText,\n type SearchTokenPlan,\n} from \"./search-tokenizer.js\";\nimport type {\n ArchiveFindMatch,\n ArchiveFindObjectType,\n} from \"../query/archive-view.js\";\n\nexport const TEXT_SENTENCE_KIND = {\n source: 1,\n summary: 2,\n} as const;\n\nexport type TextSentenceKind =\n (typeof TEXT_SENTENCE_KIND)[keyof typeof TEXT_SENTENCE_KIND];\n\nexport const SEARCH_OBJECT_PROPERTY_OWNER_KIND = {\n chapter: 1,\n chunk: 2,\n entity: 3,\n} as const;\n\nexport type SearchObjectPropertyOwnerKind =\n (typeof SEARCH_OBJECT_PROPERTY_OWNER_KIND)[keyof typeof SEARCH_OBJECT_PROPERTY_OWNER_KIND];\n\nexport const SEARCH_OBJECT_PROPERTY_KIND = {\n title: 1,\n label: 1,\n content: 2,\n surface: 1,\n} as const;\n\nexport type SearchObjectPropertyKind =\n (typeof SEARCH_OBJECT_PROPERTY_KIND)[keyof typeof SEARCH_OBJECT_PROPERTY_KIND];\n\nexport interface TextSentenceRecordInput {\n readonly chapterId: number;\n readonly kind: TextSentenceKind;\n readonly sentenceIndex: number;\n readonly text: string;\n readonly wordsCount: number;\n}\n\nexport interface SearchObjectPropertyRecordInput {\n readonly chapterId?: number;\n readonly ownerId: string;\n readonly ownerKind: SearchObjectPropertyOwnerKind;\n readonly propertyKind: SearchObjectPropertyKind;\n readonly text: string;\n}\n\nexport interface SearchIndexInput {\n readonly objectProperties: readonly SearchObjectPropertyRecordInput[];\n readonly textSentences: readonly TextSentenceRecordInput[];\n}\n\nexport type SearchIndexProgressPhase =\n | \"checking\"\n | \"clearing\"\n | \"collecting\"\n | \"finalizing\"\n | \"indexing-objects\"\n | \"indexing-text\";\n\nexport interface SearchIndexProgressEvent {\n readonly done?: number;\n readonly phase: SearchIndexProgressPhase;\n readonly total?: number;\n readonly unit?: \"chapter\" | \"object\" | \"sentence\";\n}\n\nexport type SearchIndexProgressReporter = (\n event: SearchIndexProgressEvent,\n) => void | Promise<void>;\n\nexport type SearchIndexStatus = \"current\" | \"dirty\" | \"missing\";\n\nexport interface SearchIndexTextHit {\n readonly chapterId: number;\n readonly kind: TextSentenceKind;\n readonly rank: number;\n readonly score: number;\n readonly sentenceIndex: number;\n readonly wordsCount: number;\n}\n\nexport interface SearchIndexObjectHit {\n readonly chapterId?: number;\n readonly ownerId: string;\n readonly ownerKind: SearchObjectPropertyOwnerKind;\n readonly propertyKind: SearchObjectPropertyKind;\n readonly score: number;\n}\n\nexport interface SearchIndexQueryResult {\n readonly objectHits: readonly SearchIndexObjectHit[];\n readonly terms: readonly string[];\n readonly textHits: readonly SearchIndexTextHit[];\n}\n\nconst SEARCH_INDEX_VERSION = \"3\";\nexport const SEARCH_INDEX_FTS_HIT_LIMIT = 32_000;\nconst FTS5_RANK_SCORE_SCALE = 1_000_000;\nconst TIER_WEIGHTS = [1, 0.45, 0.08] as const;\n\nexport interface ArchiveIndexSettings {\n readonly ftsEmbedded: boolean;\n}\n\nexport async function readArchiveIndexSettings(\n document: ReadonlyDocument,\n): Promise<ArchiveIndexSettings> {\n return await document.readDatabase(async (database) => {\n const row = await database.queryOne(\n `\n SELECT fts_embedded\n FROM archive_index_settings\n WHERE id = 1\n `,\n undefined,\n (value) => ({\n ftsEmbedded: getNumber(value, \"fts_embedded\") !== 0,\n }),\n );\n\n return row ?? { ftsEmbedded: false };\n });\n}\n\nexport async function setFtsIndexEmbedded(\n document: Document,\n embedded: boolean,\n): Promise<void> {\n await document.readDatabase(async (database) => {\n await database.run(\n `\n INSERT INTO archive_index_settings(id, fts_embedded)\n VALUES (1, ?)\n ON CONFLICT(id)\n DO UPDATE SET fts_embedded = excluded.fts_embedded\n `,\n [embedded ? 1 : 0],\n );\n });\n}\n\nexport async function isSearchIndexCurrent(\n document: ReadonlyDocument,\n input?: SearchIndexInput,\n): Promise<boolean> {\n return (await readSearchIndexStatus(document, input)) === \"current\";\n}\n\nexport async function readSearchIndexStatus(\n document: ReadonlyDocument,\n input?: SearchIndexInput,\n): Promise<SearchIndexStatus> {\n const fingerprint =\n input === undefined ? undefined : createSearchIndexFingerprint(input);\n\n try {\n return await document.readSearchIndexDatabase(async (database) => {\n const indexedFingerprint =\n await readSearchIndexFingerprintFromDatabase(database);\n\n if (indexedFingerprint === undefined) {\n return \"dirty\";\n }\n\n return fingerprint === undefined || indexedFingerprint === fingerprint\n ? \"current\"\n : \"dirty\";\n });\n } catch (error) {\n if (isMissingSearchIndexError(error)) {\n return \"missing\";\n }\n\n throw error;\n }\n}\n\nexport async function ensureSearchIndex(\n document: Document,\n input: SearchIndexInput,\n progress?: SearchIndexProgressReporter,\n): Promise<void> {\n const chaptersRevision = await document.serials.getChaptersRevision();\n\n await document.writeSearchIndexDatabase(async (database) => {\n const fingerprint = createSearchIndexFingerprint(input);\n const indexedFingerprint =\n await readSearchIndexFingerprintFromDatabase(database);\n\n if (indexedFingerprint === fingerprint) {\n return;\n }\n\n await database.transaction(async () => {\n await progress?.({ phase: \"clearing\" });\n await database.run(\"DELETE FROM text_sentence_fts\");\n await database.run(\"DELETE FROM search_object_properties_fts\");\n await database.run(\"DELETE FROM search_object_properties_records\");\n await database.run(\"DELETE FROM search_index_state\");\n\n let textDone = 0;\n for (const record of input.textSentences) {\n const plan = createSearchTokenPlan(record.text);\n const rowId = await insertTextSentenceRecord(database, record);\n\n await insertFtsRecord(database, \"text_sentence_fts\", rowId, plan);\n textDone += 1;\n await progress?.({\n done: textDone,\n phase: \"indexing-text\",\n total: input.textSentences.length,\n unit: \"sentence\",\n });\n }\n\n let objectDone = 0;\n for (const record of input.objectProperties) {\n const plan = createSearchTokenPlan(record.text);\n const rowId = await insertSearchObjectPropertyRecord(database, record);\n\n await insertFtsRecord(\n database,\n \"search_object_properties_fts\",\n rowId,\n plan,\n );\n objectDone += 1;\n await progress?.({\n done: objectDone,\n phase: \"indexing-objects\",\n total: input.objectProperties.length,\n unit: \"object\",\n });\n }\n\n await progress?.({ phase: \"finalizing\" });\n await database.run(\n `\n INSERT INTO search_index_state(key, value)\n VALUES ('version', ?)\n `,\n [SEARCH_INDEX_VERSION],\n );\n await database.run(\n `\n INSERT INTO search_index_state(key, value)\n VALUES ('fingerprint', ?)\n `,\n [fingerprint],\n );\n await database.run(\n `\n INSERT INTO search_index_state(key, value)\n VALUES ('chaptersRevision', ?)\n `,\n [String(chaptersRevision)],\n );\n });\n });\n}\n\nexport async function querySearchIndex(\n document: ReadonlyDocument,\n query: string,\n options: {\n readonly chapters?: readonly number[];\n readonly match?: ArchiveFindMatch;\n readonly objectHitLimit?: number;\n readonly textAfter?: {\n readonly chapterId: number;\n readonly kind: TextSentenceKind;\n readonly rank: number;\n readonly sentenceIndex: number;\n };\n readonly textHitLimit?: number;\n readonly types?: readonly ArchiveFindObjectType[] | null;\n } = {},\n): Promise<SearchIndexQueryResult | undefined> {\n const plan = createSearchTokenPlan(query);\n\n if (!hasSearchTokens(plan)) {\n return undefined;\n }\n\n const terms = listSearchPlanTerms(plan);\n\n return await document.readSearchIndexDatabase(async (database) => {\n const tierQueries = createTierQueries(query, plan, options.match ?? \"any\");\n const objectHitLimit = options.objectHitLimit ?? SEARCH_INDEX_FTS_HIT_LIMIT;\n const textHitLimit = options.textHitLimit ?? SEARCH_INDEX_FTS_HIT_LIMIT;\n const queriesObjects = shouldQueryObjects(options.types);\n const queriesText = createTextKindFilter(options.types).length > 0;\n const objectHitsByKey = new Map<string, SearchIndexObjectHit>();\n const textHitsByKey = new Map<string, SearchIndexTextHit>();\n\n for (const tierQuery of tierQueries) {\n if (tierQuery.matchExpression === \"\") {\n continue;\n }\n\n const objectHitRemaining = Math.max(\n 0,\n objectHitLimit - objectHitsByKey.size,\n );\n const textHitRemaining = Math.max(0, textHitLimit - textHitsByKey.size);\n\n if (\n (!queriesObjects || objectHitRemaining <= 0) &&\n (!queriesText || textHitRemaining <= 0)\n ) {\n break;\n }\n\n const [objectRows, textRows] = await Promise.all([\n queryObjectRows(database, tierQuery.matchExpression, {\n ...options,\n objectHitLimit: objectHitRemaining,\n }),\n queryTextRows(database, tierQuery.matchExpression, {\n ...options,\n textHitLimit: textHitRemaining,\n }),\n ]);\n\n for (const hit of objectRows) {\n const key = createObjectHitKey(hit);\n\n if (!objectHitsByKey.has(key)) {\n objectHitsByKey.set(key, hit);\n }\n }\n for (const hit of textRows) {\n const key = createTextHitKey(hit);\n\n if (!textHitsByKey.has(key)) {\n textHitsByKey.set(key, hit);\n }\n }\n if (\n (!queriesObjects || objectHitsByKey.size >= objectHitLimit) &&\n (!queriesText || textHitsByKey.size >= textHitLimit)\n ) {\n break;\n }\n }\n\n return {\n objectHits: [...objectHitsByKey.values()],\n terms,\n textHits: [...textHitsByKey.values()],\n };\n });\n}\n\nasync function queryObjectRows(\n database: Database,\n matchExpression: string,\n options: {\n readonly chapters?: readonly number[];\n readonly objectHitLimit?: number;\n readonly types?: readonly ArchiveFindObjectType[] | null;\n },\n): Promise<readonly SearchIndexObjectHit[]> {\n if (!shouldQueryObjects(options.types) || options.objectHitLimit === 0) {\n return [];\n }\n\n return await database.queryAll(\n `\n SELECT\n r.owner_kind AS owner_kind,\n r.owner_id AS owner_id,\n r.property_kind AS property_kind,\n r.chapter_id AS chapter_id,\n bm25(search_object_properties_fts, ?, ?, ?) AS rank\n FROM search_object_properties_fts\n JOIN search_object_properties_records AS r\n ON r.id = search_object_properties_fts.rowid\n WHERE search_object_properties_fts MATCH ?\n ${createChapterSql(options.chapters)}\n ORDER BY rank ASC, r.chapter_id, r.owner_kind, r.owner_id, r.property_kind\n ${createLimitSql(options.objectHitLimit)}\n `,\n [\n ...TIER_WEIGHTS,\n matchExpression,\n ...createChapterParams(options.chapters),\n ...createLimitParams(options.objectHitLimit),\n ],\n (row) => ({\n ownerId: String(row.owner_id),\n ownerKind: getNumber(row, \"owner_kind\") as SearchObjectPropertyOwnerKind,\n propertyKind: getNumber(row, \"property_kind\") as SearchObjectPropertyKind,\n score: rankToScore(getNumber(row, \"rank\")),\n ...(row.chapter_id === null\n ? {}\n : { chapterId: getNumber(row, \"chapter_id\") }),\n }),\n );\n}\n\nasync function queryTextRows(\n database: Database,\n matchExpression: string,\n options: {\n readonly chapters?: readonly number[];\n readonly textAfter?: {\n readonly chapterId: number;\n readonly kind: TextSentenceKind;\n readonly rank: number;\n readonly sentenceIndex: number;\n };\n readonly textHitLimit?: number;\n readonly types?: readonly ArchiveFindObjectType[] | null;\n },\n): Promise<readonly SearchIndexTextHit[]> {\n const kinds = createTextKindFilter(options.types);\n\n if (kinds.length === 0) {\n return [];\n }\n if (options.textHitLimit === 0) {\n return [];\n }\n\n const after = options.textAfter;\n\n return await database.queryAll(\n `\n SELECT\n kind,\n chapter_id,\n sentence_index,\n words_count,\n rank\n FROM (\n SELECT\n r.kind AS kind,\n r.chapter_id AS chapter_id,\n r.sentence_index AS sentence_index,\n r.words_count AS words_count,\n bm25(text_sentence_fts, ?, ?, ?) AS rank\n FROM text_sentence_fts\n JOIN text_sentence_records AS r\n ON r.id = text_sentence_fts.rowid\n WHERE text_sentence_fts MATCH ?\n AND r.kind IN (${kinds.map(() => \"?\").join(\", \")})\n ${createChapterSql(options.chapters)}\n )\n ${\n after === undefined\n ? \"\"\n : `\n WHERE (\n rank > ?\n OR (rank = ? AND chapter_id > ?)\n OR (rank = ? AND chapter_id = ? AND sentence_index > ?)\n OR (\n rank = ?\n AND chapter_id = ?\n AND sentence_index = ?\n AND kind > ?\n )\n )\n `\n }\n ORDER BY rank ASC, chapter_id, sentence_index, kind\n ${createLimitSql(options.textHitLimit)}\n `,\n [\n ...TIER_WEIGHTS,\n matchExpression,\n ...kinds,\n ...createChapterParams(options.chapters),\n ...(after === undefined\n ? []\n : [\n after.rank,\n after.rank,\n after.chapterId,\n after.rank,\n after.chapterId,\n after.sentenceIndex,\n after.rank,\n after.chapterId,\n after.sentenceIndex,\n after.kind,\n ]),\n ...createLimitParams(options.textHitLimit),\n ],\n (row) => {\n const rank = getNumber(row, \"rank\");\n\n return {\n chapterId: getNumber(row, \"chapter_id\"),\n kind: getNumber(row, \"kind\") as TextSentenceKind,\n rank,\n score: rankToScore(rank),\n sentenceIndex: getNumber(row, \"sentence_index\"),\n wordsCount: getNumber(row, \"words_count\"),\n };\n },\n );\n}\n\nfunction createTierQueries(\n query: string,\n plan: SearchTokenPlan,\n match: ArchiveFindMatch,\n): readonly {\n readonly matchExpression: string;\n}[] {\n if (match === \"all\") {\n return createAllMatchTierQueries(query);\n }\n\n return [\n {\n matchExpression: createMatchExpression([\n { column: \"tier1\", tokens: plan.tier1.map((token) => token.encoded) },\n ]),\n },\n {\n matchExpression: createMatchExpression([\n { column: \"tier1\", tokens: plan.tier1.map((token) => token.encoded) },\n { column: \"tier2\", tokens: plan.tier2.map((token) => token.encoded) },\n ]),\n },\n {\n matchExpression: createMatchExpression([\n { column: \"tier1\", tokens: plan.tier1.map((token) => token.encoded) },\n { column: \"tier2\", tokens: plan.tier2.map((token) => token.encoded) },\n { column: \"tier3\", tokens: plan.tier3.map((token) => token.encoded) },\n ]),\n },\n ];\n}\n\nfunction createAllMatchTierQueries(query: string): readonly {\n readonly matchExpression: string;\n}[] {\n const termPlans = normalizeSearchText(query)\n .split(/\\s+/u)\n .map((term) => createSearchTokenPlan(term))\n .filter(hasSearchTokens);\n\n const createExpression = (\n selectTokens: (plan: SearchTokenPlan) => readonly string[],\n ): string =>\n termPlans\n .map((termPlan) => {\n const tokens = [...new Set(selectTokens(termPlan))];\n\n return tokens.length === 0 ? \"\" : `(${tokens.join(\" OR \")})`;\n })\n .filter((term) => term !== \"\")\n .join(\" AND \");\n\n return [\n {\n matchExpression: createExpression((termPlan) =>\n termPlan.tier1.map((token) => `tier1:${escapeFtsToken(token.encoded)}`),\n ),\n },\n {\n matchExpression: createExpression((termPlan) => [\n ...termPlan.tier1.map(\n (token) => `tier1:${escapeFtsToken(token.encoded)}`,\n ),\n ...termPlan.tier2.map(\n (token) => `tier2:${escapeFtsToken(token.encoded)}`,\n ),\n ]),\n },\n {\n matchExpression: createExpression((termPlan) => [\n ...termPlan.tier1.map(\n (token) => `tier1:${escapeFtsToken(token.encoded)}`,\n ),\n ...termPlan.tier2.map(\n (token) => `tier2:${escapeFtsToken(token.encoded)}`,\n ),\n ...termPlan.tier3.map(\n (token) => `tier3:${escapeFtsToken(token.encoded)}`,\n ),\n ]),\n },\n ];\n}\n\nfunction createMatchExpression(\n groups: readonly {\n readonly column: string;\n readonly tokens: readonly string[];\n }[],\n): string {\n return groups\n .map((group) => ({\n ...group,\n tokens: [...new Set(group.tokens)],\n }))\n .filter((group) => group.tokens.length > 0)\n .map(\n (group) =>\n `${group.column}:(${group.tokens.map(escapeFtsToken).join(\" OR \")})`,\n )\n .join(\" OR \");\n}\n\nfunction escapeFtsToken(token: string): string {\n return `\"${token.replaceAll('\"', '\"\"')}\"`;\n}\n\nasync function insertTextSentenceRecord(\n database: Database,\n record: TextSentenceRecordInput,\n): Promise<number> {\n const rowId = await database.queryOne(\n `\n SELECT id\n FROM text_sentence_records\n WHERE kind = ? AND chapter_id = ? AND sentence_index = ?\n `,\n [record.kind, record.chapterId, record.sentenceIndex],\n (row) => getNumber(row, \"id\"),\n );\n\n if (rowId !== undefined) {\n return rowId;\n }\n\n await database.run(\n `\n INSERT INTO text_sentence_records (\n kind, chapter_id, sentence_index, words_count, byte_offset, byte_length\n )\n VALUES (?, ?, ?, ?, 0, 0)\n `,\n [record.kind, record.chapterId, record.sentenceIndex, record.wordsCount],\n );\n\n return await database.getLastInsertRowId();\n}\n\nasync function insertSearchObjectPropertyRecord(\n database: Database,\n record: SearchObjectPropertyRecordInput,\n): Promise<number> {\n await database.run(\n `\n INSERT INTO search_object_properties_records (\n owner_kind, owner_id, property_kind, chapter_id\n )\n VALUES (?, ?, ?, ?)\n `,\n [\n record.ownerKind,\n record.ownerId,\n record.propertyKind,\n record.chapterId ?? null,\n ],\n );\n\n return await database.getLastInsertRowId();\n}\n\nasync function insertFtsRecord(\n database: Database,\n table: \"search_object_properties_fts\" | \"text_sentence_fts\",\n rowId: number,\n plan: SearchTokenPlan,\n): Promise<void> {\n await database.run(\n `\n INSERT INTO ${table}(rowid, tier1, tier2, tier3)\n VALUES (?, ?, ?, ?)\n `,\n [\n rowId,\n serializeTokens(plan.tier1),\n serializeTokens(plan.tier2),\n serializeTokens(plan.tier3),\n ],\n );\n}\n\nexport function createSearchIndexFingerprint(input: SearchIndexInput): string {\n const hash = createHash(\"sha256\");\n\n for (const record of input.textSentences) {\n hash.update(\"text\");\n hash.update(\"\\0\");\n hash.update(String(record.kind));\n hash.update(\"\\0\");\n hash.update(String(record.chapterId));\n hash.update(\"\\0\");\n hash.update(String(record.sentenceIndex));\n hash.update(\"\\0\");\n hash.update(record.text);\n hash.update(\"\\0\");\n }\n\n for (const record of input.objectProperties) {\n hash.update(\"object-property\");\n hash.update(\"\\0\");\n hash.update(String(record.ownerKind));\n hash.update(\"\\0\");\n hash.update(record.ownerId);\n hash.update(\"\\0\");\n hash.update(String(record.propertyKind));\n hash.update(\"\\0\");\n hash.update(String(record.chapterId ?? \"\"));\n hash.update(\"\\0\");\n hash.update(record.text);\n hash.update(\"\\0\");\n }\n\n return hash.digest(\"hex\");\n}\n\nexport async function readSearchIndexFingerprintFromDatabase(\n database: Database,\n): Promise<string | undefined> {\n return await database.queryOne(\n `\n SELECT value\n FROM search_index_state\n WHERE key = 'fingerprint'\n `,\n undefined,\n (row) => String(row.value),\n );\n}\n\nfunction isMissingSearchIndexError(error: unknown): boolean {\n const code =\n typeof error === \"object\" && error !== null && \"code\" in error\n ? error.code\n : undefined;\n\n return (\n code === \"SQLITE_CANTOPEN\" ||\n (error instanceof Error &&\n (error.message.includes(\"Archive SQLite entry is missing: fts.db\") ||\n error.message.includes(\"no such table: search_index_state\")))\n );\n}\n\nfunction serializeTokens(\n tokens: readonly {\n readonly encoded: string;\n }[],\n): string {\n return tokens.map((token) => token.encoded).join(\" \");\n}\n\nfunction createChapterSql(chapters: readonly number[] | undefined): string {\n return chapters === undefined || chapters.length === 0\n ? \"\"\n : `AND r.chapter_id IN (${chapters.map(() => \"?\").join(\", \")})`;\n}\n\nfunction createChapterParams(\n chapters: readonly number[] | undefined,\n): readonly SqlBindValue[] {\n return chapters === undefined ? [] : [...chapters];\n}\n\nfunction createLimitSql(limit: number | undefined): string {\n return limit === undefined ? \"\" : \"LIMIT ?\";\n}\n\nfunction createLimitParams(limit: number | undefined): readonly SqlBindValue[] {\n return limit === undefined ? [] : [limit];\n}\n\nfunction shouldQueryObjects(\n types: readonly ArchiveFindObjectType[] | null | undefined,\n): boolean {\n return (\n types === undefined ||\n types === null ||\n types.includes(\"chapter\") ||\n types.includes(\"chapter-title\") ||\n types.includes(\"node\") ||\n types.includes(\"entity\")\n );\n}\n\nfunction createTextKindFilter(\n types: readonly ArchiveFindObjectType[] | null | undefined,\n): readonly TextSentenceKind[] {\n if (types === undefined || types === null) {\n return [TEXT_SENTENCE_KIND.summary, TEXT_SENTENCE_KIND.source];\n }\n\n const kinds: TextSentenceKind[] = [];\n\n if (types.includes(\"summary\")) {\n kinds.push(TEXT_SENTENCE_KIND.summary);\n }\n if (types.includes(\"source\")) {\n kinds.push(TEXT_SENTENCE_KIND.source);\n }\n\n return kinds;\n}\n\nfunction rankToScore(rank: number): number {\n const relevance = Math.max(0, -rank) * FTS5_RANK_SCORE_SCALE;\n\n return relevance / (1 + relevance);\n}\n\nfunction createObjectHitKey(hit: SearchIndexObjectHit): string {\n return [\n hit.ownerKind,\n hit.ownerId,\n hit.propertyKind,\n hit.chapterId ?? \"\",\n ].join(\":\");\n}\n\nfunction createTextHitKey(hit: SearchIndexTextHit): string {\n return [hit.kind, hit.chapterId, hit.sentenceIndex].join(\":\");\n}\n","import { isAbsolute, relative, resolve } from \"path\";\nimport { homedir } from \"os\";\n\nexport const WIKI_GRAPH_URI_PREFIX = \"wikg://\";\nexport const WIKI_GRAPH_JOB_URI_PREFIX = \"wikg://local/job\";\nexport const WIKI_GRAPH_ARCHIVE_EXTENSION = \".wikg\";\n\nexport interface LocatedWikiGraphUri {\n readonly archivePath?: string;\n readonly objectUri?: string;\n}\n\nexport function isWikiGraphUri(value: string | undefined): value is string {\n return isWikiGraphUriPrefix(value);\n}\n\nexport function isWikiGraphJobUri(value: string | undefined): value is string {\n return isWikiGraphLocalJobUri(value);\n}\n\nexport function parseLocatedWikiGraphUri(uri: string): LocatedWikiGraphUri {\n const prefix = getWikiGraphUriPrefix(uri);\n\n if (prefix === undefined) {\n throw new Error(formatWikiGraphUriExpectedError(uri));\n }\n\n const body = uri.slice(prefix.length);\n const split = body.split(\"#\", 2);\n const path = split[0] ?? \"\";\n const hash = split[1] ?? \"\";\n const parts = path.split(\"/\");\n const archiveIndex = parts.findIndex((part) =>\n part.endsWith(WIKI_GRAPH_ARCHIVE_EXTENSION),\n );\n\n if (archiveIndex < 0) {\n return { objectUri: uri };\n }\n\n const archivePath = parts.slice(0, archiveIndex + 1).join(\"/\");\n const objectPath = parts.slice(archiveIndex + 1).join(\"/\");\n\n if (archivePath === \"\") {\n throw new Error(`Invalid Wiki Graph archive URI: ${uri}`);\n }\n\n return {\n archivePath: resolveArchivePath(archivePath),\n ...(objectPath === \"\"\n ? {}\n : {\n objectUri: formatWikiGraphObjectUri(\n objectPath,\n hash === \"\" ? undefined : hash,\n ),\n }),\n };\n}\n\nfunction resolveArchivePath(archivePath: string): string {\n if (archivePath.startsWith(\"~/\")) {\n return resolve(homedir(), archivePath.slice(2));\n }\n\n return resolve(archivePath);\n}\n\nexport function formatLocatedWikiGraphUri(\n archivePath: string,\n objectUri?: string,\n): string {\n const uriArchivePath = archivePath.replace(/\\\\/gu, \"/\");\n\n if (objectUri === undefined || objectUri === WIKI_GRAPH_URI_PREFIX) {\n return `${WIKI_GRAPH_URI_PREFIX}${uriArchivePath}`;\n }\n\n return `${WIKI_GRAPH_URI_PREFIX}${uriArchivePath}/${stripWikiGraphUriPrefix(\n objectUri,\n )}`;\n}\n\nexport function formatWikiGraphCommandUri(\n archivePath: string,\n objectUri?: string,\n cwd = process.cwd(),\n): string {\n return formatLocatedWikiGraphUri(\n formatCommandArchivePath(archivePath, cwd),\n objectUri,\n );\n}\n\nfunction formatCommandArchivePath(archivePath: string, cwd: string): string {\n const resolvedCwd = resolve(cwd);\n const resolvedArchivePath = isAbsolute(archivePath)\n ? resolve(archivePath)\n : resolve(resolvedCwd, archivePath);\n const relativeArchivePath = relative(resolvedCwd, resolvedArchivePath);\n\n if (\n relativeArchivePath !== \"\" &&\n !relativeArchivePath.startsWith(\"..\") &&\n !isAbsolute(relativeArchivePath)\n ) {\n return relativeArchivePath;\n }\n\n return resolvedArchivePath;\n}\n\nexport function formatLocatedChapterUri(\n archivePath: string,\n chapterId: number,\n): string {\n return formatLocatedWikiGraphUri(\n archivePath,\n formatWikiGraphObjectUri(`chapter/${chapterId}`),\n );\n}\n\nexport function formatLocatedChapterResourceUri(\n archivePath: string,\n chapterId: number,\n resource: \"source\" | \"summary\" | \"title\",\n): string {\n return formatLocatedWikiGraphUri(\n archivePath,\n formatWikiGraphObjectUri(`chapter/${chapterId}/${resource}`),\n );\n}\n\nexport function formatLocatedChapterSourceCollectionUri(\n archivePath: string,\n chapterId: number,\n): string {\n return formatLocatedWikiGraphUri(\n archivePath,\n formatWikiGraphObjectUri(`chapter/${chapterId}/source`),\n );\n}\n\nexport function formatWikiGraphObjectUri(path: string, hash?: string): string {\n const normalizedPath = path.replace(/^\\/+/u, \"\").replace(/\\/+$/u, \"\");\n\n return `${WIKI_GRAPH_URI_PREFIX}${normalizedPath}${\n hash === undefined ? \"\" : `#${hash}`\n }`;\n}\n\nexport function requireArchiveUri(uri: string): string {\n const parsed = parseLocatedWikiGraphUri(uri);\n\n if (parsed.archivePath === undefined || parsed.objectUri !== undefined) {\n throw new Error(\n `${formatWikiGraphUriExpectedError(uri)} Expected a .wikg archive locator.`,\n );\n }\n\n return parsed.archivePath;\n}\n\nexport function requireLocatedObjectUri(uri: string): {\n readonly archivePath: string;\n readonly objectUri: string;\n} {\n const parsed = parseLocatedWikiGraphUri(uri);\n\n if (parsed.archivePath === undefined || parsed.objectUri === undefined) {\n throw new Error(\n `${formatWikiGraphUriExpectedError(uri)} Expected an object URI with a .wikg archive locator.`,\n );\n }\n\n return {\n archivePath: parsed.archivePath,\n objectUri: parsed.objectUri,\n };\n}\n\nexport function requireLocatedObjectOrArchiveUri(uri: string): {\n readonly archivePath: string;\n readonly objectUri?: string;\n} {\n const parsed = parseLocatedWikiGraphUri(uri);\n\n if (parsed.archivePath === undefined) {\n throw new Error(formatWikiGraphUriExpectedError(uri));\n }\n\n return {\n archivePath: parsed.archivePath,\n ...(parsed.objectUri === undefined ? {} : { objectUri: parsed.objectUri }),\n };\n}\n\nexport function formatWikiGraphUriExpectedError(value: string): string {\n const example =\n value.endsWith(WIKI_GRAPH_ARCHIVE_EXTENSION) && value.startsWith(\"/\")\n ? `${WIKI_GRAPH_URI_PREFIX}${value}`\n : \"wikg:///absolute/path/book.wikg\";\n\n return [\n `Expected a Wiki Graph URI with a .wikg archive locator: ${value}`,\n `Example: ${example}`,\n \"See: wg help uri\",\n ].join(\"\\n\");\n}\n\nfunction stripWikiGraphUriPrefix(uri: string): string {\n const prefix = getWikiGraphUriPrefix(uri);\n\n if (prefix === undefined) {\n throw new Error(`Expected a Wiki Graph object URI: ${uri}`);\n }\n\n return uri.slice(prefix.length).replace(/^\\/+/u, \"\");\n}\n\nfunction getWikiGraphUriPrefix(uri: string): string | undefined {\n if (uri.startsWith(WIKI_GRAPH_URI_PREFIX)) {\n return WIKI_GRAPH_URI_PREFIX;\n }\n\n return undefined;\n}\n\nfunction isWikiGraphUriPrefix(value: string | undefined): value is string {\n return value?.startsWith(WIKI_GRAPH_URI_PREFIX) === true;\n}\n\nfunction isWikiGraphLocalJobUri(value: string | undefined): boolean {\n return (\n value === WIKI_GRAPH_JOB_URI_PREFIX ||\n value?.startsWith(`${WIKI_GRAPH_JOB_URI_PREFIX}/`) === true\n );\n}\n","import type {\n ChunkRecord,\n MentionLinkRecord,\n MentionRecord,\n ReadonlySerialTextStream,\n ReadonlyDocument,\n ReadingEdgeRecord,\n SentenceId,\n} from \"../../document/index.js\";\nimport type { BookMeta } from \"../../source/index.js\";\nimport type { Document } from \"../../document/index.js\";\nimport {\n WikipageResolver,\n type WikipageResolverOptions,\n} from \"../../wikipage/index.js\";\nimport type { QidResolution, WikipageSitelink } from \"../../wikipage/index.js\";\n\nimport {\n getGraphNode,\n listGraphNeighbors,\n type GraphNeighbor,\n type GraphNode,\n} from \"../../facade/graph.js\";\nimport {\n getChapterTree,\n listChapters,\n type ChapterEntry,\n type ChapterTree,\n} from \"../../facade/chapter.js\";\nimport {\n createLexicalQuery,\n listLexicalQueryCandidateTerms,\n createMentionLexicalHits,\n scoreLexicalText,\n type LexicalQuery,\n} from \"./lexical-search.js\";\nimport {\n createEntitySearchSession,\n createSearchSession,\n decodeBucketSearchSessionCursor,\n decodeSearchSessionCursor,\n encodeBucketSearchSessionCursor,\n populateSearchSessionObjectCaches,\n readCachedEntitySearchSessionPage,\n readCachedSearchSessionPage,\n readEntitySearchEvidenceMentions,\n readEntitySearchSessionPage,\n readSearchSessionChunkBucketPage,\n readSearchSessionDescriptor,\n readSearchSessionMetadataForCursor,\n readSearchSessionObjectBucketPage,\n readSearchSessionPage,\n SEARCH_EVIDENCE_KIND,\n type BucketSearchCursor,\n type SearchChunkHitInput,\n type SearchChunkCursorKey,\n type SearchChapterTitleCursorKey,\n type SearchEntityHitInput,\n type SearchEvidenceHitEventInput,\n type SearchObjectCursorKey,\n type SearchSessionDescriptor,\n type SearchTextCursorKey,\n type SearchTripleHitInput,\n} from \"./search-cache.js\";\nimport {\n createSearchIndexFingerprint,\n ensureSearchIndex,\n querySearchIndex,\n readSearchIndexStatus,\n SEARCH_INDEX_FTS_HIT_LIMIT,\n SEARCH_OBJECT_PROPERTY_KIND,\n SEARCH_OBJECT_PROPERTY_OWNER_KIND,\n TEXT_SENTENCE_KIND,\n type SearchIndexInput,\n type SearchIndexObjectHit,\n type SearchIndexProgressReporter,\n type SearchIndexQueryResult,\n type SearchIndexTextHit,\n} from \"../search-index/search-index.js\";\nimport { WIKI_GRAPH_URI_PREFIX } from \"../../common/wiki-graph-uri.js\";\n\nexport type ArchiveObjectType =\n | \"chapter\"\n | \"chapter-title\"\n | \"chapter-tree\"\n | \"edge\"\n | \"entity\"\n | \"fragment\"\n | \"meta\"\n | \"node\"\n | \"source\"\n | \"state\"\n | \"summary\"\n | \"triple\";\ntype ChapterStateTarget =\n | \"knowledge-graph\"\n | \"reading-graph\"\n | \"reading-summary\"\n | \"source\";\ntype ChapterStateValue = \"missing\" | \"ready\";\ntype ChapterState = Record<ChapterStateTarget, ChapterStateValue>;\n\nexport type ArchiveCollectionType =\n | \"chapter\"\n | \"chapter-title\"\n | \"entity\"\n | \"fragment\"\n | \"meta\"\n | \"node\"\n | \"source\"\n | \"summary\"\n | \"triple\";\n\nexport type ArchiveFindObjectType =\n | \"chapter\"\n | \"chapter-title\"\n | \"chapter-tree\"\n | \"entity\"\n | \"fragment\"\n | \"meta\"\n | \"node\"\n | \"source\"\n | \"summary\"\n | \"triple\";\n\nexport type ArchiveFindFilterType =\n | \"chapter\"\n | \"chapter-title\"\n | \"entity\"\n | \"fragment\"\n | \"meta\"\n | \"node\"\n | \"source\"\n | \"summary\"\n | \"triple\";\n\nexport interface ArchiveIndex {\n readonly chapters: readonly ChapterEntry[];\n readonly edgeCount: number;\n readonly meta: BookMeta | undefined;\n readonly nodeCount: number;\n readonly summaryCount: number;\n}\n\nexport interface ArchiveFindHit {\n readonly backlinks?: ArchiveBacklinks;\n readonly chapter?: number;\n readonly evidence?: ArchiveFindEvidencePreview;\n readonly evidenceLinks?: readonly MentionLinkRecord[];\n readonly evidenceMentions?: readonly EntityEvidenceMention[];\n readonly field: ArchiveFindField;\n readonly id: string;\n readonly matchCount?: number;\n readonly matchedTerms?: readonly string[];\n readonly missingTerms?: readonly string[];\n readonly position?: ArchiveFindPosition;\n readonly score?: number;\n readonly snippet: string;\n readonly state?: ChapterState;\n readonly title: string;\n readonly triple?: {\n readonly objectLabel: string;\n readonly predicate: string;\n readonly subjectLabel: string;\n };\n readonly type: ArchiveFindObjectType;\n}\n\ninterface EntityEvidenceMention {\n readonly match: Pick<\n ArchiveFindHit,\n \"matchCount\" | \"matchedTerms\" | \"missingTerms\" | \"score\"\n >;\n readonly mention: MentionRecord;\n}\n\ninterface EvidenceReadContext {\n readonly chapters: Map<number, Promise<ChapterEntry>>;\n readonly streamIndexes: Map<string, Promise<ArchiveTextStreamIndex>>;\n}\n\nexport interface ArchiveFindEvidencePreview {\n readonly nextCursor: string | null;\n readonly shown: number;\n readonly sources: readonly ArchiveEvidenceItem[];\n readonly total: number;\n}\n\nexport type ArchiveFindField =\n | \"content\"\n | \"metadata\"\n | \"source\"\n | \"summary\"\n | \"title\";\n\nexport interface ArchiveFindOptions {\n readonly archiveKey?: string;\n readonly backlinks?: boolean;\n readonly chapters?: readonly number[];\n readonly cursor?: string;\n readonly evidenceLimit?: number;\n readonly ids?: readonly string[];\n readonly limit?: number;\n readonly match?: ArchiveFindMatch;\n readonly order?: ArchiveFindOrder;\n readonly sourceContext?: number;\n readonly triplePattern?: ArchiveTriplePattern;\n readonly types?: readonly ArchiveFindFilterType[];\n}\n\nexport type ArchiveFindOrder = \"doc-asc\" | \"doc-desc\";\nexport type ArchiveFindMatch = \"all\" | \"any\";\n\nexport interface ArchiveFindPosition {\n readonly chapter: number;\n readonly documentOrder?: number;\n readonly fragment?: number;\n readonly sentence?: number;\n}\n\nexport interface ArchiveFindResult {\n readonly chapters: readonly number[] | null;\n readonly items: readonly ArchiveFindHit[];\n readonly lens: ArchiveFindLens;\n readonly lensHint: ArchiveFindLensHint | null;\n readonly limit: number;\n readonly match: ArchiveFindMatch;\n readonly nextCursor: string | null;\n readonly order: ArchiveFindOrder;\n readonly query: string;\n readonly terms: readonly string[];\n readonly types: readonly ArchiveFindFilterType[] | null;\n}\n\nexport type ArchiveFindLens = \"broad\" | \"exact\" | \"typed\";\n\nexport interface ArchiveFindLensHint {\n readonly lenses: {\n readonly chapter: string;\n readonly chunk: string;\n readonly entity: string;\n readonly node: string;\n readonly triple: string;\n };\n readonly message: string;\n}\n\nexport interface ArchiveCollectionOptions {\n readonly backlinks?: boolean;\n readonly chapters?: readonly number[];\n readonly cursor?: string;\n readonly evidenceLimit?: number;\n readonly ids?: readonly string[];\n readonly limit?: number;\n readonly order?: ArchiveFindOrder;\n readonly sourceContext?: number;\n readonly triplePattern?: ArchiveTriplePattern;\n readonly types?: readonly ArchiveCollectionType[];\n}\n\nexport interface ArchiveTriplePattern {\n readonly objectQid?: string;\n readonly predicate?: string;\n readonly subjectQid?: string;\n}\n\nexport interface ArchiveCollectionResult {\n readonly chapters: readonly number[] | null;\n readonly ids: readonly string[] | null;\n readonly items: readonly ArchiveFindHit[];\n readonly limit: number;\n readonly nextCursor: string | null;\n readonly order: ArchiveFindOrder;\n readonly types: readonly ArchiveCollectionType[] | null;\n}\n\nexport interface ArchiveBacklinks {\n readonly chunks: ArchiveBacklinkBucket;\n readonly entities: ArchiveBacklinkBucket;\n readonly triples: ArchiveBacklinkBucket;\n}\n\nexport interface ArchiveBacklinkBucket {\n readonly items: readonly ArchiveFindHit[];\n readonly limit: number;\n readonly nextCursor: string | null;\n}\n\nexport type ArchiveListKind =\n | \"chapters\"\n | \"edges\"\n | \"fragments\"\n | \"meta\"\n | \"nodes\"\n | \"summaries\";\n\nexport type ArchiveListItem =\n | {\n readonly evidence?: ArchiveFindEvidencePreview;\n readonly id: string;\n readonly label: string;\n readonly score?: number;\n readonly state?: ChapterState;\n readonly summary: string;\n readonly type: Exclude<ArchiveObjectType, \"triple\">;\n }\n | {\n readonly evidence?: ArchiveFindEvidencePreview;\n readonly id: string;\n readonly label: string;\n readonly evidenceLinks?: readonly MentionLinkRecord[];\n readonly objectLabel: string;\n readonly objectQid: string;\n readonly predicate: string;\n readonly score?: number;\n readonly subjectLabel: string;\n readonly subjectQid: string;\n readonly summary: string;\n readonly type: \"triple\";\n };\n\nexport type ArchivePage =\n | {\n readonly id: string;\n readonly state: ChapterState;\n readonly title: string;\n readonly type: \"chapter\";\n }\n | {\n readonly id: string;\n readonly title: string;\n readonly type: \"chapter-title\";\n }\n | {\n readonly id: string;\n readonly title: string;\n readonly tree: ChapterTree;\n readonly type: \"chapter-tree\";\n }\n | {\n readonly generatedNodeSummary: string;\n readonly id: string;\n readonly incoming: readonly GraphNeighbor[];\n readonly neighbors: readonly GraphNeighbor[];\n readonly outgoing: readonly GraphNeighbor[];\n readonly position: ArchiveFindPosition | undefined;\n readonly sourceFragments: readonly ArchiveNodeSourceFragment[];\n readonly title: string;\n readonly type: \"node\";\n }\n | {\n readonly backlinks?: ArchiveBacklinks;\n readonly fragment: ArchiveSourceFragment;\n readonly id: string;\n readonly nextFragmentId: string | undefined;\n readonly nodes: readonly ArchiveNodeLabel[];\n readonly previousFragmentId: string | undefined;\n readonly title: string;\n readonly type: \"fragment\";\n }\n | {\n readonly content: string;\n readonly id: string;\n readonly title: string;\n readonly type: \"summary\";\n }\n | {\n readonly evidence: ArchiveFindEvidencePreview;\n readonly id: string;\n readonly label: string;\n readonly labels: readonly string[];\n readonly mentionCount: number;\n readonly qid: string;\n readonly type: \"entity\";\n }\n | {\n readonly en: ArchiveEntityWikipageLocale | null;\n readonly id: string;\n readonly type: \"entity-wikipage\";\n readonly zh: ArchiveEntityWikipageLocale | null;\n }\n | {\n readonly evidence: ArchiveFindEvidencePreview;\n readonly id: string;\n readonly label: string;\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n readonly type: \"triple\";\n }\n | {\n readonly authors?: readonly string[];\n readonly description?: string;\n readonly id: string;\n readonly publisher?: string;\n readonly title: string;\n readonly type: \"meta\";\n }\n | {\n readonly id: string;\n readonly state: ChapterState;\n readonly type: \"state\";\n }\n | {\n readonly id: string;\n readonly target: ChapterStateTarget;\n readonly type: \"state\";\n readonly value: ChapterStateValue;\n };\n\nexport interface ArchiveEntityWikipageLocale {\n readonly description?: string;\n readonly title: string;\n readonly url: string;\n}\n\nexport interface ArchivePack {\n readonly anchor: ArchivePage;\n readonly budget: number;\n readonly related: readonly ArchiveListItem[];\n}\n\nexport type ArchiveRelatedRole = \"any\" | \"object\" | \"self\" | \"subject\";\n\nexport interface ArchiveRelatedOptions {\n readonly cursor?: string;\n readonly evidenceLimit?: number;\n readonly limit?: number;\n readonly order?: ArchiveFindOrder;\n readonly query?: string;\n readonly role?: ArchiveRelatedRole;\n readonly sourceContext?: number;\n}\n\nexport interface ArchiveRelatedResult {\n readonly items: readonly ArchiveListItem[];\n readonly limit: number;\n readonly nextCursor: string | null;\n}\n\nexport interface ArchiveEvidence {\n readonly items: readonly ArchiveEvidenceItem[];\n readonly limit: number;\n readonly nextCursor: string | null;\n}\n\nexport interface ArchiveEvidenceItem {\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly fragmentId?: number;\n readonly id: string;\n readonly score?: number;\n readonly source: string;\n readonly startSentenceIndex: number;\n readonly title: string;\n readonly type: \"source\";\n}\n\nexport interface ArchiveNodeLabel {\n readonly id: string;\n readonly title: string;\n}\n\ninterface PositionedNodeLabel {\n readonly label: ArchiveNodeLabel;\n readonly position: ArchiveFindPosition | undefined;\n}\n\nexport interface ArchiveSourceFragment {\n readonly fragmentId?: number;\n readonly id: string;\n readonly preview: string;\n readonly sentenceCount: number;\n readonly text: string;\n readonly wordsCount: number;\n}\n\ntype ArchiveTextStreamKind = \"source\" | \"summary\";\ntype SourceEvidenceRange = {\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly score?: number;\n readonly startSentenceIndex: number;\n};\n\ntype TextStreamHitRange = {\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly hit: ArchiveFindHit;\n readonly startSentenceIndex: number;\n readonly stream: ArchiveTextStreamKind;\n};\n\ninterface ArchiveTextStreamSentence {\n readonly fragmentId: number;\n readonly globalIndex: number;\n readonly localIndex: number;\n readonly text: string;\n readonly wordsCount: number;\n}\n\ninterface ArchiveTextStreamIndex {\n readonly sentences: readonly ArchiveTextStreamSentence[];\n}\n\nexport interface ArchiveNodeSourceFragment {\n readonly id: string;\n readonly text: string;\n readonly truncated: boolean;\n}\n\nexport interface ArchiveEvidenceOptions {\n readonly cursor?: string;\n readonly limit?: number;\n readonly order?: ArchiveFindOrder;\n readonly query?: string;\n readonly sourceContext?: number;\n}\n\nconst DEFAULT_SOURCE_CONTEXT = 2;\n\nexport interface ArchivePageOptions {\n readonly backlinks?: boolean;\n readonly evidenceLimit?: number;\n readonly order?: ArchiveFindOrder;\n readonly sourceContext?: number;\n readonly wikipageResolverOptions?: WikipageResolverOptions;\n}\n\nexport async function getArchiveIndex(\n document: ReadonlyDocument,\n): Promise<ArchiveIndex> {\n const [chapters, meta, nodes, edges] = await Promise.all([\n listChapters(document),\n document.readBookMeta(),\n document.chunks.countAll(),\n document.readingEdges.countAll(),\n ]);\n\n return {\n chapters,\n edgeCount: edges,\n meta,\n nodeCount: nodes,\n summaryCount: chapters.filter((chapter) => chapter.stage === \"summarized\")\n .length,\n };\n}\n\nexport async function listArchiveObjects(\n document: ReadonlyDocument,\n kind: ArchiveListKind,\n): Promise<readonly ArchiveListItem[]> {\n switch (kind) {\n case \"chapters\":\n return await Promise.all(\n (await listChapters(document)).map(async (chapter) => {\n const state = await createChapterState(document, chapter);\n\n return {\n id: formatChapterId(chapter.chapterId),\n label: chapter.title ?? \"[untitled]\",\n state,\n summary: formatChapterStateSummary(state),\n type: \"chapter\" as const,\n };\n }),\n );\n case \"edges\":\n return (await document.readingEdges.listAll()).map((edge) => ({\n id: formatEdgeId(edge),\n label: `${formatNodeId(edge.fromId)} -> ${formatNodeId(edge.toId)}`,\n summary: `weight ${formatWeight(edge.weight)}`,\n type: \"edge\",\n }));\n case \"meta\": {\n const meta = await document.readBookMeta();\n\n return [\n {\n id: ARCHIVE_ROOT_ID,\n label: formatMetaTitle(meta),\n summary: formatMetaSummary(meta),\n type: \"meta\",\n },\n ];\n }\n case \"nodes\":\n return (await document.chunks.listAll()).map((node) => ({\n id: formatNodeId(node.id),\n label: node.label,\n summary: node.content,\n type: \"node\",\n }));\n case \"summaries\":\n return (\n await Promise.all(\n (await listChapters(document)).map(async (chapter) => {\n const summary = await document.readSummary(chapter.chapterId);\n\n if (summary === undefined) {\n return undefined;\n }\n\n return {\n id: formatSummaryId(chapter.chapterId),\n label: chapter.title ?? `[chapter ${chapter.chapterId}]`,\n summary: createSnippet(summary),\n type: \"summary\" as const,\n };\n }),\n )\n ).filter(isDefined);\n case \"fragments\":\n return (\n await Promise.all(\n (await listChapters(document)).map(async (chapter) => {\n const title = chapter.title ?? formatChapterId(chapter.chapterId);\n\n return listTextStreamSentenceCollection(\n await createTextStreamIndex(\n document,\n chapter.chapterId,\n \"source\",\n ),\n chapter.chapterId,\n \"source\",\n title,\n chapter.documentOrder,\n ).map((hit) => ({\n id: hit.id,\n label: title,\n summary: hit.snippet,\n type: \"source\" as const,\n }));\n }),\n )\n ).flat();\n }\n}\n\nexport async function listArchiveCollection(\n document: ReadonlyDocument,\n options: ArchiveCollectionOptions = {},\n): Promise<ArchiveCollectionResult> {\n const items: ArchiveFindHit[] = [];\n const documentOrders = await document.serials.listDocumentOrders();\n const chapterFilter =\n options.chapters === undefined ? undefined : new Set(options.chapters);\n const types = options.types ?? [\n \"meta\",\n \"chapter-title\",\n \"entity\",\n \"node\",\n \"triple\",\n ];\n\n if (types.includes(\"meta\")) {\n const meta = await document.readBookMeta();\n\n if (meta !== undefined) {\n items.push({\n field: \"metadata\",\n id: ARCHIVE_ROOT_ID,\n snippet: formatMetaSummary(meta),\n title: meta.title ?? \"Archive metadata\",\n type: \"meta\",\n });\n }\n }\n\n if (types.includes(\"chapter\") || types.includes(\"chapter-title\")) {\n for (const chapter of filterChapters(\n await listChapters(document),\n chapterFilter,\n )) {\n const title = chapter.title ?? `[chapter ${chapter.chapterId}]`;\n\n if (types.includes(\"chapter\") || types.includes(\"chapter-title\")) {\n items.push({\n chapter: chapter.chapterId,\n field: \"title\",\n id: formatChapterTitleId(chapter.chapterId),\n position: {\n chapter: chapter.chapterId,\n documentOrder: chapter.documentOrder,\n },\n snippet: title,\n title,\n type: \"chapter-title\",\n });\n }\n }\n }\n\n if (types.includes(\"node\")) {\n for (const node of await document.chunks.listAll()) {\n if (!isChapterAllowed(chapterFilter, node.sentenceId[0])) {\n continue;\n }\n\n const position = createNodePosition(node.sentenceIds, documentOrders);\n\n items.push({\n chapter: node.sentenceId[0],\n field: \"content\",\n id: formatNodeId(node.id),\n ...(position === undefined ? {} : { position }),\n snippet: createSnippet(node.content),\n title: node.label,\n type: \"node\",\n });\n }\n }\n\n if (types.includes(\"entity\")) {\n items.push(\n ...listEntityCollection(\n filterMentionsByChapterSet(\n await listAllMentions(document),\n chapterFilter,\n ),\n documentOrders,\n ),\n );\n }\n\n if (types.includes(\"triple\")) {\n items.push(\n ...(await listTripleCollection(document, chapterFilter, documentOrders)),\n );\n }\n\n const result = createCollectionResult(items, options);\n const evidenceItems = await hydrateFindHitEvidence(document, result.items, {\n ...(options.evidenceLimit === undefined\n ? {}\n : { evidenceLimit: options.evidenceLimit }),\n order: options.order ?? \"doc-asc\",\n ...(options.sourceContext === undefined\n ? {}\n : { sourceContext: options.sourceContext }),\n });\n\n return {\n ...result,\n items: await hydrateFindHitBacklinks(document, evidenceItems, options),\n };\n}\n\nfunction filterChapters<T extends { readonly chapterId: number }>(\n chapters: readonly T[],\n chapterFilter: ReadonlySet<number> | undefined,\n): readonly T[] {\n return chapterFilter === undefined\n ? chapters\n : chapters.filter((chapter) => chapterFilter.has(chapter.chapterId));\n}\n\nfunction isChapterAllowed(\n chapterFilter: ReadonlySet<number> | undefined,\n chapterId: number,\n): boolean {\n return chapterFilter === undefined || chapterFilter.has(chapterId);\n}\n\nexport async function findArchiveObjects(\n document: ReadonlyDocument,\n query: string,\n options: ArchiveFindOptions = {},\n): Promise<ArchiveFindResult> {\n const limit = options.limit ?? DEFAULT_FIND_LIMIT;\n const textOnlySearch = isTextOnlySearch(options);\n\n if (\n options.cursor !== undefined &&\n (!textOnlySearch || !isFindCursor(options.cursor))\n ) {\n const bucketCursor = tryDecodeBucketSearchSessionCursor(options.cursor);\n\n if (bucketCursor !== undefined) {\n return await readBucketedSearchResultPage(document, bucketCursor, {\n ...options,\n limit,\n });\n }\n\n const cursor = decodeSearchSessionCursor(options.cursor);\n const descriptor = await readSearchSessionDescriptor(\n cursor.sessionId,\n options.archiveKey ?? \"archive\",\n );\n\n assertSearchCursorTypesMatch(options.types, descriptor.types);\n\n const page = isEntitySearchTypes(descriptor.types)\n ? await readEntitySearchSessionPage(\n cursor.sessionId,\n cursor.offset,\n limit,\n options.archiveKey ?? \"archive\",\n cursor.createdAt,\n )\n : await readSearchSessionPage(\n cursor.sessionId,\n cursor.offset,\n limit,\n options.archiveKey ?? \"archive\",\n cursor.createdAt,\n );\n\n return await hydrateFindResultBacklinks(\n document,\n {\n chapters: page.chapters,\n items: await hydrateFindHitEvidence(\n document,\n page.items,\n createFindEvidenceHydrationOptions(options, cursor.sessionId),\n ),\n lens: parseFindLens(page.lens),\n lensHint: page.lens === \"broad\" ? BROAD_FIND_LENS_HINT : null,\n limit,\n match: parseFindMatch(page.match),\n nextCursor: page.nextCursor,\n order: options.order ?? \"doc-asc\",\n query: page.query,\n terms: page.terms,\n types: parseFindTypes(descriptor.types),\n },\n options,\n );\n }\n\n const requestedTypes = options.types ?? null;\n const wantsStructuredSearch =\n requestedTypes === null ||\n requestedTypes.includes(\"entity\") ||\n requestedTypes.includes(\"triple\");\n const search = createLexicalQuery(query);\n\n if (search === undefined) {\n return createFindResult(query, [], options);\n }\n\n if (textOnlySearch) {\n return await findTextOnlyArchiveObjectsIndexed(\n document,\n query,\n options,\n search,\n );\n }\n\n const revisionScope = await createSearchRevisionScope(\n document,\n options.chapters,\n );\n const cacheInput = {\n archiveKey: options.archiveKey ?? \"archive\",\n chapters: options.chapters ?? null,\n lens: options.types === undefined ? \"broad\" : \"typed\",\n match: options.match ?? \"any\",\n order: options.order ?? \"doc-asc\",\n query,\n revisionScope,\n terms: search.terms,\n types: options.types ?? null,\n };\n const canReadSearchCache = options.triplePattern === undefined;\n const usesBucketedSearch =\n options.types === undefined && options.triplePattern === undefined;\n\n if (canReadSearchCache && isEntityOnlySearch(options)) {\n const cachedPage = await readCachedEntitySearchSessionPage(\n cacheInput,\n 0,\n limit,\n );\n\n if (cachedPage !== undefined) {\n return await hydrateFindResultBacklinks(\n document,\n {\n chapters: cachedPage.chapters,\n items: await hydrateFindHitEvidence(\n document,\n cachedPage.items,\n createFindEvidenceHydrationOptions(options, cachedPage.sessionId),\n ),\n lens: parseFindLens(cachedPage.lens),\n lensHint: cachedPage.lens === \"broad\" ? BROAD_FIND_LENS_HINT : null,\n limit,\n match: parseFindMatch(cachedPage.match),\n nextCursor: cachedPage.nextCursor,\n order: options.order ?? \"doc-asc\",\n query: cachedPage.query,\n terms: cachedPage.terms,\n types: parseFindTypes(cachedPage.types),\n },\n options,\n );\n }\n } else if (canReadSearchCache && !usesBucketedSearch) {\n const cachedPage = await readCachedSearchSessionPage(cacheInput, 0, limit);\n\n if (cachedPage !== undefined) {\n return await hydrateFindResultBacklinks(\n document,\n {\n chapters: cachedPage.chapters,\n items: await hydrateFindHitEvidence(\n document,\n cachedPage.items,\n createFindEvidenceHydrationOptions(options),\n ),\n lens: parseFindLens(cachedPage.lens),\n lensHint: cachedPage.lens === \"broad\" ? BROAD_FIND_LENS_HINT : null,\n limit,\n match: parseFindMatch(cachedPage.match),\n nextCursor: cachedPage.nextCursor,\n order: options.order ?? \"doc-asc\",\n query: cachedPage.query,\n terms: cachedPage.terms,\n types: parseFindTypes(cachedPage.types),\n },\n options,\n );\n }\n }\n\n if (usesBucketedSearch) {\n if (!(await isArchiveSearchIndexCurrent(document))) {\n throw new Error(\n \"Wiki Graph search index is missing or outdated. Run `<archive-uri>/index enable` before searching.\",\n );\n }\n const sessionId = await createSearchSession({\n archiveKey: options.archiveKey ?? \"archive\",\n chapters: options.chapters ?? null,\n lens: \"broad\",\n match: options.match ?? \"any\",\n order: options.order ?? \"doc-asc\",\n query,\n revisionScope,\n terms: search.terms,\n types: null,\n });\n const descriptor = await readSearchSessionDescriptor(\n sessionId,\n options.archiveKey ?? \"archive\",\n );\n\n return await readBucketedSearchResultPage(\n document,\n {\n createdAt: descriptor.createdAt,\n cursor: { bucket: 0 },\n sessionId,\n },\n { ...options, limit },\n );\n }\n\n const allMentions = wantsStructuredSearch\n ? await document.mentions.listBySurfaceTerms(\n listLexicalQueryCandidateTerms(query),\n )\n : [];\n const indexed = await findArchiveObjectsIndexed(document, query, options);\n const structuredHits = wantsStructuredSearch\n ? [\n ...findEntities(search, { mentions: allMentions }),\n ...(await findTriples(document, search, { mentions: allMentions })),\n ]\n : [];\n const hits = [...structuredHits, ...(indexed?.hits ?? [])];\n if (isEntityOnlySearch(options)) {\n const ranked = createRankedFindResult(\n query,\n filterLexicalHitsByMatch(hits, search, options.match ?? \"any\"),\n options,\n search.terms,\n );\n const entityCacheInput = createEntitySearchCacheInput(\n ranked.items,\n indexed?.result,\n );\n const sentenceCacheInput = await createSentenceEvidenceSearchCacheInput(\n document,\n indexed?.result,\n options,\n );\n const sessionId = await createEntitySearchSession({\n archiveKey: options.archiveKey ?? \"archive\",\n chapters: ranked.chapters,\n chunkHits: sentenceCacheInput.chunkHits,\n entityHits: [\n ...entityCacheInput.entityHits,\n ...sentenceCacheInput.entityHits,\n ],\n evidenceEvents: [\n ...entityCacheInput.evidenceEvents,\n ...sentenceCacheInput.evidenceEvents,\n ],\n lens: ranked.lens,\n match: ranked.match,\n order: ranked.order,\n query,\n revisionScope,\n terms: ranked.terms,\n tripleHits: sentenceCacheInput.tripleHits,\n types: ranked.types,\n });\n const firstPage = await readEntitySearchSessionPage(sessionId, 0, limit);\n\n return await hydrateFindResultBacklinks(\n document,\n {\n ...ranked,\n items: await hydrateFindHitEvidence(\n document,\n firstPage.items,\n createFindEvidenceHydrationOptions(options, sessionId),\n ),\n nextCursor: firstPage.nextCursor,\n },\n options,\n );\n }\n\n const ranked = createRankedFindResult(\n query,\n filterLexicalHitsByMatch(hits, search, options.match ?? \"any\"),\n options,\n search.terms,\n );\n const entityCacheInput = createEntitySearchCacheInput(\n ranked.items,\n indexed?.result,\n );\n const sentenceCacheInput = await createSentenceEvidenceSearchCacheInput(\n document,\n indexed?.result,\n options,\n );\n const sessionId = await createSearchSession({\n archiveKey: options.archiveKey ?? \"archive\",\n chapters: ranked.chapters,\n chunkHits: sentenceCacheInput.chunkHits,\n entityHits: [\n ...entityCacheInput.entityHits,\n ...sentenceCacheInput.entityHits,\n ],\n evidenceEvents: [\n ...entityCacheInput.evidenceEvents,\n ...sentenceCacheInput.evidenceEvents,\n ],\n items: ranked.items,\n lens: ranked.lens,\n match: ranked.match,\n order: ranked.order,\n query,\n revisionScope,\n terms: ranked.terms,\n tripleHits: sentenceCacheInput.tripleHits,\n types: ranked.types,\n });\n const firstPage = await readSearchSessionPage(sessionId, 0, limit);\n\n return await hydrateFindResultBacklinks(\n document,\n {\n ...ranked,\n items: await hydrateFindHitEvidence(\n document,\n firstPage.items,\n createFindEvidenceHydrationOptions(options),\n ),\n nextCursor: firstPage.nextCursor,\n },\n options,\n );\n}\n\nasync function findTextOnlyArchiveObjectsIndexed(\n document: ReadonlyDocument,\n query: string,\n options: ArchiveFindOptions,\n search: LexicalQuery,\n): Promise<ArchiveFindResult> {\n const indexed = await findArchiveObjectsIndexed(document, query, options);\n const hits = indexed?.hits ?? [];\n\n return createFindResult(\n query,\n filterLexicalHitsByMatch(hits, search, options.match ?? \"any\"),\n options,\n indexed?.result.terms ?? search.terms,\n );\n}\n\nasync function createSearchRevisionScope(\n document: ReadonlyDocument,\n chapters: readonly number[] | undefined,\n): Promise<string> {\n if (chapters === undefined || chapters.length === 0) {\n return JSON.stringify({\n chaptersRevision: await document.serials.getChaptersRevision(),\n scope: \"all\",\n });\n }\n\n const uniqueChapters = [...new Set(chapters)].sort(compareNumbers);\n const revisions = await document.serials.getRevisions(uniqueChapters);\n\n return JSON.stringify({\n chapters: uniqueChapters.map(\n (chapterId) => [chapterId, revisions.get(chapterId) ?? 0] as const,\n ),\n scope: \"chapters\",\n });\n}\n\nasync function readBucketedSearchResultPage(\n document: ReadonlyDocument,\n cursor: {\n readonly createdAt: number;\n readonly cursor: BucketSearchCursor;\n readonly sessionId: string;\n },\n options: ArchiveFindOptions & { readonly limit: number },\n): Promise<ArchiveFindResult> {\n const session = await readSearchSessionMetadataForCursor(\n cursor.sessionId,\n options.archiveKey ?? \"archive\",\n cursor.createdAt,\n );\n\n assertSearchCursorTypesMatch(options.types, session.types);\n\n const items: ArchiveFindHit[] = [];\n let bucketCursor: BucketSearchCursor | undefined = cursor.cursor;\n\n while (bucketCursor !== undefined && items.length < options.limit) {\n const remaining = options.limit - items.length;\n const page = await readBucketPage(\n document,\n session,\n bucketCursor,\n remaining,\n );\n\n items.push(...page.items);\n bucketCursor = page.nextCursor;\n }\n\n return await hydrateFindResultBacklinks(\n document,\n {\n chapters: session.chapters,\n items: await hydrateFindHitEvidence(\n document,\n items,\n createFindEvidenceHydrationOptions(options, cursor.sessionId),\n ),\n lens: parseFindLens(session.lens),\n lensHint: session.lens === \"broad\" ? BROAD_FIND_LENS_HINT : null,\n limit: options.limit,\n match: parseFindMatch(session.match),\n nextCursor:\n bucketCursor === undefined\n ? null\n : encodeBucketSearchSessionCursor(\n cursor.sessionId,\n bucketCursor,\n session.createdAt,\n ),\n order: options.order ?? \"doc-asc\",\n query: session.query,\n terms: session.terms,\n types: parseFindTypes(session.types),\n },\n options,\n );\n}\n\nasync function readBucketPage(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n cursor: BucketSearchCursor,\n limit: number,\n): Promise<{\n readonly items: readonly ArchiveFindHit[];\n readonly nextCursor: BucketSearchCursor | undefined;\n}> {\n switch (cursor.bucket) {\n case 0:\n return await readChapterTitleBucketPage(\n document,\n session,\n cursor.key,\n limit,\n );\n case 1:\n return await readObjectBucketPage(document, session, cursor.key, limit);\n case 2:\n return await readChunkBucketPage(document, session, cursor.key, limit);\n case 3:\n return await readTextBucketPage(document, session, cursor.key, limit);\n }\n}\n\nasync function readChapterTitleBucketPage(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n after: SearchChapterTitleCursorKey | undefined,\n limit: number,\n): Promise<{\n readonly items: readonly ArchiveFindHit[];\n readonly nextCursor: BucketSearchCursor | undefined;\n}> {\n const result = await querySearchIndex(document, session.query, {\n ...(session.chapters === null ? {} : { chapters: session.chapters }),\n match: parseFindMatch(session.match),\n objectHitLimit: SEARCH_INDEX_FTS_HIT_LIMIT,\n textHitLimit: 0,\n types: [\"chapter-title\"],\n });\n const chapters = new Map(\n (await listChapters(document)).map((chapter) => [\n chapter.chapterId,\n chapter,\n ]),\n );\n const hits = (result?.objectHits ?? [])\n .filter(\n (hit) => hit.ownerKind === SEARCH_OBJECT_PROPERTY_OWNER_KIND.chapter,\n )\n .sort(compareChapterTitleIndexHits)\n .filter((hit) => isAfterChapterTitleKey(hit, after));\n const page = hits.slice(0, limit + 1);\n const hydrated = (\n await Promise.all(\n page.slice(0, limit).map(async (hit) => {\n const item = await hydrateSearchObjectHit(document, chapters, hit);\n\n return item === undefined\n ? undefined\n : withSearchTerms(item, session.terms);\n }),\n )\n ).filter(isDefined);\n const last = page.at(limit - 1);\n\n return {\n items: hydrated,\n nextCursor:\n page.length > limit && last !== undefined\n ? {\n bucket: 0,\n key: {\n chapterId: parseSearchPropertyIntegerOwnerId(last.ownerId),\n score: last.score,\n },\n }\n : { bucket: 1 },\n };\n}\n\nasync function readObjectBucketPage(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n after: SearchObjectCursorKey | undefined,\n limit: number,\n): Promise<{\n readonly items: readonly ArchiveFindHit[];\n readonly nextCursor: BucketSearchCursor | undefined;\n}> {\n if (!session.objectCachesPopulated) {\n await populateObjectBucketCaches(document, session);\n }\n const page = await readSearchSessionObjectBucketPage(\n session.sessionId,\n 1,\n after,\n limit,\n );\n const items = page.slice(0, limit);\n const hydrated = await Promise.all(\n items.map(async (hit) => await hydrateCachedObjectBucketHit(document, hit)),\n );\n const last = items.at(-1);\n\n return {\n items: hydrated,\n nextCursor:\n page.length > limit && last !== undefined\n ? {\n bucket: 1,\n key: {\n id: getObjectBucketCursorId(last),\n kind: last.type === \"triple\" ? \"triple\" : \"entity\",\n score: last.score ?? 0,\n },\n }\n : { bucket: 2 },\n };\n}\n\nasync function populateObjectBucketCaches(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n): Promise<void> {\n const search = createLexicalQuery(session.query);\n\n if (search === undefined) {\n return;\n }\n\n const [allMentions, indexed] = await Promise.all([\n document.mentions.listBySurfaceTerms(\n listLexicalQueryCandidateTerms(session.query),\n ),\n querySearchIndex(document, session.query, {\n ...(session.chapters === null ? {} : { chapters: session.chapters }),\n match: parseFindMatch(session.match),\n objectHitLimit: SEARCH_INDEX_FTS_HIT_LIMIT,\n textHitLimit: 0,\n types: null,\n }),\n ]);\n const structuredHits = [\n ...findEntities(search, { mentions: allMentions }),\n ...(await findTriples(document, search, { mentions: allMentions })),\n ];\n const entityCacheInput = createEntitySearchCacheInput(\n structuredHits,\n indexed,\n );\n\n await populateSearchSessionObjectCaches({\n entityHits: entityCacheInput.entityHits,\n evidenceEvents: entityCacheInput.evidenceEvents,\n sessionId: session.sessionId,\n });\n}\n\nasync function readChunkBucketPage(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n after: SearchChunkCursorKey | undefined,\n limit: number,\n): Promise<{\n readonly items: readonly ArchiveFindHit[];\n readonly nextCursor: BucketSearchCursor | undefined;\n}> {\n const page = await readSearchSessionChunkBucketPage(\n session.sessionId,\n after,\n limit,\n );\n const items = page.slice(0, limit);\n const hydrated = (\n await Promise.all(\n items.map(\n async (hit) => await hydrateCachedChunkBucketHit(document, hit),\n ),\n )\n ).filter(isDefined);\n const last = items.at(-1);\n\n return {\n items: hydrated,\n nextCursor:\n page.length > limit && last !== undefined\n ? {\n bucket: 2,\n key: {\n chunkId: parseSearchPropertyIntegerOwnerId(\n last.id.slice(\"wikg://chunk/\".length),\n ),\n score: last.score ?? 0,\n },\n }\n : { bucket: 3 },\n };\n}\n\nasync function readTextBucketPage(\n document: ReadonlyDocument,\n session: SearchSessionDescriptor,\n after: SearchTextCursorKey | undefined,\n limit: number,\n): Promise<{\n readonly items: readonly ArchiveFindHit[];\n readonly nextCursor: BucketSearchCursor | undefined;\n}> {\n const result = await querySearchIndex(document, session.query, {\n ...(session.chapters === null ? {} : { chapters: session.chapters }),\n match: parseFindMatch(session.match),\n objectHitLimit: 0,\n ...(after === undefined\n ? {}\n : {\n textAfter: {\n chapterId: after.chapterId,\n kind: after.kind as SearchIndexTextHit[\"kind\"],\n rank: after.rank,\n sentenceIndex: after.sentenceIndex,\n },\n }),\n textHitLimit: createBucketQueryWindow(limit),\n types: [\"source\", \"summary\"],\n });\n const chapters = new Map(\n (await listChapters(document)).map((chapter) => [\n chapter.chapterId,\n chapter,\n ]),\n );\n const hits = [...(result?.textHits ?? [])]\n .sort(compareTextIndexHits)\n .filter((hit) => isAfterTextKey(hit, after));\n const page = hits.slice(0, limit + 1);\n const hydrated = (\n await Promise.all(\n page.slice(0, limit).map(async (hit) => {\n const item = await hydrateSearchTextHit(document, chapters, hit);\n\n return item === undefined\n ? undefined\n : withSearchTerms(item, session.terms);\n }),\n )\n ).filter(isDefined);\n const last = page.at(limit - 1);\n\n return {\n items: hydrated,\n nextCursor:\n page.length > limit && last !== undefined\n ? {\n bucket: 3,\n key: {\n chapterId: last.chapterId,\n kind: last.kind,\n rank: last.rank,\n sentenceIndex: last.sentenceIndex,\n },\n }\n : undefined,\n };\n}\n\nfunction createBucketQueryWindow(limit: number): number {\n return Math.max(limit + 1, limit * 3 + 1, 100);\n}\n\nasync function hydrateCachedObjectBucketHit(\n document: ReadonlyDocument,\n hit: ArchiveFindHit,\n): Promise<ArchiveFindHit> {\n if (hit.type === \"entity\") {\n const qid = parseEntityQid(hit.id);\n\n if (qid === undefined) {\n return hit;\n }\n const mentions = await document.mentions.listByQid(qid);\n const [first] = [...mentions].sort(compareMentions);\n\n if (first === undefined) {\n return hit;\n }\n\n return {\n ...hit,\n chapter: first.chapterId,\n position: {\n chapter: first.chapterId,\n sentence: first.sentenceIndex ?? 0,\n },\n snippet: first.note ?? first.surface,\n title: selectEntityLabel(mentions),\n };\n }\n if (hit.type === \"triple\") {\n return hit;\n }\n\n return hit;\n}\n\nasync function hydrateCachedChunkBucketHit(\n document: ReadonlyDocument,\n hit: ArchiveFindHit,\n): Promise<ArchiveFindHit | undefined> {\n const chunkId = parseSearchPropertyIntegerOwnerId(\n hit.id.slice(\"wikg://chunk/\".length),\n );\n const node = await document.chunks.getById(chunkId);\n\n if (node === undefined) {\n return undefined;\n }\n const { position: _position, ...baseHit } = hit;\n const position = createNodePosition(node.sentenceIds);\n\n return {\n ...baseHit,\n chapter: node.sentenceId[0],\n field: \"title\",\n ...(position === undefined ? {} : { position }),\n snippet: createSnippet(node.content),\n title: node.label,\n };\n}\n\nfunction compareChapterTitleIndexHits(\n left: SearchIndexObjectHit,\n right: SearchIndexObjectHit,\n): number {\n return (\n compareNumbers(right.score, left.score) ||\n compareNumbers(\n parseSearchPropertyIntegerOwnerId(left.ownerId),\n parseSearchPropertyIntegerOwnerId(right.ownerId),\n )\n );\n}\n\nfunction compareTextIndexHits(\n left: SearchIndexTextHit,\n right: SearchIndexTextHit,\n): number {\n return (\n compareNumbers(left.rank, right.rank) ||\n compareNumbers(left.chapterId, right.chapterId) ||\n compareNumbers(left.sentenceIndex, right.sentenceIndex) ||\n compareNumbers(left.kind, right.kind)\n );\n}\n\nfunction isAfterChapterTitleKey(\n hit: SearchIndexObjectHit,\n key: SearchChapterTitleCursorKey | undefined,\n): boolean {\n if (key === undefined) {\n return true;\n }\n\n return (\n compareChapterTitleIndexHits(\n {\n ...hit,\n ownerId: String(key.chapterId),\n score: key.score,\n },\n hit,\n ) < 0\n );\n}\n\nfunction isAfterTextKey(\n hit: SearchIndexTextHit,\n key: SearchTextCursorKey | undefined,\n): boolean {\n if (key === undefined) {\n return true;\n }\n\n return (\n compareTextIndexHits(\n {\n chapterId: key.chapterId,\n kind: key.kind as SearchIndexTextHit[\"kind\"],\n rank: key.rank,\n score: 0,\n sentenceIndex: key.sentenceIndex,\n wordsCount: 0,\n },\n hit,\n ) < 0\n );\n}\n\nfunction getObjectBucketCursorId(hit: ArchiveFindHit): string {\n if (hit.type !== \"triple\") {\n return hit.id.replace(/^wikg:\\/\\/entity\\//u, \"\");\n }\n\n const triple = parseTripleCursorId(hit.id);\n\n return triple ?? hit.id;\n}\n\nfunction parseTripleCursorId(id: string): string | undefined {\n const match = /^wikg:\\/\\/triple\\/([^/]+)\\/([^/]+)\\/([^/]+)$/u.exec(id);\n\n if (\n match?.[1] === undefined ||\n match[2] === undefined ||\n match[3] === undefined\n ) {\n return undefined;\n }\n\n return `${match[1]}\\u001f${decodeURIComponent(match[2])}\\u001f${match[3]}`;\n}\n\nfunction tryDecodeBucketSearchSessionCursor(cursor: string):\n | {\n readonly createdAt: number;\n readonly cursor: BucketSearchCursor;\n readonly sessionId: string;\n }\n | undefined {\n try {\n return decodeBucketSearchSessionCursor(cursor);\n } catch {\n return undefined;\n }\n}\n\nexport async function rebuildArchiveSearchIndex(\n document: Document,\n progress?: SearchIndexProgressReporter,\n): Promise<void> {\n const input = await createSearchIndexRecords(document, progress);\n\n if ((await readSearchIndexStatus(document, input)) === \"dirty\") {\n await document.deleteSearchIndexDatabase();\n }\n\n await ensureSearchIndex(document, input, progress);\n}\n\nexport async function isArchiveSearchIndexCurrent(\n document: ReadonlyDocument,\n): Promise<boolean> {\n return (await readArchiveSearchIndexStatus(document)) === \"current\";\n}\n\nexport async function readArchiveSearchIndexStatus(\n document: ReadonlyDocument,\n): Promise<\"current\" | \"dirty\" | \"missing\"> {\n return await readSearchIndexStatus(\n document,\n await createSearchIndexRecords(document),\n );\n}\n\nexport async function clearDirtyArchiveSearchIndex(\n document: Document,\n): Promise<void> {\n if ((await readArchiveSearchIndexStatus(document)) === \"dirty\") {\n await document.deleteSearchIndexDatabase();\n }\n}\n\nexport async function createArchiveSearchIndexFingerprint(\n document: ReadonlyDocument,\n): Promise<string> {\n return createSearchIndexFingerprint(await createSearchIndexRecords(document));\n}\n\nexport async function grepArchiveObjects(\n document: ReadonlyDocument,\n query: string,\n options: ArchiveFindOptions = {},\n): Promise<ArchiveFindResult> {\n const search = createPhraseSearch(query);\n\n if (search === undefined) {\n return createFindResult(\n query,\n [],\n { ...options, match: \"all\" },\n [],\n \"exact\",\n );\n }\n\n const hits: ArchiveFindHit[] = [];\n\n hits.push(...findMeta(await document.readBookMeta(), search));\n hits.push(...(await findChapters(document, search)));\n hits.push(...(await findNodes(document, search)));\n\n return createFindResult(\n query,\n hits,\n { ...options, match: \"all\" },\n [query.trim().toLowerCase()],\n \"exact\",\n );\n}\n\nasync function findArchiveObjectsIndexed(\n document: ReadonlyDocument,\n query: string,\n options: ArchiveFindOptions,\n): Promise<\n | {\n readonly hits: readonly ArchiveFindHit[];\n readonly result: SearchIndexQueryResult;\n }\n | undefined\n> {\n if (!(await isArchiveSearchIndexCurrent(document))) {\n throw new Error(\n \"Wiki Graph search index is missing or outdated. Run `<archive-uri>/index enable` before searching.\",\n );\n }\n const result = await querySearchIndex(document, query, {\n ...(options.chapters === undefined ? {} : { chapters: options.chapters }),\n ...(options.match === undefined ? {} : { match: options.match }),\n ...createSearchIndexQueryLimitOptions(options),\n types: options.types ?? null,\n });\n\n return result === undefined\n ? undefined\n : {\n hits: await hydrateSearchIndexHits(\n document,\n result,\n createSearchIndexHydrationOptions(options),\n ),\n result,\n };\n}\n\nasync function queryRequiredSearchIndex(\n document: ReadonlyDocument,\n query: string,\n options: Parameters<typeof querySearchIndex>[2],\n): Promise<SearchIndexQueryResult | undefined> {\n if (!(await isArchiveSearchIndexCurrent(document))) {\n throw new Error(\n \"Wiki Graph search index is missing or outdated. Run `<archive-uri>/index enable` before searching.\",\n );\n }\n\n return await querySearchIndex(document, query, options);\n}\n\nasync function hydrateSearchIndexHits(\n document: ReadonlyDocument,\n result: SearchIndexQueryResult,\n options: {\n readonly textHitLimit?: number;\n } = {},\n): Promise<readonly ArchiveFindHit[]> {\n const chapters = new Map(\n (await listChapters(document)).map((chapter) => [\n chapter.chapterId,\n chapter,\n ]),\n );\n const hits: ArchiveFindHit[] = [];\n\n for (const hit of result.objectHits) {\n const hydrated = await hydrateSearchObjectHit(document, chapters, hit);\n\n if (hydrated !== undefined) {\n hits.push(withSearchTerms(hydrated, result.terms));\n }\n }\n\n const textHits =\n options.textHitLimit === undefined\n ? result.textHits\n : result.textHits.slice(0, options.textHitLimit);\n\n for (const hit of textHits) {\n const hydrated = await hydrateSearchTextHit(document, chapters, hit);\n\n if (hydrated !== undefined) {\n hits.push(withSearchTerms(hydrated, result.terms));\n }\n }\n\n return hits;\n}\n\nfunction createSearchIndexHydrationOptions(options: ArchiveFindOptions): {\n readonly textHitLimit?: number;\n} {\n if (!isTextOnlySearch(options) || options.limit === undefined) {\n return {};\n }\n\n return { textHitLimit: createTextOnlySearchCacheWindow(options.limit) };\n}\n\nfunction createSearchIndexQueryLimitOptions(options: ArchiveFindOptions): {\n readonly textHitLimit?: number;\n} {\n if (!isTextOnlySearch(options) || options.limit === undefined) {\n return {};\n }\n\n return { textHitLimit: createTextOnlySearchCacheWindow(options.limit) };\n}\n\nfunction createTextOnlySearchCacheWindow(limit: number): number {\n return Math.max(limit + 1, TEXT_ONLY_SEARCH_CACHE_WINDOW);\n}\n\nfunction isTextOnlySearch(options: ArchiveFindOptions): boolean {\n return (\n options.types !== undefined &&\n options.types.length > 0 &&\n options.types.every((type) => type === \"source\" || type === \"summary\")\n );\n}\n\nfunction withSearchTerms(\n hit: ArchiveFindHit,\n terms: readonly string[],\n): ArchiveFindHit {\n return {\n ...hit,\n matchCount: terms.length,\n matchedTerms: terms,\n missingTerms: [],\n };\n}\n\nasync function hydrateSearchObjectHit(\n document: ReadonlyDocument,\n chapters: ReadonlyMap<number, ChapterEntry>,\n hit: SearchIndexObjectHit,\n): Promise<ArchiveFindHit | undefined> {\n switch (hit.ownerKind) {\n case SEARCH_OBJECT_PROPERTY_OWNER_KIND.chapter: {\n const chapterId = parseSearchPropertyIntegerOwnerId(hit.ownerId);\n const chapter = chapters.get(chapterId);\n\n if (chapter === undefined) {\n return undefined;\n }\n\n const title = chapter.title ?? `[chapter ${chapter.chapterId}]`;\n\n return {\n chapter: chapter.chapterId,\n field: \"title\",\n id: formatChapterTitleId(chapter.chapterId),\n matchCount: 1,\n position: { chapter: chapter.chapterId },\n score: hit.score,\n snippet: title,\n title,\n type: \"chapter-title\",\n };\n }\n case SEARCH_OBJECT_PROPERTY_OWNER_KIND.chunk: {\n const chunkId = parseSearchPropertyIntegerOwnerId(hit.ownerId);\n const node = await document.chunks.getById(chunkId);\n\n if (node === undefined) {\n return undefined;\n }\n\n const position = createNodePosition(node.sentenceIds);\n const isLabel = hit.propertyKind === SEARCH_OBJECT_PROPERTY_KIND.label;\n\n return {\n chapter: node.sentenceId[0],\n field: isLabel ? \"title\" : \"content\",\n id: formatNodeId(node.id),\n matchCount: 1,\n ...(position === undefined ? {} : { position }),\n score: hit.score,\n snippet: isLabel ? node.label : createSnippet(node.content),\n title: node.label,\n type: \"node\",\n };\n }\n case SEARCH_OBJECT_PROPERTY_OWNER_KIND.entity:\n return undefined;\n }\n}\n\nfunction parseSearchPropertyIntegerOwnerId(ownerId: string): number {\n const value = Number(ownerId);\n\n if (!Number.isSafeInteger(value)) {\n throw new Error(`Invalid search property owner id: ${ownerId}`);\n }\n\n return value;\n}\n\nasync function hydrateSearchTextHit(\n document: ReadonlyDocument,\n chapters: ReadonlyMap<number, ChapterEntry>,\n hit: SearchIndexTextHit,\n): Promise<ArchiveFindHit | undefined> {\n const stream =\n hit.kind === TEXT_SENTENCE_KIND.source\n ? (\"source\" as const)\n : (\"summary\" as const);\n const chapter = chapters.get(hit.chapterId);\n\n if (chapter === undefined) {\n return undefined;\n }\n\n const range = await readTextStreamRange(\n document,\n hit.chapterId,\n stream,\n hit.sentenceIndex,\n hit.sentenceIndex,\n );\n\n return {\n chapter: hit.chapterId,\n field: stream,\n id: range.id,\n matchCount: 1,\n position: {\n chapter: hit.chapterId,\n sentence: hit.sentenceIndex,\n },\n score: hit.score,\n snippet: createSnippet(range.text),\n title: chapter.title ?? `[chapter ${hit.chapterId}]`,\n type: stream,\n };\n}\n\nasync function createSearchIndexRecords(\n document: ReadonlyDocument,\n progress?: SearchIndexProgressReporter,\n): Promise<SearchIndexInput> {\n const objectProperties: SearchIndexInput[\"objectProperties\"][number][] = [];\n const textSentences: SearchIndexInput[\"textSentences\"][number][] = [];\n const chapters = await listChapters(document);\n let chapterDone = 0;\n\n for (const chapter of chapters) {\n const title = chapter.title ?? `[chapter ${chapter.chapterId}]`;\n\n objectProperties.push({\n chapterId: chapter.chapterId,\n ownerId: String(chapter.chapterId),\n ownerKind: SEARCH_OBJECT_PROPERTY_OWNER_KIND.chapter,\n propertyKind: SEARCH_OBJECT_PROPERTY_KIND.title,\n text: title,\n });\n\n textSentences.push(\n ...(await createTextStreamSearchIndexRecords(\n document,\n chapter.chapterId,\n \"summary\",\n title,\n )),\n );\n textSentences.push(\n ...(await createTextStreamSearchIndexRecords(\n document,\n chapter.chapterId,\n \"source\",\n title,\n )),\n );\n chapterDone += 1;\n await progress?.({\n done: chapterDone,\n phase: \"collecting\",\n total: chapters.length,\n unit: \"chapter\",\n });\n }\n\n for (const node of await document.chunks.listAll()) {\n objectProperties.push({\n chapterId: node.sentenceId[0],\n ownerId: String(node.id),\n ownerKind: SEARCH_OBJECT_PROPERTY_OWNER_KIND.chunk,\n propertyKind: SEARCH_OBJECT_PROPERTY_KIND.label,\n text: node.label,\n });\n objectProperties.push({\n chapterId: node.sentenceId[0],\n ownerId: String(node.id),\n ownerKind: SEARCH_OBJECT_PROPERTY_OWNER_KIND.chunk,\n propertyKind: SEARCH_OBJECT_PROPERTY_KIND.content,\n text: node.content,\n });\n }\n\n for (const mention of await document.mentions.listAll()) {\n objectProperties.push({\n chapterId: mention.chapterId,\n ownerId: mention.qid,\n ownerKind: SEARCH_OBJECT_PROPERTY_OWNER_KIND.entity,\n propertyKind: SEARCH_OBJECT_PROPERTY_KIND.surface,\n text: mention.surface,\n });\n }\n\n return { objectProperties, textSentences };\n}\n\nasync function createTextStreamSearchIndexRecords(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n _title: string,\n): Promise<SearchIndexInput[\"textSentences\"]> {\n const index = await createTextStreamIndex(document, chapterId, stream);\n\n return index.sentences.map((sentence) => ({\n chapterId,\n kind:\n stream === \"source\"\n ? TEXT_SENTENCE_KIND.source\n : TEXT_SENTENCE_KIND.summary,\n sentenceIndex: sentence.globalIndex,\n text: sentence.text,\n wordsCount: sentence.wordsCount,\n }));\n}\n\nexport async function readArchiveText(\n document: ReadonlyDocument,\n id: string,\n): Promise<string> {\n const reference = parseArchiveReference(id);\n\n switch (reference.type) {\n case \"chapter\":\n throw new Error(\n `Chapter ${formatChapterId(reference.id)} is a scope URI, not a readable object.`,\n );\n case \"chapter-title\": {\n const chapter = await requireChapter(document, reference.id);\n\n return chapter.title ?? `[chapter ${reference.id}]`;\n }\n case \"fragment\":\n return (\n await readSourceFragment(\n document,\n reference.serialId,\n reference.fragmentId,\n )\n ).text;\n case \"summary\": {\n const summary = await readTextStreamText(\n document,\n reference.id,\n \"summary\",\n );\n\n if (summary.trim() === \"\") {\n throw new Error(`Summary ${formatSummaryId(reference.id)} is missing.`);\n }\n\n return summary;\n }\n case \"node\": {\n const { node } = await requireNode(document, reference.id);\n\n return node.content;\n }\n case \"meta\": {\n return formatMetaText(await document.readBookMeta());\n }\n }\n}\n\nexport async function readArchivePage(\n document: ReadonlyDocument,\n id: string,\n options: ArchivePageOptions = {},\n): Promise<ArchivePage> {\n if (isWikiGraphObjectUri(id)) {\n return await readWikiGraphPage(\n document,\n normalizeWikiGraphObjectUri(id),\n options,\n );\n }\n\n const reference = parseArchiveReference(id);\n\n switch (reference.type) {\n case \"chapter\": {\n throw new Error(\n `Chapter ${formatChapterId(reference.id)} is a scope URI, not a readable object. Use wikg://chapter/${reference.id}/title or wikg://chapter/${reference.id}/state.`,\n );\n }\n case \"chapter-title\": {\n const chapter = await requireChapter(document, reference.id);\n\n return {\n id: formatChapterTitleId(reference.id),\n title: chapter.title ?? `[chapter ${reference.id}]`,\n type: \"chapter-title\",\n };\n }\n case \"fragment\": {\n const [fragment, relatedNodes, fragmentIds] = await Promise.all([\n readSourceFragment(document, reference.serialId, reference.fragmentId),\n listFragmentNodes(document, reference.serialId, reference.fragmentId),\n document.getSerialFragments(reference.serialId).listFragmentIds(),\n ]);\n const fragmentIndex = fragmentIds.indexOf(reference.fragmentId);\n const previousFragmentId =\n fragmentIndex > 0 ? fragmentIds[fragmentIndex - 1] : undefined;\n const nextFragmentId =\n fragmentIndex >= 0 && fragmentIndex < fragmentIds.length - 1\n ? fragmentIds[fragmentIndex + 1]\n : undefined;\n\n return {\n fragment,\n id: fragment.id,\n nextFragmentId:\n nextFragmentId === undefined\n ? undefined\n : formatFragmentId(reference.serialId, nextFragmentId),\n nodes: relatedNodes,\n previousFragmentId:\n previousFragmentId === undefined\n ? undefined\n : formatFragmentId(reference.serialId, previousFragmentId),\n title: fragment.id,\n type: \"fragment\",\n };\n }\n case \"meta\":\n return {\n ...createMetaPage(await document.readBookMeta()),\n id: ARCHIVE_ROOT_ID,\n type: \"meta\",\n };\n case \"node\": {\n const { chapterId, node } = await requireNode(document, reference.id);\n const [neighbors, sourceFragments] = await Promise.all([\n listGraphNeighbors(document, chapterId, reference.id),\n readNodeSourceFragments(document, node),\n ]);\n const outgoing = neighbors.filter(\n (neighbor) => neighbor.direction === \"outgoing\",\n );\n const incoming = neighbors.filter(\n (neighbor) => neighbor.direction === \"incoming\",\n );\n\n return {\n generatedNodeSummary: node.content,\n id: formatNodeId(node.id),\n incoming,\n neighbors,\n outgoing,\n position: createNodePosition(node.sentenceIds),\n sourceFragments,\n title: node.label,\n type: \"node\",\n };\n }\n case \"summary\": {\n const chapter = await requireChapter(document, reference.id);\n const content = await readTextStreamText(\n document,\n reference.id,\n \"summary\",\n );\n\n if (content.trim() === \"\") {\n throw new Error(`Summary ${formatSummaryId(reference.id)} is missing.`);\n }\n\n return {\n content,\n id: formatSummaryId(reference.id),\n title: chapter.title ?? `[chapter ${reference.id}]`,\n type: \"summary\",\n };\n }\n }\n}\n\nasync function readWikiGraphPage(\n document: ReadonlyDocument,\n uri: string,\n options: ArchivePageOptions = {},\n): Promise<ArchivePage> {\n uri = normalizeWikiGraphObjectUri(uri);\n const reference = parseWikiGraphReference(uri);\n\n switch (reference.type) {\n case \"meta\":\n return await readArchivePage(document, ARCHIVE_ROOT_ID, options);\n case \"chapter\":\n throw new Error(\n `wikg://chapter/${reference.chapterId} is a scope URI, not a readable object. Use wikg://chapter/${reference.chapterId}/title or wikg://chapter/${reference.chapterId}/state.`,\n );\n case \"chapter-title\":\n return await readArchivePage(\n document,\n formatChapterTitleId(reference.chapterId),\n options,\n );\n case \"chapter-state\": {\n const details = await requireChapter(document, reference.chapterId);\n const targets = await createChapterState(document, details);\n\n return {\n id:\n reference.target === undefined\n ? `wikg://chapter/${reference.chapterId}/state`\n : `wikg://chapter/${reference.chapterId}/state/${reference.target}`,\n ...(reference.target === undefined\n ? { state: targets }\n : { target: reference.target, value: targets[reference.target] }),\n type: \"state\",\n };\n }\n case \"chapter-tree\":\n return {\n id: \"chapter-tree\",\n title: \"Chapter tree\",\n tree: await getChapterTree(document),\n type: \"chapter-tree\",\n };\n case \"entity\": {\n const mentions = filterMentionsByChapter(\n await document.mentions.listByQid(reference.qid),\n reference.chapterId,\n );\n\n if (mentions.length === 0) {\n throw new Error(`Entity ${uri} was not found in this archive.`);\n }\n\n return {\n evidence: await createMentionEvidencePreview(\n document,\n mentions,\n options.evidenceLimit,\n createEvidenceReadContext(),\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n ),\n id: uri,\n label: selectEntityLabel(mentions),\n labels: selectEntityLabels(mentions),\n mentionCount: mentions.length,\n qid: reference.qid,\n type: \"entity\",\n };\n }\n case \"entity-wikipage\":\n return {\n ...(await resolveEntityWikipage(reference.qid, options)),\n id: uri,\n type: \"entity-wikipage\",\n };\n case \"triple\": {\n const links = await filterMentionLinksByChapter(\n document,\n await document.mentionLinks.listByTriple({\n objectQid: reference.objectQid,\n predicate: reference.predicate,\n subjectQid: reference.subjectQid,\n }),\n reference.chapterId,\n );\n\n if (links.length === 0) {\n throw new Error(`Triple ${uri} was not found in this archive.`);\n }\n\n return {\n evidence: await createMentionLinkEvidencePreview(\n document,\n links,\n options.evidenceLimit,\n createEvidenceReadContext(),\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n ),\n id: uri,\n label: await createTriplePageLabel(document, reference),\n objectQid: reference.objectQid,\n predicate: reference.predicate,\n subjectQid: reference.subjectQid,\n type: \"triple\",\n };\n }\n case \"chunk\": {\n if (reference.chapterId !== undefined) {\n const { chapterId } = await requireNode(document, reference.id);\n\n if (chapterId !== reference.chapterId) {\n throw new Error(`Chunk ${uri} was not found in this archive.`);\n }\n }\n return await readArchivePage(\n document,\n formatNodeId(reference.id),\n options,\n );\n }\n case \"text-stream\":\n return {\n ...(options.backlinks === true\n ? { backlinks: await createTextStreamBacklinks(document, reference) }\n : {}),\n fragment: await createTextStreamRangeFragment(document, reference),\n id: uri,\n nextFragmentId: undefined,\n nodes: [],\n previousFragmentId: undefined,\n title: uri,\n type: \"fragment\",\n };\n }\n}\n\nexport async function listArchiveLinks(\n document: ReadonlyDocument,\n id: string,\n direction: \"backlinks\" | \"links\",\n): Promise<readonly GraphNeighbor[]> {\n return (await listAllArchiveLinks(document, id)).filter((neighbor) =>\n direction === \"links\"\n ? neighbor.direction === \"outgoing\"\n : neighbor.direction === \"incoming\",\n );\n}\n\nexport async function listAllArchiveLinks(\n document: ReadonlyDocument,\n id: string,\n): Promise<readonly GraphNeighbor[]> {\n if (isWikiGraphObjectUri(id)) {\n id = normalizeWikiGraphObjectUri(id);\n const reference = parseWikiGraphReference(id);\n\n if (reference.type !== \"chunk\") {\n return [];\n }\n\n const { chapterId } = await requireNode(document, reference.id);\n\n if (\n reference.chapterId !== undefined &&\n reference.chapterId !== chapterId\n ) {\n throw new Error(`Chunk ${id} was not found in this archive.`);\n }\n\n return await listGraphNeighbors(document, chapterId, reference.id);\n }\n\n const reference = parseArchiveReference(id);\n\n if (reference.type !== \"node\") {\n return [];\n }\n\n const { chapterId } = await requireNode(document, reference.id);\n return await listGraphNeighbors(document, chapterId, reference.id);\n}\n\nexport async function listRelatedArchiveObjects(\n document: ReadonlyDocument,\n id: string,\n options: ArchiveRelatedOptions = {},\n): Promise<ArchiveRelatedResult> {\n if (isWikiGraphObjectUri(id)) {\n return await listRelatedWikiGraphObjects(\n document,\n normalizeWikiGraphObjectUri(id),\n options,\n );\n }\n\n const reference = parseArchiveReference(id);\n if (reference.type !== \"node\") {\n rejectRelatedRole(options.role, id);\n return paginateRelatedItems([], options);\n }\n rejectRelatedRole(options.role, id);\n\n const documentOrders = await document.serials.listDocumentOrders();\n const { chapterId } = await requireNode(document, reference.id);\n const items = sortGraphNeighborsByListMode(\n await listGraphNeighbors(document, chapterId, reference.id),\n documentOrders,\n options.order ?? \"doc-asc\",\n ).map((neighbor) => ({\n id: formatNodeId(neighbor.node.id),\n label: neighbor.node.label,\n summary: neighbor.node.content,\n type: \"node\" as const,\n }));\n\n return await hydrateRelatedItemsEvidence(\n document,\n await filterAndSortChunkRelatedItemsByQuery(document, items, options.query),\n options,\n );\n}\n\nasync function listRelatedWikiGraphObjects(\n document: ReadonlyDocument,\n uri: string,\n options: ArchiveRelatedOptions,\n): Promise<ArchiveRelatedResult> {\n const reference = parseWikiGraphReference(uri);\n\n switch (reference.type) {\n case \"chapter\": {\n throw new Error(`Related is not available for scope URI: ${uri}`);\n }\n case \"chapter-title\":\n rejectRelatedRole(options.role, uri);\n return paginateRelatedItems([], options);\n case \"chunk\": {\n rejectRelatedRole(options.role, uri);\n const { chapterId } = await requireNode(document, reference.id);\n\n if (\n reference.chapterId !== undefined &&\n reference.chapterId !== chapterId\n ) {\n throw new Error(`Chunk ${uri} was not found in this archive.`);\n }\n\n const items = sortGraphNeighborsByListMode(\n await listGraphNeighbors(document, chapterId, reference.id),\n await document.serials.listDocumentOrders(),\n options.order ?? \"doc-asc\",\n ).map((neighbor) => ({\n id: formatNodeId(neighbor.node.id),\n label: neighbor.node.label,\n summary: neighbor.node.content,\n type: \"node\" as const,\n }));\n\n return await hydrateRelatedItemsEvidence(\n document,\n await filterAndSortChunkRelatedItemsByQuery(\n document,\n items,\n options.query,\n ),\n options,\n );\n }\n case \"text-stream\": {\n rejectRelatedQuery(options.query, uri);\n rejectRelatedRole(options.role, uri);\n const chapter = await requireChapter(document, reference.chapterId);\n\n return await hydrateRelatedItemsEvidence(\n document,\n [\n {\n id: formatChapterId(reference.chapterId),\n label: chapter.title ?? `[chapter ${reference.chapterId}]`,\n summary: `${chapter.stage}; ${chapter.fragmentCount} fragments`,\n type: \"chapter\",\n },\n ],\n options,\n );\n }\n case \"entity\":\n return await listRelatedEntityObjects(document, reference, options);\n case \"triple\":\n throw new Error(\n `Related is only available for chunk and entity objects: ${uri}`,\n );\n case \"chapter-tree\":\n case \"entity-wikipage\":\n case \"meta\":\n case \"chapter-state\":\n rejectRelatedQuery(options.query, uri);\n rejectRelatedRole(options.role, uri);\n return paginateRelatedItems([], options);\n }\n}\n\nasync function resolveEntityWikipage(\n qid: string,\n options: ArchivePageOptions,\n): Promise<{\n readonly en: ArchiveEntityWikipageLocale | null;\n readonly zh: ArchiveEntityWikipageLocale | null;\n}> {\n const [en, zh] = await Promise.all([\n resolveEntityWikipageLocale(qid, \"en\", \"enwiki\", options),\n resolveEntityWikipageLocale(qid, \"zh\", \"zhwiki\", options),\n ]);\n\n return { en, zh };\n}\n\nasync function resolveEntityWikipageLocale(\n qid: string,\n language: \"en\" | \"zh\",\n wiki: \"enwiki\" | \"zhwiki\",\n options: ArchivePageOptions,\n): Promise<ArchiveEntityWikipageLocale | null> {\n const resolver = await WikipageResolver.open({\n ...options.wikipageResolverOptions,\n language,\n wiki,\n });\n\n try {\n const [resolution] = await resolver.resolveQids([qid]);\n\n if (resolution === undefined) {\n return null;\n }\n\n return createEntityWikipageLocale(resolution, wiki);\n } finally {\n await resolver.close();\n }\n}\n\nfunction createEntityWikipageLocale(\n resolution: QidResolution,\n wiki: \"enwiki\" | \"zhwiki\",\n): ArchiveEntityWikipageLocale | null {\n const sitelink =\n resolution.sitelinks?.find((item) => item.wiki === wiki) ??\n (resolution.sitelink?.wiki === wiki ? resolution.sitelink : undefined);\n\n if (sitelink === undefined) {\n return null;\n }\n\n return {\n ...(resolution.description === undefined\n ? {}\n : { description: resolution.description }),\n title: sitelink.title,\n url: formatWikipediaPageUrl(sitelink),\n };\n}\n\nfunction formatWikipediaPageUrl(sitelink: WikipageSitelink): string {\n const language = sitelink.wiki === \"zhwiki\" ? \"zh\" : \"en\";\n\n return `https://${language}.wikipedia.org/wiki/${encodeURIComponent(\n sitelink.title.replace(/ /gu, \"_\"),\n )}`;\n}\n\nasync function listRelatedEntityObjects(\n document: ReadonlyDocument,\n reference: Extract<WikiGraphReference, { readonly type: \"entity\" }>,\n options: ArchiveRelatedOptions,\n): Promise<ArchiveRelatedResult> {\n const mentions = filterMentionsByChapter(\n await document.mentions.listByQid(reference.qid),\n reference.chapterId,\n );\n\n if (mentions.length === 0) {\n throw new Error(\n `Entity ${formatEntityUri(reference.qid)} was not found in this archive.`,\n );\n }\n\n const chapters = [\n ...new Set(mentions.map((mention) => mention.chapterId)),\n ].sort(compareNumbers);\n const role = options.role ?? \"any\";\n const triplesById = new Map<\n string,\n Extract<ArchiveListItem, { readonly type: \"triple\" }>\n >();\n\n for (const chapterId of chapters) {\n for (const link of await document.mentionLinks.listByChapter(chapterId)) {\n const [source, target] = await Promise.all([\n document.mentions.getById(link.sourceMentionId),\n document.mentions.getById(link.targetMentionId),\n ]);\n\n if (source === undefined || target === undefined) {\n continue;\n }\n if (\n !matchesRelatedEntityRole(source.qid, target.qid, reference.qid, role)\n ) {\n continue;\n }\n\n const id = formatTripleUri(source.qid, link.predicate, target.qid);\n\n const existing = triplesById.get(id);\n\n if (existing !== undefined) {\n triplesById.set(id, {\n ...existing,\n evidenceLinks: [...(existing.evidenceLinks ?? []), link],\n });\n continue;\n }\n\n triplesById.set(id, {\n evidenceLinks: [link],\n id,\n label: `${source.surface} ${link.predicate} ${target.surface}`,\n objectLabel: target.surface,\n objectQid: target.qid,\n predicate: link.predicate,\n subjectLabel: source.surface,\n subjectQid: source.qid,\n summary: `${source.qid} ${link.predicate} ${target.qid}`,\n type: \"triple\",\n });\n }\n }\n\n return await hydrateRelatedItemsEvidence(\n document,\n await filterAndSortEntityRelatedTriplesByQuery(\n document,\n sortRelatedItemsByListMode(\n [...triplesById.values()],\n options.order ?? \"doc-asc\",\n await document.serials.listDocumentOrders(),\n ),\n reference.qid,\n options.query,\n ),\n options,\n );\n}\n\nasync function filterAndSortChunkRelatedItemsByQuery(\n document: ReadonlyDocument,\n items: readonly ArchiveListItem[],\n query: string | undefined,\n): Promise<readonly ArchiveListItem[]> {\n if (query === undefined) {\n return items;\n }\n\n const indexResult = await queryRequiredSearchIndex(document, query, {\n types: [\"node\"],\n });\n\n if (indexResult === undefined) {\n return [];\n }\n\n const scoresByChunkId = new Map<number, number[]>();\n const allowedChunkIds = new Set(\n items.flatMap((item) => {\n if (item.type !== \"node\") {\n return [];\n }\n const reference = parseArchiveReference(item.id);\n\n return reference.type === \"node\" ? [reference.id] : [];\n }),\n );\n\n for (const hit of indexResult.objectHits) {\n if (\n hit.ownerKind !== SEARCH_OBJECT_PROPERTY_OWNER_KIND.chunk ||\n !allowedChunkIds.has(Number(hit.ownerId))\n ) {\n continue;\n }\n\n const chunkId = Number(hit.ownerId);\n const scores = scoresByChunkId.get(chunkId) ?? [];\n\n scores.push(hit.score);\n scoresByChunkId.set(chunkId, scores);\n }\n\n return items\n .flatMap((item) => {\n if (item.type !== \"node\") {\n return [];\n }\n const reference = parseArchiveReference(item.id);\n\n if (reference.type !== \"node\") {\n return [];\n }\n const scores = scoresByChunkId.get(reference.id);\n\n return scores === undefined\n ? []\n : [{ ...item, score: aggregateEvidenceScores(scores) }];\n })\n .sort(compareRelatedQueryItems);\n}\n\nasync function filterAndSortEntityRelatedTriplesByQuery(\n document: ReadonlyDocument,\n items: readonly ArchiveListItem[],\n anchorQid: string,\n query: string | undefined,\n): Promise<readonly ArchiveListItem[]> {\n if (query === undefined) {\n return items;\n }\n const scope = await createEntityRelatedQueryScope(document, items, anchorQid);\n\n const indexResult = await queryRequiredSearchIndex(document, query, {\n chapters: [...scope.chapterIds],\n types: [\"entity\", \"source\"],\n });\n\n if (indexResult === undefined) {\n return [];\n }\n\n const sentenceScores = new Map(\n indexResult.textHits\n .filter((hit) => hit.kind === TEXT_SENTENCE_KIND.source)\n .map(\n (hit) =>\n [\n createSentenceHitKey(hit.chapterId, hit.sentenceIndex),\n hit.score,\n ] as const,\n ),\n );\n const endpointScoresByKey = new Map<string, number[]>();\n\n for (const hit of indexResult.objectHits) {\n if (\n hit.ownerKind !== SEARCH_OBJECT_PROPERTY_OWNER_KIND.entity ||\n hit.ownerId === anchorQid ||\n hit.chapterId === undefined\n ) {\n continue;\n }\n\n const key = createEntityRelatedEndpointKey(hit.ownerId, hit.chapterId);\n const scores = endpointScoresByKey.get(key) ?? [];\n\n scores.push(hit.score);\n endpointScoresByKey.set(key, scores);\n }\n\n return items\n .flatMap((item) => {\n if (item.type !== \"triple\") {\n return [];\n }\n\n const scores = [\n ...[...(scope.endpointKeysByTripleId.get(item.id) ?? [])].flatMap(\n (key) => endpointScoresByKey.get(key) ?? [],\n ),\n ...(item.evidenceLinks ?? []).flatMap((link) =>\n link.evidenceSentenceIds.flatMap(([chapterId, sentenceIndex]) => {\n const score = sentenceScores.get(\n createSentenceHitKey(chapterId, sentenceIndex),\n );\n\n return score === undefined ? [] : [score];\n }),\n ),\n ];\n\n return scores.length === 0\n ? []\n : [{ ...item, score: aggregateEvidenceScores(scores) }];\n })\n .sort(compareRelatedQueryItems);\n}\n\nasync function createEntityRelatedQueryScope(\n document: ReadonlyDocument,\n items: readonly ArchiveListItem[],\n anchorQid: string,\n): Promise<{\n readonly chapterIds: ReadonlySet<number>;\n readonly endpointKeysByTripleId: ReadonlyMap<string, ReadonlySet<string>>;\n}> {\n const chapterIds = new Set<number>();\n const endpointKeysByTripleId = new Map<string, Set<string>>();\n const mentionCache = new Map<string, MentionRecord | undefined>();\n\n for (const item of items) {\n if (item.type !== \"triple\") {\n continue;\n }\n\n const endpointKeys = new Set<string>();\n\n for (const link of item.evidenceLinks ?? []) {\n for (const [chapterId] of link.evidenceSentenceIds) {\n chapterIds.add(chapterId);\n }\n\n const [source, target] = await Promise.all([\n getCachedMention(document, mentionCache, link.sourceMentionId),\n getCachedMention(document, mentionCache, link.targetMentionId),\n ]);\n\n for (const mention of [source, target]) {\n if (mention === undefined || mention.qid === anchorQid) {\n continue;\n }\n\n chapterIds.add(mention.chapterId);\n endpointKeys.add(\n createEntityRelatedEndpointKey(mention.qid, mention.chapterId),\n );\n }\n }\n\n endpointKeysByTripleId.set(item.id, endpointKeys);\n }\n\n return { chapterIds, endpointKeysByTripleId };\n}\n\nasync function getCachedMention(\n document: ReadonlyDocument,\n cache: Map<string, MentionRecord | undefined>,\n mentionId: string,\n): Promise<MentionRecord | undefined> {\n if (!cache.has(mentionId)) {\n cache.set(mentionId, await document.mentions.getById(mentionId));\n }\n\n return cache.get(mentionId);\n}\n\nfunction createEntityRelatedEndpointKey(\n qid: string,\n chapterId: number,\n): string {\n return `${qid}:${chapterId}`;\n}\n\nfunction compareRelatedQueryItems(\n left: ArchiveListItem,\n right: ArchiveListItem,\n): number {\n const scoreComparison = (right.score ?? 0) - (left.score ?? 0);\n\n if (scoreComparison !== 0) {\n return scoreComparison;\n }\n\n return compareListHits(\n createFindHitFromListItem(left),\n createFindHitFromListItem(right),\n \"doc-asc\",\n );\n}\n\nfunction sortRelatedItemsByListMode(\n items: readonly ArchiveListItem[],\n order: ArchiveFindOrder = \"doc-asc\",\n documentOrders?: ReadonlyMap<number, number>,\n): readonly ArchiveListItem[] {\n return [...items].sort((left, right) =>\n compareListHits(\n createFindHitFromListItem(left, documentOrders),\n createFindHitFromListItem(right, documentOrders),\n order,\n ),\n );\n}\n\nfunction sortGraphNeighborsByListMode(\n neighbors: readonly GraphNeighbor[],\n documentOrders: ReadonlyMap<number, number>,\n order: ArchiveFindOrder,\n): readonly GraphNeighbor[] {\n const direction = order === \"doc-asc\" ? 1 : -1;\n\n return [...neighbors].sort(\n (left, right) =>\n compareSentenceIds(\n getFirstGraphNodeSentenceId(left.node),\n getFirstGraphNodeSentenceId(right.node),\n documentOrders,\n ) * direction,\n );\n}\n\nfunction createFindHitFromListItem(\n item: ArchiveListItem,\n documentOrders?: ReadonlyMap<number, number>,\n): ArchiveFindHit {\n const position = createListItemPosition(item, documentOrders);\n const score =\n item.type === \"triple\" ? (item.evidenceLinks?.length ?? 0) : undefined;\n\n return {\n field: \"title\",\n id: item.id,\n ...(position === undefined ? {} : { position }),\n ...(score === undefined ? {} : { score }),\n snippet: item.summary,\n title: item.label,\n type: toFindObjectType(item.type),\n };\n}\n\nfunction toFindObjectType(\n type: ArchiveListItem[\"type\"],\n): ArchiveFindObjectType {\n switch (type) {\n case \"edge\":\n case \"state\":\n return \"meta\";\n default:\n return type;\n }\n}\n\nfunction createListItemPosition(\n item: ArchiveListItem,\n documentOrders?: ReadonlyMap<number, number>,\n): ArchiveFindPosition | undefined {\n if (item.type === \"triple\") {\n return createFirstMentionLinkPosition(\n item.evidenceLinks ?? [],\n documentOrders,\n );\n }\n\n return undefined;\n}\n\nfunction createFirstMentionLinkPosition(\n links: readonly MentionLinkRecord[],\n documentOrders?: ReadonlyMap<number, number>,\n): ArchiveFindPosition | undefined {\n const sentenceIds = links.flatMap((link) => link.evidenceSentenceIds);\n const [first] = sentenceIds.sort((left, right) =>\n compareSentenceIds(left, right, documentOrders),\n );\n\n return first === undefined\n ? undefined\n : createSentencePosition(first, documentOrders);\n}\n\nfunction getFirstGraphNodeSentenceId(node: GraphNode): SentenceId {\n return (\n [...node.sentenceIds].sort(compareSentenceIds)[0] ?? [\n Number.MAX_SAFE_INTEGER,\n Number.MAX_SAFE_INTEGER,\n ]\n );\n}\n\nfunction matchesRelatedEntityRole(\n subjectQid: string,\n objectQid: string,\n qid: string,\n role: ArchiveRelatedRole,\n): boolean {\n const isSubject = subjectQid === qid;\n const isObject = objectQid === qid;\n const isSelf = isSubject && isObject;\n\n switch (role) {\n case \"any\":\n return isSubject || isObject;\n case \"subject\":\n return isSubject && !isSelf;\n case \"object\":\n return isObject && !isSelf;\n case \"self\":\n return isSelf;\n }\n}\n\nfunction rejectRelatedRole(\n role: ArchiveRelatedRole | undefined,\n id: string,\n): void {\n if (role !== undefined && role !== \"any\") {\n throw new Error(`--role is only available for entity related: ${id}`);\n }\n}\n\nfunction rejectRelatedQuery(query: string | undefined, id: string): void {\n if (query !== undefined) {\n throw new Error(\n `Related query is only available for chunk and entity: ${id}`,\n );\n }\n}\n\nasync function hydrateRelatedItemsEvidence(\n document: ReadonlyDocument,\n items: readonly ArchiveListItem[],\n options: ArchiveRelatedOptions,\n): Promise<ArchiveRelatedResult> {\n const page = paginateRelatedItems(items, options);\n\n if (options.evidenceLimit === undefined) {\n return {\n ...page,\n items: page.items.map((item) => {\n if (item.type !== \"triple\") {\n return item;\n }\n const { evidenceLinks: _evidenceLinks, ...publicItem } = item;\n return publicItem;\n }),\n };\n }\n\n const context = createEvidenceReadContext();\n const evidenceLimit = options.evidenceLimit;\n\n return {\n ...page,\n items: await Promise.all(\n page.items.map(async (item) => {\n if (item.evidence !== undefined) {\n return item;\n }\n if (item.type === \"triple\") {\n const evidence = await createMentionLinkEvidencePreview(\n document,\n item.evidenceLinks ?? [],\n evidenceLimit,\n context,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n );\n const { evidenceLinks: _evidenceLinks, ...publicItem } = item;\n\n return { ...publicItem, evidence };\n }\n if (item.type === \"node\") {\n const reference = parseArchiveReference(item.id);\n\n if (reference.type !== \"node\") {\n return item;\n }\n\n const { node } = await requireNode(document, reference.id);\n return {\n ...item,\n evidence: await createSourceEvidencePreview(\n document,\n createNodeEvidenceRanges(node),\n evidenceLimit,\n context,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n ),\n };\n }\n return item;\n }),\n ),\n };\n}\n\nfunction paginateRelatedItems(\n items: readonly ArchiveListItem[],\n options: ArchiveRelatedOptions,\n): ArchiveRelatedResult {\n const limit = options.limit ?? 20;\n const offset = parseRelatedCursor(options.cursor);\n const pageItems = items.slice(offset, offset + limit);\n const nextOffset = offset + pageItems.length;\n\n return {\n items: pageItems,\n limit,\n nextCursor: nextOffset >= items.length ? null : String(nextOffset),\n };\n}\n\nfunction parseRelatedCursor(cursor: string | undefined): number {\n if (cursor === undefined) {\n return 0;\n }\n\n if (!/^(0|[1-9][0-9]*)$/u.test(cursor)) {\n throw new Error(`Invalid related cursor: ${cursor}`);\n }\n\n return Number(cursor);\n}\n\nexport async function listArchiveEvidence(\n document: ReadonlyDocument,\n uri: string,\n options: ArchiveEvidenceOptions = {},\n): Promise<ArchiveEvidence> {\n const reference = parseWikiGraphReference(uri);\n\n switch (reference.type) {\n case \"chapter\":\n case \"chapter-title\":\n case \"chapter-state\":\n case \"chapter-tree\":\n case \"entity-wikipage\":\n case \"meta\":\n case \"text-stream\":\n throw new Error(`Evidence is not available for ${uri}.`);\n case \"chunk\": {\n const { chapterId, node } = await requireNode(document, reference.id);\n\n if (\n reference.chapterId !== undefined &&\n reference.chapterId !== chapterId\n ) {\n throw new Error(`Chunk ${uri} was not found in this archive.`);\n }\n\n return await createSourceEvidencePage(\n document,\n createNodeEvidenceRanges(node),\n options,\n );\n }\n case \"entity\":\n return await createSourceEvidencePage(\n document,\n await createMentionEvidenceRanges(\n document,\n filterMentionsByChapter(\n await document.mentions.listByQid(reference.qid),\n reference.chapterId,\n ),\n ),\n options,\n );\n case \"triple\":\n return await createSourceEvidencePage(\n document,\n createMentionLinkEvidenceRanges(\n document,\n await filterMentionLinksByChapter(\n document,\n await document.mentionLinks.listByTriple({\n objectQid: reference.objectQid,\n predicate: reference.predicate,\n subjectQid: reference.subjectQid,\n }),\n reference.chapterId,\n ),\n ),\n options,\n );\n }\n}\n\nexport async function packArchiveContext(\n document: ReadonlyDocument,\n id: string,\n budget: number,\n): Promise<ArchivePack> {\n validatePackReference(id);\n\n const [anchor, related] = await Promise.all([\n readArchivePage(document, id, { evidenceLimit: 3 }),\n listRelatedArchiveObjects(document, id, { evidenceLimit: 3 }),\n ]);\n\n return {\n anchor,\n budget,\n related: related.items,\n };\n}\n\nfunction validatePackReference(id: string): void {\n const reference = parseWikiGraphReference(id);\n\n switch (reference.type) {\n case \"chunk\":\n case \"entity\":\n return;\n case \"chapter\":\n case \"chapter-title\":\n case \"chapter-state\":\n case \"chapter-tree\":\n case \"entity-wikipage\":\n case \"meta\":\n case \"text-stream\":\n throw new Error(\n `Pack is only available for chunk and entity objects: ${id}`,\n );\n }\n}\n\nexport function formatChapterId(chapterId: number): string {\n return `chapter:${chapterId}`;\n}\n\nexport function formatChapterTitleId(chapterId: number): string {\n return `chapter-title:${chapterId}`;\n}\n\nexport function formatEdgeId(edge: ReadingEdgeRecord): string {\n return `edge:${edge.fromId}->${edge.toId}`;\n}\n\nexport function formatNodeId(nodeId: number): string {\n return `node:${nodeId}`;\n}\n\nexport function formatSummaryId(chapterId: number): string {\n return `summary:${chapterId}`;\n}\n\nexport function formatFragmentId(serialId: number, fragmentId: number): string {\n return `fragment:${serialId}:${fragmentId}`;\n}\n\nfunction findEntities(\n search: LexicalQuery,\n context: {\n readonly mentions: readonly MentionRecord[];\n },\n): readonly ArchiveFindHit[] {\n const candidatesByQid = new Map<\n string,\n Array<{\n readonly hit: ArchiveFindHit;\n readonly mention: MentionRecord;\n }>\n >();\n\n for (const { match, mention } of createMentionLexicalHits(\n context.mentions,\n search,\n )) {\n const candidates = candidatesByQid.get(mention.qid) ?? [];\n\n candidates.push({\n hit: {\n chapter: mention.chapterId,\n field: \"title\" as const,\n id: `wikg://entity/${mention.qid}`,\n ...createFindMatchFields(match),\n position: {\n chapter: mention.chapterId,\n sentence: mention.sentenceIndex ?? 0,\n },\n snippet: mention.note ?? mention.surface,\n title: mention.surface,\n type: \"entity\" as const,\n },\n mention,\n });\n candidatesByQid.set(mention.qid, candidates);\n }\n\n return [...candidatesByQid.values()].map((candidates) => {\n const rankedCandidates = [...candidates].sort((left, right) => {\n const scoreComparison = (right.hit.score ?? 0) - (left.hit.score ?? 0);\n\n if (scoreComparison !== 0) {\n return scoreComparison;\n }\n if (left.hit.position === undefined) {\n return right.hit.position === undefined ? 0 : 1;\n }\n if (right.hit.position === undefined) {\n return -1;\n }\n return compareArchivePositions(left.hit.position, right.hit.position);\n });\n const [best] = rankedCandidates;\n\n if (best === undefined) {\n throw new Error(\"Internal error: entity search candidate is empty.\");\n }\n\n return {\n ...best.hit,\n score: aggregateEvidenceScores(\n rankedCandidates.map((candidate) => candidate.hit.score ?? 0),\n ),\n evidenceMentions: rankedCandidates.map((candidate) => ({\n match: {\n matchCount: candidate.hit.matchCount ?? 0,\n matchedTerms: candidate.hit.matchedTerms ?? [],\n missingTerms: candidate.hit.missingTerms ?? [],\n score: candidate.hit.score ?? 0,\n },\n mention: candidate.mention,\n })),\n };\n });\n}\n\nasync function findTriples(\n document: ReadonlyDocument,\n search: LexicalQuery,\n context: {\n readonly mentions: readonly MentionRecord[];\n },\n): Promise<readonly ArchiveFindHit[]> {\n const mentionsById = new Map(\n context.mentions.map((mention) => [mention.id, mention]),\n );\n const hitsByTriple = new Map<string, ArchiveFindHit[]>();\n\n for (const chapter of await listChapters(document)) {\n for (const link of await document.mentionLinks.listByChapter(\n chapter.chapterId,\n )) {\n const [source, target] = await Promise.all([\n getMentionForTripleSearch(document, mentionsById, link.sourceMentionId),\n getMentionForTripleSearch(document, mentionsById, link.targetMentionId),\n ]);\n\n if (source === undefined || target === undefined) {\n continue;\n }\n\n const text = `${source.surface} ${link.predicate} ${target.surface}`;\n const match = scoreLexicalText(text, search);\n\n if (match === undefined) {\n continue;\n }\n\n const id = formatTripleUri(source.qid, link.predicate, target.qid);\n const next = {\n chapter: source.chapterId,\n evidenceLinks: [link],\n field: \"content\" as const,\n id,\n ...createFindMatchFields(match),\n position: {\n chapter: source.chapterId,\n sentence: source.sentenceIndex ?? 0,\n },\n snippet: link.note ?? text,\n title: text,\n triple: {\n objectLabel: target.surface,\n predicate: link.predicate,\n subjectLabel: source.surface,\n },\n type: \"triple\" as const,\n };\n const values = hitsByTriple.get(id) ?? [];\n\n values.push(next);\n hitsByTriple.set(id, values);\n }\n }\n\n return [...hitsByTriple.values()].map(groupTripleEvidenceHits);\n}\n\nasync function listAllMentions(\n document: ReadonlyDocument,\n): Promise<readonly MentionRecord[]> {\n return await document.mentions.listAll();\n}\n\nfunction groupTripleEvidenceHits(\n evidenceHits: readonly ArchiveFindHit[],\n): ArchiveFindHit {\n const rankedHits = [...evidenceHits].sort(compareFindEvidenceHits);\n const [best] = rankedHits;\n\n if (best === undefined) {\n throw new Error(\"Internal error: triple search candidate is empty.\");\n }\n\n return {\n ...best,\n evidenceLinks: rankedHits.flatMap((hit) => hit.evidenceLinks ?? []),\n score: aggregateEvidenceScores(rankedHits.map((hit) => hit.score ?? 0)),\n };\n}\n\nfunction listEntityCollection(\n mentions: readonly MentionRecord[],\n documentOrders?: ReadonlyMap<number, number>,\n): readonly ArchiveFindHit[] {\n const mentionsByQid = new Map<string, MentionRecord[]>();\n\n for (const mention of mentions) {\n const values = mentionsByQid.get(mention.qid) ?? [];\n\n values.push(mention);\n mentionsByQid.set(mention.qid, values);\n }\n\n return [...mentionsByQid.entries()].map(([qid, qidMentions]) => {\n const [first] = qidMentions.sort(compareMentions);\n\n if (first === undefined) {\n throw new Error(\"Internal error: entity collection candidate is empty.\");\n }\n\n return {\n chapter: first.chapterId,\n evidenceMentions: qidMentions.map((mention) =>\n createUnscoredEntityEvidenceMention(mention),\n ),\n field: \"title\",\n id: `wikg://entity/${qid}`,\n position: {\n chapter: first.chapterId,\n documentOrder: documentOrders?.get(first.chapterId) ?? first.chapterId,\n sentence: first.sentenceIndex ?? 0,\n },\n score: qidMentions.length,\n snippet: `${qidMentions.length} mentions`,\n title: selectEntityLabel(qidMentions),\n type: \"entity\",\n };\n });\n}\n\nfunction listTextStreamSentenceCollection(\n index: ArchiveTextStreamIndex,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n title: string,\n documentOrder?: number,\n): readonly ArchiveFindHit[] {\n return index.sentences.map((sentence) => ({\n chapter: chapterId,\n field: stream,\n id: formatTextStreamRangeUri(\n chapterId,\n stream,\n sentence.globalIndex,\n sentence.globalIndex,\n ),\n position: {\n chapter: chapterId,\n documentOrder: documentOrder ?? chapterId,\n fragment: sentence.fragmentId,\n sentence: sentence.localIndex,\n },\n snippet: createSnippet(sentence.text),\n title,\n type: stream === \"source\" ? \"source\" : \"summary\",\n }));\n}\n\nasync function listTripleCollection(\n document: ReadonlyDocument,\n chapterFilter?: ReadonlySet<number>,\n documentOrders?: ReadonlyMap<number, number>,\n): Promise<readonly ArchiveFindHit[]> {\n const hitsById = new Map<string, ArchiveFindHit>();\n\n for (const chapter of filterChapters(\n await listChapters(document),\n chapterFilter,\n )) {\n for (const link of await document.mentionLinks.listByChapter(\n chapter.chapterId,\n )) {\n const [source, target] = await Promise.all([\n document.mentions.getById(link.sourceMentionId),\n document.mentions.getById(link.targetMentionId),\n ]);\n\n if (source === undefined || target === undefined) {\n continue;\n }\n\n const id = formatTripleUri(source.qid, link.predicate, target.qid);\n const existing = hitsById.get(id);\n\n if (existing !== undefined) {\n hitsById.set(id, {\n ...existing,\n evidenceLinks: [...(existing.evidenceLinks ?? []), link],\n score: (existing.evidenceLinks?.length ?? 0) + 1,\n });\n continue;\n }\n\n hitsById.set(id, {\n chapter: source.chapterId,\n evidenceLinks: [link],\n field: \"title\",\n id,\n position: {\n chapter: source.chapterId,\n documentOrder:\n documentOrders?.get(source.chapterId) ?? source.chapterId,\n sentence: source.sentenceIndex ?? 0,\n },\n score: 1,\n snippet: `${source.surface} ${link.predicate} ${target.surface}`,\n title: `${source.qid} ${link.predicate} ${target.qid}`,\n triple: {\n objectLabel: target.surface,\n predicate: link.predicate,\n subjectLabel: source.surface,\n },\n type: \"triple\",\n });\n }\n }\n\n return [...hitsById.values()];\n}\n\nfunction compareMentions(left: MentionRecord, right: MentionRecord): number {\n return (\n compareNumbers(left.chapterId, right.chapterId) ||\n compareNumbers(left.sentenceIndex ?? 0, right.sentenceIndex ?? 0) ||\n left.id.localeCompare(right.id)\n );\n}\n\nasync function getMentionForTripleSearch(\n document: ReadonlyDocument,\n cache: Map<string, MentionRecord>,\n mentionId: string,\n): Promise<MentionRecord | undefined> {\n const cached = cache.get(mentionId);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const mention = await document.mentions.getById(mentionId);\n\n if (mention !== undefined) {\n cache.set(mentionId, mention);\n }\n\n return mention;\n}\n\nasync function hydrateFindHitEvidence(\n document: ReadonlyDocument,\n hits: readonly ArchiveFindHit[],\n options: {\n readonly evidenceLimit?: number;\n readonly order?: ArchiveFindOrder;\n readonly sessionId?: string;\n readonly sourceContext?: number;\n } = {},\n): Promise<readonly ArchiveFindHit[]> {\n const evidenceContext = createEvidenceReadContext();\n\n const hydrated = await Promise.all(\n hits.map(async (rawHit) => {\n const displayHit = await hydrateEntityDisplayHit(document, rawHit);\n const hit = await hydrateTextStreamHitContext(\n document,\n displayHit,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n evidenceContext,\n );\n\n if (hit.evidence !== undefined && hit.evidence.sources.length > 0) {\n return hit;\n }\n if (\n hit.evidence !== undefined &&\n hit.type === \"entity\" &&\n options.sessionId !== undefined\n ) {\n return await hydrateEntitySessionHitEvidence(\n document,\n hit,\n options.sessionId,\n options.evidenceLimit,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n evidenceContext,\n );\n }\n if (hit.evidenceLinks !== undefined) {\n if (options.evidenceLimit === undefined) {\n const { evidenceLinks: _evidenceLinks, ...publicHit } = hit;\n\n return publicHit;\n }\n\n const evidence = await createMentionLinkEvidencePreview(\n document,\n hit.evidenceLinks,\n options.evidenceLimit,\n evidenceContext,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n );\n const { evidenceLinks: _evidenceLinks, ...publicHit } = hit;\n\n return {\n ...publicHit,\n evidence,\n };\n }\n if (hit.evidenceMentions === undefined) {\n return hit;\n }\n if (options.evidenceLimit === undefined) {\n const { evidenceMentions: _evidenceMentions, ...publicHit } = hit;\n\n return publicHit;\n }\n\n const evidence = await createMentionEvidencePreview(\n document,\n hit.evidenceMentions.map((item) => item.mention),\n options.evidenceLimit,\n evidenceContext,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n options.order ?? \"doc-asc\",\n );\n const { evidenceMentions: _evidenceMentions, ...publicHit } = hit;\n\n return {\n ...publicHit,\n evidence,\n };\n }),\n );\n\n return await coalesceTextStreamFindHits(document, hydrated, evidenceContext);\n}\n\nasync function hydrateEntityDisplayHit(\n document: ReadonlyDocument,\n hit: ArchiveFindHit,\n): Promise<ArchiveFindHit> {\n if (hit.type !== \"entity\") {\n return hit;\n }\n\n const qid = parseEntityQid(hit.id);\n\n if (qid === undefined) {\n return hit;\n }\n if (hit.title !== qid && hit.snippet !== qid) {\n return hit;\n }\n\n const mentions = await document.mentions.listByQid(qid);\n const [first] = mentions.sort(compareMentions);\n\n if (first === undefined) {\n return hit;\n }\n\n return {\n ...hit,\n chapter: first.chapterId,\n position: {\n chapter: first.chapterId,\n sentence: first.sentenceIndex ?? 0,\n },\n snippet: hit.snippet === qid ? (first.note ?? first.surface) : hit.snippet,\n title: hit.title === qid ? selectEntityLabel(mentions) : hit.title,\n };\n}\n\nasync function hydrateTextStreamHitContext(\n document: ReadonlyDocument,\n hit: ArchiveFindHit,\n sourceContext: number,\n context: EvidenceReadContext,\n): Promise<ArchiveFindHit> {\n if (sourceContext <= 0 || (hit.type !== \"source\" && hit.type !== \"summary\")) {\n return hit;\n }\n if (hit.matchCount === undefined && hit.matchedTerms === undefined) {\n return hit;\n }\n\n const reference = parseTextStreamHitReference(hit.id);\n\n if (reference === undefined) {\n return hit;\n }\n\n const range = await readTextStreamRange(\n document,\n reference.chapterId,\n reference.stream,\n reference.startSentenceIndex - sourceContext,\n reference.endSentenceIndex + sourceContext,\n context,\n );\n\n return {\n ...hit,\n id: range.id,\n snippet: range.text,\n };\n}\n\nfunction parseTextStreamHitReference(\n uri: string,\n): Extract<WikiGraphReference, { readonly type: \"text-stream\" }> | undefined {\n if (!isWikiGraphObjectUri(uri)) {\n return undefined;\n }\n\n try {\n const reference = parseWikiGraphReference(normalizeWikiGraphObjectUri(uri));\n\n return reference.type === \"text-stream\" ? reference : undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function coalesceTextStreamFindHits(\n document: ReadonlyDocument,\n hits: readonly ArchiveFindHit[],\n context: EvidenceReadContext,\n): Promise<readonly ArchiveFindHit[]> {\n const entries: Array<\n | { readonly hit: ArchiveFindHit; readonly type: \"hit\" }\n | { range: TextStreamHitRange; readonly type: \"range\" }\n > = [];\n\n for (const hit of hits) {\n const range = parseSearchTextStreamHitRange(hit);\n\n if (range === undefined || hit.backlinks !== undefined) {\n entries.push({ hit, type: \"hit\" });\n continue;\n }\n\n const overlappingIndexes = entries\n .map((entry, index) =>\n entry.type === \"range\" &&\n areMergeableTextStreamHitRanges(entry.range, range)\n ? index\n : -1,\n )\n .filter((index) => index >= 0);\n\n if (overlappingIndexes.length === 0) {\n entries.push({ range, type: \"range\" });\n continue;\n }\n\n const firstIndex = overlappingIndexes[0] ?? 0;\n const ranges = overlappingIndexes.flatMap((index) => {\n const entry = entries[index];\n\n return entry?.type === \"range\" ? [entry.range] : [];\n });\n\n entries[firstIndex] = {\n range: await mergeTextStreamHitRangeGroup(\n document,\n [...ranges, range],\n context,\n ),\n type: \"range\",\n };\n\n for (const index of overlappingIndexes.slice(1).reverse()) {\n entries.splice(index, 1);\n }\n }\n\n return entries.map((entry) =>\n entry.type === \"range\" ? entry.range.hit : entry.hit,\n );\n}\n\nfunction parseSearchTextStreamHitRange(\n hit: ArchiveFindHit,\n): TextStreamHitRange | undefined {\n if (hit.type !== \"source\" && hit.type !== \"summary\") {\n return undefined;\n }\n if (hit.matchCount === undefined && hit.matchedTerms === undefined) {\n return undefined;\n }\n\n const reference = parseTextStreamHitReference(hit.id);\n\n if (reference === undefined) {\n return undefined;\n }\n\n return {\n chapterId: reference.chapterId,\n endSentenceIndex: reference.endSentenceIndex,\n hit,\n startSentenceIndex: reference.startSentenceIndex,\n stream: reference.stream,\n };\n}\n\nfunction areMergeableTextStreamHitRanges(\n left: TextStreamHitRange,\n right: TextStreamHitRange,\n): boolean {\n return (\n left.chapterId === right.chapterId &&\n left.stream === right.stream &&\n right.startSentenceIndex <= left.endSentenceIndex + 1 &&\n left.startSentenceIndex <= right.endSentenceIndex + 1\n );\n}\n\nasync function mergeTextStreamHitRangeGroup(\n document: ReadonlyDocument,\n ranges: readonly TextStreamHitRange[],\n context: EvidenceReadContext,\n): Promise<TextStreamHitRange> {\n const [representative] = ranges;\n\n if (representative === undefined) {\n throw new Error(\n \"Internal error: cannot merge empty text stream hit group.\",\n );\n }\n\n const startSentenceIndex = Math.min(\n ...ranges.map((range) => range.startSentenceIndex),\n );\n const endSentenceIndex = Math.max(\n ...ranges.map((range) => range.endSentenceIndex),\n );\n const text = await readTextStreamRange(\n document,\n representative.chapterId,\n representative.stream,\n startSentenceIndex,\n endSentenceIndex,\n context,\n );\n const hits = ranges.map((range) => range.hit);\n const scores = hits\n .map((hit) => hit.score)\n .filter((score): score is number => score !== undefined);\n\n return {\n chapterId: representative.chapterId,\n endSentenceIndex,\n hit: {\n ...representative.hit,\n id: text.id,\n matchCount: Math.max(...hits.map((hit) => hit.matchCount ?? 0)),\n matchedTerms: mergeStringLists(\n hits.flatMap((hit) => hit.matchedTerms ?? []),\n ),\n missingTerms: mergeStringLists(\n hits.flatMap((hit) => hit.missingTerms ?? []),\n ),\n ...(scores.length === 0 ? {} : { score: Math.max(...scores) }),\n snippet: text.text,\n },\n startSentenceIndex,\n stream: representative.stream,\n };\n}\n\nasync function hydrateFindResultBacklinks(\n document: ReadonlyDocument,\n result: ArchiveFindResult,\n options: Pick<ArchiveFindOptions, \"backlinks\">,\n): Promise<ArchiveFindResult> {\n return {\n ...result,\n items: await hydrateFindHitBacklinks(document, result.items, options),\n };\n}\n\nasync function hydrateFindHitBacklinks(\n document: ReadonlyDocument,\n hits: readonly ArchiveFindHit[],\n options:\n | Pick<ArchiveFindOptions, \"backlinks\">\n | Pick<ArchiveCollectionOptions, \"backlinks\">,\n): Promise<readonly ArchiveFindHit[]> {\n if (options.backlinks !== true) {\n return hits;\n }\n\n return await Promise.all(\n hits.map(async (hit) => {\n const reference = parseSourceBacklinkReference(hit.id);\n\n if (reference === undefined) {\n return hit;\n }\n\n return {\n ...hit,\n backlinks: await createTextStreamBacklinks(document, reference),\n };\n }),\n );\n}\n\nfunction parseSourceBacklinkReference(\n uri: string,\n): Extract<WikiGraphReference, { readonly type: \"text-stream\" }> | undefined {\n if (!isWikiGraphObjectUri(uri)) {\n return undefined;\n }\n\n try {\n const reference = parseWikiGraphReference(normalizeWikiGraphObjectUri(uri));\n\n return reference.type === \"text-stream\" && reference.stream === \"source\"\n ? reference\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction createFindEvidenceHydrationOptions(\n options: ArchiveFindOptions,\n sessionId?: string,\n): {\n readonly evidenceLimit?: number;\n readonly order?: ArchiveFindOrder;\n readonly sessionId?: string;\n readonly sourceContext?: number;\n} {\n return {\n ...(options.evidenceLimit === undefined\n ? {}\n : { evidenceLimit: options.evidenceLimit }),\n order: options.order ?? \"doc-asc\",\n ...(sessionId === undefined ? {} : { sessionId }),\n ...(options.sourceContext === undefined\n ? {}\n : { sourceContext: options.sourceContext }),\n };\n}\n\nasync function createTextStreamBacklinks(\n document: ReadonlyDocument,\n reference: Extract<WikiGraphReference, { readonly type: \"text-stream\" }>,\n): Promise<ArchiveBacklinks> {\n if (reference.stream !== \"source\") {\n return createEmptyBacklinks();\n }\n\n const sentenceKeys = await createTextStreamRangeSentenceKeySet(\n document,\n reference,\n );\n const [chunks, mentions, links] = await Promise.all([\n createChunkBacklinkHits(document, sentenceKeys),\n createEntityBacklinkHits(document, sentenceKeys),\n createTripleBacklinkHits(document, reference.chapterId, sentenceKeys),\n ]);\n\n return {\n chunks: createBacklinkBucket(chunks),\n entities: createBacklinkBucket(mentions),\n triples: createBacklinkBucket(links),\n };\n}\n\nfunction createEmptyBacklinks(): ArchiveBacklinks {\n return {\n chunks: createBacklinkBucket([]),\n entities: createBacklinkBucket([]),\n triples: createBacklinkBucket([]),\n };\n}\n\nfunction createBacklinkBucket(\n hits: readonly ArchiveFindHit[],\n): ArchiveBacklinkBucket {\n const sorted = [...hits].sort((left, right) =>\n compareListHits(left, right, \"doc-asc\"),\n );\n\n return {\n items: sorted,\n limit: sorted.length,\n nextCursor: null,\n };\n}\n\nasync function createTextStreamRangeSentenceKeySet(\n document: ReadonlyDocument,\n reference: Extract<WikiGraphReference, { readonly type: \"text-stream\" }>,\n): Promise<ReadonlySet<string>> {\n const index = await createTextStreamIndex(\n document,\n reference.chapterId,\n reference.stream,\n );\n const lastSentenceIndex = Math.max(0, index.sentences.length - 1);\n const start = clampInteger(\n reference.startSentenceIndex,\n 0,\n lastSentenceIndex,\n );\n const end = clampInteger(\n reference.endSentenceIndex,\n start,\n lastSentenceIndex,\n );\n const keys = new Set<string>();\n\n for (const sentence of index.sentences.slice(start, end + 1)) {\n keys.add(formatSentenceKey(reference.chapterId, sentence.globalIndex));\n }\n\n return keys;\n}\n\nasync function createChunkBacklinkHits(\n document: ReadonlyDocument,\n sentenceKeys: ReadonlySet<string>,\n): Promise<readonly ArchiveFindHit[]> {\n return (await document.chunks.listAll())\n .filter((chunk) =>\n chunk.sentenceIds.some((sentenceId) =>\n sentenceKeys.has(formatSentenceIdKey(sentenceId)),\n ),\n )\n .map((chunk) => {\n const position = createNodePosition(chunk.sentenceIds);\n\n return {\n chapter: chunk.sentenceId[0],\n field: \"content\" as const,\n id: formatNodeId(chunk.id),\n ...(position === undefined ? {} : { position }),\n snippet: createSnippet(chunk.content),\n title: chunk.label,\n type: \"node\" as const,\n };\n });\n}\n\nasync function createEntityBacklinkHits(\n document: ReadonlyDocument,\n sentenceKeys: ReadonlySet<string>,\n): Promise<readonly ArchiveFindHit[]> {\n return listEntityCollection(\n (await listAllMentions(document)).filter((mention) =>\n sentenceKeys.has(\n formatSentenceKey(mention.chapterId, mention.sentenceIndex ?? 0),\n ),\n ),\n );\n}\n\nasync function createTripleBacklinkHits(\n document: ReadonlyDocument,\n chapterId: number,\n sentenceKeys: ReadonlySet<string>,\n): Promise<readonly ArchiveFindHit[]> {\n const hitsById = new Map<string, ArchiveFindHit>();\n\n for (const link of await document.mentionLinks.listByChapter(chapterId)) {\n const evidenceSentenceIds = link.evidenceSentenceIds.filter((sentenceId) =>\n sentenceKeys.has(formatSentenceIdKey(sentenceId)),\n );\n\n if (evidenceSentenceIds.length === 0) {\n continue;\n }\n\n const [source, target] = await Promise.all([\n document.mentions.getById(link.sourceMentionId),\n document.mentions.getById(link.targetMentionId),\n ]);\n\n if (source === undefined || target === undefined) {\n continue;\n }\n\n const id = formatTripleUri(source.qid, link.predicate, target.qid);\n const existing = hitsById.get(id);\n const evidenceLink = { ...link, evidenceSentenceIds };\n\n if (existing !== undefined) {\n hitsById.set(id, {\n ...existing,\n evidenceLinks: [...(existing.evidenceLinks ?? []), evidenceLink],\n score: (existing.evidenceLinks?.length ?? 0) + 1,\n });\n continue;\n }\n\n hitsById.set(id, {\n chapter: source.chapterId,\n evidenceLinks: [evidenceLink],\n field: \"title\",\n id,\n position: createSentencePosition(\n [...evidenceSentenceIds].sort(compareSentenceIds)[0] ?? [\n source.chapterId,\n source.sentenceIndex ?? 0,\n ],\n ),\n score: 1,\n snippet: `${source.surface} ${link.predicate} ${target.surface}`,\n title: `${source.qid} ${link.predicate} ${target.qid}`,\n triple: {\n objectLabel: target.surface,\n predicate: link.predicate,\n subjectLabel: source.surface,\n },\n type: \"triple\",\n });\n }\n\n return [...hitsById.values()];\n}\n\nfunction formatSentenceIdKey(sentenceId: SentenceId): string {\n return formatSentenceKey(sentenceId[0], sentenceId[1]);\n}\n\nfunction formatSentenceKey(chapterId: number, sentenceIndex: number): string {\n return `${chapterId}:${sentenceIndex}`;\n}\n\nfunction createUnscoredEntityEvidenceMention(\n mention: MentionRecord,\n): EntityEvidenceMention {\n return {\n match: {\n matchCount: 0,\n matchedTerms: [],\n missingTerms: [],\n score: 0,\n },\n mention,\n };\n}\n\nasync function hydrateEntitySessionHitEvidence(\n document: ReadonlyDocument,\n hit: ArchiveFindHit,\n sessionId: string,\n evidenceLimit: number | undefined,\n sourceContext: number,\n context: EvidenceReadContext = createEvidenceReadContext(),\n): Promise<ArchiveFindHit> {\n if (evidenceLimit === undefined) {\n const { evidence: _evidence, ...publicHit } = hit;\n\n return publicHit;\n }\n\n const qid = parseEntityQid(hit.id);\n\n if (qid === undefined) {\n return hit;\n }\n\n const mentionsByQid = await document.mentions.listByQid(qid);\n const mentionsById = new Map(\n mentionsByQid.map((mention) => [mention.id, mention]),\n );\n const allMentions = (\n await readEntitySearchEvidenceMentions(\n sessionId,\n mentionsByQid.map((mention) => mention.id),\n 10_000,\n )\n ).flatMap((hit) => {\n const mention = mentionsById.get(hit.mentionId);\n\n return mention === undefined ? [] : [mention];\n });\n const ranges = await createMentionEvidenceRanges(document, allMentions);\n const mergedRanges = await createExpandedSourceEvidenceRanges(\n document,\n ranges,\n sourceContext,\n context,\n );\n const sources = await Promise.all(\n mergedRanges\n .slice(0, evidenceLimit)\n .map(\n async (range) =>\n await createSourceEvidenceItem(\n document,\n range.chapterId,\n range.startSentenceIndex,\n range.endSentenceIndex,\n context,\n ),\n ),\n );\n\n return {\n ...hit,\n ...(allMentions[0] === undefined\n ? {}\n : {\n chapter: allMentions[0].chapterId,\n position: {\n chapter: allMentions[0].chapterId,\n sentence: allMentions[0].sentenceIndex ?? 0,\n },\n snippet: allMentions[0].note ?? allMentions[0].surface,\n title: allMentions[0].surface,\n }),\n evidence: {\n nextCursor:\n sources.length < mergedRanges.length\n ? encodeFindCursor(sources.length)\n : null,\n shown: sources.length,\n sources,\n total: mergedRanges.length,\n },\n };\n}\n\nfunction formatTripleUri(\n subjectQid: string,\n predicate: string,\n objectQid: string,\n): string {\n return `wikg://triple/${subjectQid}/${encodeURIComponent(predicate)}/${objectQid}`;\n}\n\nfunction formatEntityUri(qid: string): string {\n return `wikg://entity/${qid}`;\n}\n\nfunction isEntityOnlySearch(options: ArchiveFindOptions): boolean {\n return isEntitySearchTypes(options.types ?? null);\n}\n\nfunction isEntitySearchTypes(types: readonly string[] | null): boolean {\n return types !== null && types.length === 1 && types[0] === \"entity\";\n}\n\nfunction assertSearchCursorTypesMatch(\n requestedTypes: readonly string[] | undefined,\n sessionTypes: readonly string[] | null,\n): void {\n if (requestedTypes === undefined) {\n return;\n }\n if (requestedTypes.length !== (sessionTypes?.length ?? 0)) {\n throw new Error(\"Search cursor does not match the requested result types.\");\n }\n const sessionTypeSet = new Set(sessionTypes ?? []);\n\n if (requestedTypes.some((type) => !sessionTypeSet.has(type))) {\n throw new Error(\"Search cursor does not match the requested result types.\");\n }\n}\n\nfunction createEntitySearchCacheInput(\n hits: readonly ArchiveFindHit[],\n indexResult?: SearchIndexQueryResult,\n): {\n readonly entityHits: readonly SearchEntityHitInput[];\n readonly evidenceEvents: readonly SearchEvidenceHitEventInput[];\n} {\n const evidenceEvents: SearchEvidenceHitEventInput[] = [];\n const evidenceScoresByQid = new Map<string, number[]>();\n const propertyScoresByQid = new Map<string, number[]>();\n\n for (const hit of indexResult?.objectHits ?? []) {\n if (hit.ownerKind !== SEARCH_OBJECT_PROPERTY_OWNER_KIND.entity) {\n continue;\n }\n\n const scores = propertyScoresByQid.get(hit.ownerId) ?? [];\n\n scores.push(hit.score);\n propertyScoresByQid.set(hit.ownerId, scores);\n }\n\n for (const hit of hits) {\n if (hit.type !== \"entity\" || hit.evidenceMentions === undefined) {\n continue;\n }\n\n const qid = parseEntityQid(hit.id);\n\n if (qid === undefined) {\n continue;\n }\n\n const scores = evidenceScoresByQid.get(qid) ?? [];\n\n for (const evidenceMention of hit.evidenceMentions) {\n const sentenceIndex = evidenceMention.mention.sentenceIndex;\n\n if (sentenceIndex === undefined) {\n continue;\n }\n\n const score = evidenceMention.match.score ?? hit.score ?? 0;\n\n evidenceEvents.push({\n chapterId: evidenceMention.mention.chapterId,\n evidenceId: evidenceMention.mention.id,\n evidenceKind: SEARCH_EVIDENCE_KIND.mention,\n score,\n sentenceIndex,\n });\n scores.push(score);\n }\n\n evidenceScoresByQid.set(qid, scores);\n }\n\n const qids = new Set([\n ...evidenceScoresByQid.keys(),\n ...propertyScoresByQid.keys(),\n ]);\n\n return {\n entityHits: [...qids].map((qid) => ({\n evidenceTopScores: evidenceScoresByQid.get(qid) ?? [],\n propertyTopScores: propertyScoresByQid.get(qid) ?? [],\n qid,\n })),\n evidenceEvents,\n };\n}\n\nasync function createSentenceEvidenceSearchCacheInput(\n document: ReadonlyDocument,\n indexResult: SearchIndexQueryResult | undefined,\n options: ArchiveFindOptions,\n): Promise<{\n readonly chunkHits: readonly SearchChunkHitInput[];\n readonly entityHits: readonly SearchEntityHitInput[];\n readonly evidenceEvents: readonly SearchEvidenceHitEventInput[];\n readonly tripleHits: readonly SearchTripleHitInput[];\n}> {\n if (indexResult === undefined || isTextOnlySearch(options)) {\n return {\n chunkHits: [],\n entityHits: [],\n evidenceEvents: [],\n tripleHits: [],\n };\n }\n\n const sourceHits = indexResult.textHits.filter(\n (hit) => hit.kind === TEXT_SENTENCE_KIND.source,\n );\n\n if (sourceHits.length === 0) {\n return {\n chunkHits: [],\n entityHits: [],\n evidenceEvents: [],\n tripleHits: [],\n };\n }\n\n const sourceHitScores = new Map<string, number>();\n const chapterIds = new Set<number>();\n\n for (const hit of sourceHits) {\n sourceHitScores.set(\n createSentenceHitKey(hit.chapterId, hit.sentenceIndex),\n hit.score,\n );\n chapterIds.add(hit.chapterId);\n }\n\n const evidenceEvents: SearchEvidenceHitEventInput[] = [];\n const entityEvidenceScoresByQid = new Map<string, number[]>();\n const chunkEvidenceScoresById = new Map<number, number[]>();\n const tripleEvidenceScoresByKey = new Map<\n string,\n {\n readonly objectQid: string;\n readonly predicate: string;\n readonly scores: number[];\n readonly subjectQid: string;\n }\n >();\n\n for (const chapterId of chapterIds) {\n for (const mention of await document.mentions.listByChapter(chapterId)) {\n if (mention.sentenceIndex === undefined) {\n continue;\n }\n\n const score = sourceHitScores.get(\n createSentenceHitKey(chapterId, mention.sentenceIndex),\n );\n\n if (score === undefined) {\n continue;\n }\n\n evidenceEvents.push({\n chapterId,\n evidenceId: mention.id,\n evidenceKind: SEARCH_EVIDENCE_KIND.mention,\n score,\n sentenceIndex: mention.sentenceIndex,\n });\n const scores = entityEvidenceScoresByQid.get(mention.qid) ?? [];\n\n scores.push(score);\n entityEvidenceScoresByQid.set(mention.qid, scores);\n }\n\n for (const chunk of await document.chunks.listBySerial(chapterId)) {\n for (const [, sentenceIndex] of chunk.sentenceIds) {\n const score = sourceHitScores.get(\n createSentenceHitKey(chapterId, sentenceIndex),\n );\n\n if (score === undefined) {\n continue;\n }\n\n evidenceEvents.push({\n chapterId,\n evidenceId: String(chunk.id),\n evidenceKind: SEARCH_EVIDENCE_KIND.chunk,\n score,\n sentenceIndex,\n });\n const scores = chunkEvidenceScoresById.get(chunk.id) ?? [];\n\n scores.push(score);\n chunkEvidenceScoresById.set(chunk.id, scores);\n }\n }\n\n const mentionCache = new Map<string, MentionRecord>();\n\n for (const link of await document.mentionLinks.listByChapter(chapterId)) {\n const [source, target] = await Promise.all([\n getMentionForTripleSearch(document, mentionCache, link.sourceMentionId),\n getMentionForTripleSearch(document, mentionCache, link.targetMentionId),\n ]);\n\n if (source === undefined || target === undefined) {\n continue;\n }\n\n for (const [\n evidenceChapterId,\n sentenceIndex,\n ] of link.evidenceSentenceIds) {\n const score = sourceHitScores.get(\n createSentenceHitKey(evidenceChapterId, sentenceIndex),\n );\n\n if (score === undefined) {\n continue;\n }\n\n evidenceEvents.push({\n chapterId: evidenceChapterId,\n evidenceId: link.id,\n evidenceKind: SEARCH_EVIDENCE_KIND.mentionLink,\n score,\n sentenceIndex,\n });\n\n const key = formatTripleUri(source.qid, link.predicate, target.qid);\n const current = tripleEvidenceScoresByKey.get(key) ?? {\n objectQid: target.qid,\n predicate: link.predicate,\n scores: [],\n subjectQid: source.qid,\n };\n\n current.scores.push(score);\n tripleEvidenceScoresByKey.set(key, current);\n }\n }\n }\n\n return {\n chunkHits: [...chunkEvidenceScoresById.entries()].map(\n ([chunkId, scores]) => ({\n chunkId,\n evidenceTopScores: scores,\n }),\n ),\n entityHits: [...entityEvidenceScoresByQid.entries()].map(\n ([qid, scores]) => ({\n evidenceTopScores: scores,\n qid,\n }),\n ),\n evidenceEvents,\n tripleHits: [...tripleEvidenceScoresByKey.values()].map((hit) => ({\n evidenceTopScores: hit.scores,\n objectQid: hit.objectQid,\n predicate: hit.predicate,\n subjectQid: hit.subjectQid,\n })),\n };\n}\n\nfunction createSentenceHitKey(\n chapterId: number,\n sentenceIndex: number,\n): string {\n return `${chapterId}:${sentenceIndex}`;\n}\n\nfunction parseEntityQid(id: string): string | undefined {\n const normalized = normalizeWikiGraphObjectUri(id);\n const prefix = `${WIKI_GRAPH_URI_PREFIX}entity/`;\n\n return normalized.startsWith(prefix)\n ? normalized.slice(prefix.length)\n : undefined;\n}\n\nfunction filterLexicalHitsByMatch(\n hits: readonly ArchiveFindHit[],\n search: LexicalQuery,\n match: ArchiveFindMatch,\n): readonly ArchiveFindHit[] {\n if (match === \"any\") {\n return hits;\n }\n\n const requiredTerms = [...search.phrases];\n\n if (requiredTerms.length === 0) {\n return hits;\n }\n\n return hits.filter((hit) =>\n requiredTerms.every((term) => hit.matchedTerms?.includes(term) === true),\n );\n}\n\nasync function findChapters(\n document: ReadonlyDocument,\n search: ArchiveTextSearch,\n): Promise<readonly ArchiveFindHit[]> {\n const hits: ArchiveFindHit[] = [];\n\n for (const chapter of await listChapters(document)) {\n const title = chapter.title ?? `[chapter ${chapter.chapterId}]`;\n const titleMatch = matchText(title, search);\n\n if (titleMatch !== undefined) {\n hits.push({\n chapter: chapter.chapterId,\n field: \"title\",\n id: formatChapterTitleId(chapter.chapterId),\n ...createFindMatchFields(titleMatch),\n position: {\n chapter: chapter.chapterId,\n },\n snippet: title,\n title,\n type: \"chapter-title\",\n });\n }\n\n hits.push(\n ...(await findTextStreamSentences(\n document,\n chapter.chapterId,\n \"summary\",\n title,\n search,\n )),\n );\n\n hits.push(\n ...(await findTextStreamSentences(\n document,\n chapter.chapterId,\n \"source\",\n title,\n search,\n )),\n );\n }\n\n return hits;\n}\n\nasync function findTextStreamSentences(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n title: string,\n search: ArchiveTextSearch,\n): Promise<readonly ArchiveFindHit[]> {\n const index = await createTextStreamIndex(document, chapterId, stream);\n\n return index.sentences.flatMap((sentence) => {\n const match = matchText(sentence.text, search);\n\n if (match === undefined) {\n return [];\n }\n\n return [\n {\n chapter: chapterId,\n field: stream,\n id: formatTextStreamRangeUri(\n chapterId,\n stream,\n sentence.globalIndex,\n sentence.globalIndex,\n ),\n ...createFindMatchFields(match),\n position: {\n chapter: chapterId,\n fragment: sentence.fragmentId,\n sentence: sentence.localIndex,\n },\n snippet: createSnippet(sentence.text, getSnippetNeedle(match)),\n title,\n type: stream === \"source\" ? (\"source\" as const) : (\"summary\" as const),\n },\n ];\n });\n}\n\nasync function readSourceFragment(\n document: ReadonlyDocument,\n serialId: number,\n fragmentId: number,\n): Promise<ArchiveSourceFragment> {\n const fragment = await document\n .getSerialFragments(serialId)\n .getFragment(fragmentId);\n const text = fragment.sentences.map((sentence) => sentence.text).join(\"\\n\");\n\n return {\n fragmentId,\n id: formatFragmentId(serialId, fragmentId),\n preview: createSnippet(text),\n sentenceCount: fragment.sentences.length,\n text,\n wordsCount: fragment.sentences.reduce(\n (total, sentence) => total + sentence.wordsCount,\n 0,\n ),\n };\n}\n\nasync function createTextStreamRangeFragment(\n document: ReadonlyDocument,\n reference: Extract<WikiGraphReference, { readonly type: \"text-stream\" }>,\n): Promise<ArchiveSourceFragment> {\n const range = await readTextStreamRange(\n document,\n reference.chapterId,\n reference.stream,\n reference.startSentenceIndex,\n reference.endSentenceIndex,\n );\n\n return {\n fragmentId: range.startSentenceIndex,\n id: range.id,\n preview: createSnippet(range.text),\n sentenceCount: range.endSentenceIndex - range.startSentenceIndex + 1,\n text: range.text,\n wordsCount: countWords(range.text),\n };\n}\n\nasync function readTextStreamRange(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n startSentenceIndex: number,\n endSentenceIndex: number,\n context: EvidenceReadContext = createEvidenceReadContext(),\n): Promise<{\n readonly endSentenceIndex: number;\n readonly id: string;\n readonly startSentenceIndex: number;\n readonly text: string;\n}> {\n const index = await getTextStreamIndex(document, chapterId, stream, context);\n if (index.sentences.length === 0) {\n throw new Error(\n `Chapter ${formatChapterId(chapterId)} has no ${stream} text.`,\n );\n }\n\n const lastSentenceIndex = index.sentences.length - 1;\n if (startSentenceIndex > lastSentenceIndex) {\n throw new Error(\n `${stream} range ${formatTextStreamRangeUri(chapterId, stream, startSentenceIndex, endSentenceIndex)} is out of bounds. Last sentence number is ${lastSentenceIndex + 1}.`,\n );\n }\n\n const start = clampInteger(startSentenceIndex, 0, lastSentenceIndex);\n const end = clampInteger(endSentenceIndex, start, lastSentenceIndex);\n const sentences = index.sentences.slice(start, end + 1);\n const text =\n normalizeRenderedTextStreamRange(\n await readTextStreamRawRange(document, chapterId, stream, start, end),\n ) ?? joinTextStreamSentences(sentences);\n\n return {\n endSentenceIndex: end,\n id: formatTextStreamRangeUri(chapterId, stream, start, end),\n startSentenceIndex: start,\n text,\n };\n}\n\nasync function readTextStreamText(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n): Promise<string> {\n const serial = getTextStreamSerial(document, chapterId, stream);\n const text = await serial.readText?.();\n\n if (text !== undefined) {\n return text;\n }\n\n const index = await createTextStreamIndex(document, chapterId, stream);\n\n return joinTextStreamSentences(index.sentences);\n}\n\nasync function readTextStreamRawRange(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n startSentenceIndex: number,\n endSentenceIndex: number,\n): Promise<string | undefined> {\n const serial = getTextStreamSerial(document, chapterId, stream);\n\n return await serial.readTextInRange?.(startSentenceIndex, endSentenceIndex);\n}\n\nfunction getTextStreamSerial(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n): ReadonlySerialTextStream {\n return stream === \"summary\"\n ? document.getSummaryFragments(chapterId)\n : document.getSerialFragments(chapterId);\n}\n\nfunction joinTextStreamSentences(\n sentences: readonly Pick<ArchiveTextStreamSentence, \"text\">[],\n): string {\n return sentences.map((sentence) => sentence.text).join(\"\");\n}\n\nfunction normalizeRenderedTextStreamRange(\n text: string | undefined,\n): string | undefined {\n return text\n ?.replace(/^(?:[^\\S\\r\\n]*(?:\\r\\n|\\n|\\r))+/u, \"\")\n .replace(/(?:(?:\\r\\n|\\n|\\r)[^\\S\\r\\n]*)+$/u, \"\");\n}\n\nasync function getTextStreamIndex(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n context: EvidenceReadContext = createEvidenceReadContext(),\n): Promise<ArchiveTextStreamIndex> {\n const key = `${chapterId}:${stream}`;\n let index = context.streamIndexes.get(key);\n\n if (index === undefined) {\n index = createTextStreamIndex(document, chapterId, stream);\n context.streamIndexes.set(key, index);\n }\n\n return await index;\n}\n\nasync function createTextStreamIndex(\n document: ReadonlyDocument,\n chapterId: number,\n stream: ArchiveTextStreamKind,\n): Promise<ArchiveTextStreamIndex> {\n if (stream === \"summary\") {\n const fragments = document.getSummaryFragments(chapterId);\n const sentences: ArchiveTextStreamSentence[] = [];\n\n for (const fragmentId of await fragments.listFragmentIds()) {\n const fragment = await fragments.getFragment(fragmentId);\n\n for (let index = 0; index < fragment.sentences.length; index += 1) {\n const sentence = fragment.sentences[index];\n\n if (sentence === undefined) {\n continue;\n }\n\n sentences.push({\n fragmentId,\n globalIndex: sentences.length,\n localIndex: index,\n text: sentence.text,\n wordsCount: sentence.wordsCount,\n });\n }\n }\n\n return { sentences };\n }\n\n const fragments = document.getSerialFragments(chapterId);\n const sentences: ArchiveTextStreamSentence[] = [];\n\n for (const fragmentId of await fragments.listFragmentIds()) {\n const fragment = await fragments.getFragment(fragmentId);\n\n for (let index = 0; index < fragment.sentences.length; index += 1) {\n const sentence = fragment.sentences[index];\n\n if (sentence === undefined) {\n continue;\n }\n\n sentences.push({\n fragmentId,\n globalIndex: sentences.length,\n localIndex: index,\n text: sentence.text,\n wordsCount: sentence.wordsCount,\n });\n }\n }\n\n return { sentences };\n}\n\nfunction selectEntityLabel(mentions: readonly MentionRecord[]): string {\n return selectEntityLabels(mentions)[0] ?? mentions[0]?.qid ?? \"[entity]\";\n}\n\nfunction selectEntityLabels(\n mentions: readonly MentionRecord[],\n): readonly string[] {\n const counts = new Map<string, number>();\n\n for (const mention of mentions) {\n counts.set(mention.surface, (counts.get(mention.surface) ?? 0) + 1);\n }\n\n return [...counts.entries()]\n .sort((left, right) => {\n const countComparison = right[1] - left[1];\n\n return countComparison === 0\n ? left[0].localeCompare(right[0])\n : countComparison;\n })\n .map(([label]) => label);\n}\n\nasync function createTriplePageLabel(\n document: ReadonlyDocument,\n reference: Extract<WikiGraphReference, { readonly type: \"triple\" }>,\n): Promise<string> {\n const [subjectMentions, objectMentions] = await Promise.all([\n document.mentions.listByQid(reference.subjectQid),\n document.mentions.listByQid(reference.objectQid),\n ]);\n const scopedSubjectMentions = filterMentionsByChapter(\n subjectMentions,\n reference.chapterId,\n );\n const scopedObjectMentions = filterMentionsByChapter(\n objectMentions,\n reference.chapterId,\n );\n const subjectLabel =\n scopedSubjectMentions.length === 0\n ? reference.subjectQid\n : selectEntityLabel(scopedSubjectMentions);\n const objectLabel =\n scopedObjectMentions.length === 0\n ? reference.objectQid\n : selectEntityLabel(scopedObjectMentions);\n\n return `${subjectLabel}(${reference.subjectQid}) ${reference.predicate} ${objectLabel}(${reference.objectQid})`;\n}\n\nfunction countWords(text: string): number {\n const trimmed = text.trim();\n\n return trimmed === \"\" ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nasync function listFragmentNodes(\n document: ReadonlyDocument,\n chapterId: number,\n fragmentId: number,\n): Promise<readonly ArchiveNodeLabel[]> {\n return (\n await document.chunks.listBySentenceStartIndexes(chapterId, [fragmentId])\n )\n .map(createPositionedNodeLabel)\n .sort(comparePositionedNodeLabels)\n .map(({ label }) => label);\n}\n\nfunction createNodeLabel(node: ChunkRecord): ArchiveNodeLabel {\n return {\n id: formatNodeId(node.id),\n title: node.label,\n };\n}\n\nfunction createPositionedNodeLabel(node: ChunkRecord): PositionedNodeLabel {\n return {\n label: createNodeLabel(node),\n position: createNodePosition(node.sentenceIds),\n };\n}\n\nfunction comparePositionedNodeLabels(\n left: PositionedNodeLabel,\n right: PositionedNodeLabel,\n): number {\n if (left.position !== undefined && right.position !== undefined) {\n return compareArchivePositions(left.position, right.position);\n }\n\n if (left.position !== undefined) {\n return -1;\n }\n\n if (right.position !== undefined) {\n return 1;\n }\n\n return left.label.id.localeCompare(right.label.id);\n}\n\nfunction findMeta(\n meta: BookMeta | undefined,\n search: ArchiveTextSearch,\n): readonly ArchiveFindHit[] {\n if (meta === undefined) {\n return [];\n }\n\n const fields = [\n meta.title,\n ...meta.authors,\n meta.description,\n meta.publisher,\n ].filter(isDefined);\n const content = fields.join(\"\\n\");\n const contentMatch = matchText(content, search);\n\n if (contentMatch === undefined) {\n return [];\n }\n\n return [\n {\n field: \"metadata\",\n id: ARCHIVE_ROOT_ID,\n ...createFindMatchFields(contentMatch),\n snippet: createSnippet(content, getSnippetNeedle(contentMatch)),\n title: meta.title ?? \"Archive metadata\",\n type: \"meta\",\n },\n ];\n}\n\nasync function findNodes(\n document: ReadonlyDocument,\n search: ArchiveTextSearch,\n): Promise<readonly ArchiveFindHit[]> {\n const hits: ArchiveFindHit[] = [];\n\n for (const node of await document.chunks.listAll()) {\n const position = createNodePosition(node.sentenceIds);\n const labelMatch = matchText(node.label, search);\n\n if (labelMatch !== undefined) {\n hits.push({\n chapter: node.sentenceId[0],\n field: \"title\",\n id: formatNodeId(node.id),\n ...createFindMatchFields(labelMatch),\n ...(position === undefined ? {} : { position }),\n snippet: node.label,\n title: node.label,\n type: \"node\",\n });\n }\n const contentMatch = matchText(node.content, search);\n\n if (contentMatch !== undefined) {\n hits.push({\n chapter: node.sentenceId[0],\n field: \"content\",\n id: formatNodeId(node.id),\n ...createFindMatchFields(contentMatch),\n ...(position === undefined ? {} : { position }),\n snippet: createSnippet(node.content, getSnippetNeedle(contentMatch)),\n title: node.label,\n type: \"node\",\n });\n }\n }\n\n return hits;\n}\n\nasync function requireChapter(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<ChapterEntry> {\n const chapter = (await listChapters(document)).find(\n (entry) => entry.chapterId === chapterId,\n );\n\n if (chapter === undefined) {\n throw new Error(`Chapter ${formatChapterId(chapterId)} does not exist.`);\n }\n\n return chapter;\n}\n\nasync function createChapterState(\n document: ReadonlyDocument,\n chapter: ChapterEntry,\n): Promise<ChapterState> {\n const serial = await document.serials.getById(chapter.chapterId);\n\n return {\n source: chapter.stage === \"planned\" ? \"missing\" : \"ready\",\n \"reading-graph\":\n chapter.stage === \"graphed\" || chapter.stage === \"summarized\"\n ? \"ready\"\n : \"missing\",\n \"reading-summary\": chapter.stage === \"summarized\" ? \"ready\" : \"missing\",\n \"knowledge-graph\":\n serial?.knowledgeGraphReady === true ? \"ready\" : \"missing\",\n };\n}\n\nfunction formatChapterStateSummary(state: ChapterState): string {\n return [\n `source:${state.source}`,\n `reading-graph:${state[\"reading-graph\"]}`,\n `reading-summary:${state[\"reading-summary\"]}`,\n `knowledge-graph:${state[\"knowledge-graph\"]}`,\n ].join(\" \");\n}\n\nasync function requireNode(\n document: ReadonlyDocument,\n nodeId: number,\n): Promise<{\n readonly chapterId: number;\n readonly node: GraphNode;\n}> {\n const chunk = await document.chunks.getById(nodeId);\n\n if (chunk === undefined) {\n throw new Error(`Node ${formatNodeId(nodeId)} does not exist.`);\n }\n\n const chapterId = chunk.sentenceId[0];\n\n return {\n chapterId,\n node: await getGraphNode(document, chapterId, nodeId),\n };\n}\n\nasync function readNodeSourceFragments(\n document: ReadonlyDocument,\n node: GraphNode,\n): Promise<readonly ArchiveNodeSourceFragment[]> {\n const fragmentIds = await collectNodeSourceFragmentIds(document, node);\n\n return await Promise.all(\n fragmentIds.map(async ([chapterId, fragmentId]) => {\n const fragment = await readSourceFragment(\n document,\n chapterId,\n fragmentId,\n );\n const index = await createTextStreamIndex(document, chapterId, \"source\");\n const fragmentSentences = index.sentences.filter(\n (sentence) => sentence.fragmentId === fragmentId,\n );\n const firstSentence = fragmentSentences[0];\n const lastSentence = fragmentSentences[fragmentSentences.length - 1];\n const text = truncateSourceExcerpt(fragment.text);\n\n return {\n id:\n firstSentence === undefined || lastSentence === undefined\n ? fragment.id\n : formatTextStreamRangeUri(\n chapterId,\n \"source\",\n firstSentence.globalIndex,\n lastSentence.globalIndex,\n ),\n text,\n truncated: text.length < fragment.text.length,\n };\n }),\n );\n}\n\nasync function collectNodeSourceFragmentIds(\n document: ReadonlyDocument,\n node: Pick<GraphNode, \"sentenceIds\">,\n): Promise<readonly (readonly [number, number])[]> {\n const seen = new Set<string>();\n const fragmentIds: (readonly [number, number])[] = [];\n const indexes = new Map<number, Promise<ArchiveTextStreamIndex>>();\n\n for (const [chapterId, sentenceIndex] of node.sentenceIds) {\n let index = indexes.get(chapterId);\n\n if (index === undefined) {\n index = createTextStreamIndex(document, chapterId, \"source\");\n indexes.set(chapterId, index);\n }\n\n const sentence = (await index).sentences[sentenceIndex];\n\n if (sentence === undefined) {\n continue;\n }\n\n const fragmentId = sentence.fragmentId;\n const key = `${chapterId}:${fragmentId}`;\n\n if (!seen.has(key)) {\n seen.add(key);\n fragmentIds.push([chapterId, fragmentId]);\n }\n }\n\n return fragmentIds;\n}\n\nfunction truncateSourceExcerpt(text: string): string {\n return text.length <= 1200 ? text : `${text.slice(0, 1200)}...`;\n}\n\nfunction createNodeEvidenceRanges(node: Pick<GraphNode, \"sentenceIds\">): Array<{\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly startSentenceIndex: number;\n}> {\n const ranges = new Map<string, [number, number]>();\n\n for (const [chapterId, sentenceIndex] of node.sentenceIds) {\n const key = `${chapterId}`;\n const current = ranges.get(key);\n\n if (current === undefined) {\n ranges.set(key, [sentenceIndex, sentenceIndex]);\n } else {\n ranges.set(key, [\n Math.min(current[0], sentenceIndex),\n Math.max(current[1], sentenceIndex),\n ]);\n }\n }\n\n return [...ranges.entries()].map(([key, [start, end]]) => {\n const chapterId = Number(key);\n return {\n chapterId,\n endSentenceIndex: end,\n startSentenceIndex: start,\n };\n });\n}\n\nasync function createMentionEvidencePreview(\n document: ReadonlyDocument,\n mentions: readonly MentionRecord[],\n limit = 3,\n context: EvidenceReadContext = createEvidenceReadContext(),\n sourceContext = DEFAULT_SOURCE_CONTEXT,\n order: ArchiveFindOrder = \"doc-asc\",\n): Promise<ArchiveFindEvidencePreview> {\n return await createSourceEvidencePreview(\n document,\n await createMentionEvidenceRanges(document, mentions),\n limit,\n context,\n sourceContext,\n order,\n );\n}\n\nasync function createMentionEvidenceRanges(\n document: ReadonlyDocument,\n mentions: readonly MentionRecord[],\n): Promise<\n Array<{\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly startSentenceIndex: number;\n }>\n> {\n return await Promise.all(\n mentions.map(async (mention) => {\n const startSentenceIndex =\n mention.sentenceIndex ??\n (await findSentenceIndexAtOffset(\n document,\n mention.chapterId,\n mention.rangeStart,\n ));\n const endSentenceIndex =\n mention.sentenceIndex ??\n (await findSentenceIndexAtOffset(\n document,\n mention.chapterId,\n Math.max(0, mention.rangeEnd - 1),\n ));\n\n return {\n chapterId: mention.chapterId,\n endSentenceIndex,\n startSentenceIndex,\n };\n }),\n );\n}\n\nasync function createMentionLinkEvidencePreview(\n document: ReadonlyDocument,\n links: readonly MentionLinkRecord[],\n limit = 3,\n context: EvidenceReadContext = createEvidenceReadContext(),\n sourceContext = DEFAULT_SOURCE_CONTEXT,\n order: ArchiveFindOrder = \"doc-asc\",\n): Promise<ArchiveFindEvidencePreview> {\n return await createSourceEvidencePreview(\n document,\n createMentionLinkEvidenceRanges(document, links),\n limit,\n context,\n sourceContext,\n order,\n );\n}\n\nfunction createMentionLinkEvidenceRanges(\n _document: ReadonlyDocument,\n links: readonly MentionLinkRecord[],\n): Array<{\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly startSentenceIndex: number;\n}> {\n return links.flatMap((link) =>\n link.evidenceSentenceIds.map(([chapterId, sentenceIndex]) => ({\n chapterId,\n endSentenceIndex: sentenceIndex,\n startSentenceIndex: sentenceIndex,\n })),\n );\n}\n\nfunction filterMentionsByChapter(\n mentions: readonly MentionRecord[],\n chapterId: number | undefined,\n): readonly MentionRecord[] {\n return chapterId === undefined\n ? mentions\n : mentions.filter((mention) => mention.chapterId === chapterId);\n}\n\nfunction filterMentionsByChapterSet(\n mentions: readonly MentionRecord[],\n chapterFilter: ReadonlySet<number> | undefined,\n): readonly MentionRecord[] {\n return chapterFilter === undefined\n ? mentions\n : mentions.filter((mention) => chapterFilter.has(mention.chapterId));\n}\n\nasync function filterMentionLinksByChapter(\n document: ReadonlyDocument,\n links: readonly MentionLinkRecord[],\n chapterId: number | undefined,\n): Promise<readonly MentionLinkRecord[]> {\n if (chapterId === undefined) {\n return links;\n }\n\n const filtered: MentionLinkRecord[] = [];\n\n for (const link of links) {\n const source = await document.mentions.getById(link.sourceMentionId);\n\n if (source?.chapterId === chapterId) {\n filtered.push(link);\n }\n }\n\n return filtered;\n}\n\nasync function createSourceEvidencePage(\n document: ReadonlyDocument,\n ranges: readonly SourceEvidenceRange[],\n options: ArchiveEvidenceOptions,\n): Promise<ArchiveEvidence> {\n const context = createEvidenceReadContext();\n const limit = options.limit ?? DEFAULT_FIND_LIMIT;\n const start = decodeFindCursor(options.cursor);\n const evidenceRanges = await filterAndSortSourceEvidenceRangesByFtsQuery(\n document,\n ranges,\n options.query,\n options.order ?? \"doc-asc\",\n );\n const displayRanges = await createExpandedSourceEvidenceRanges(\n document,\n evidenceRanges,\n options.sourceContext ?? DEFAULT_SOURCE_CONTEXT,\n context,\n );\n const pageRanges = displayRanges.slice(start, start + limit);\n const nextOffset = start + pageRanges.length;\n const items = await Promise.all(\n pageRanges.map(\n async (range) =>\n await createSourceEvidenceItem(\n document,\n range.chapterId,\n range.startSentenceIndex,\n range.endSentenceIndex,\n context,\n range.score,\n ),\n ),\n );\n\n return {\n items,\n limit,\n nextCursor:\n nextOffset < displayRanges.length ? encodeFindCursor(nextOffset) : null,\n };\n}\n\nasync function filterAndSortSourceEvidenceRangesByFtsQuery(\n document: ReadonlyDocument,\n ranges: readonly SourceEvidenceRange[],\n queryText: string | undefined,\n order: ArchiveFindOrder,\n): Promise<readonly SourceEvidenceRange[]> {\n const documentOrders = await document.serials.listDocumentOrders();\n\n if (queryText === undefined) {\n return mergeSourceEvidenceRanges(ranges).sort((left, right) =>\n compareSourceEvidenceRanges(left, right, documentOrders, order),\n );\n }\n\n const indexResult = await queryRequiredSearchIndex(document, queryText, {\n chapters: [...new Set(ranges.map((range) => range.chapterId))],\n types: [\"source\"],\n });\n\n if (indexResult === undefined) {\n return [];\n }\n\n const matchedRanges = new Map<string, SourceEvidenceRange>();\n const rangesByChapterId = new Map<number, SourceEvidenceRange[]>();\n\n for (const range of ranges) {\n const chapterRanges = rangesByChapterId.get(range.chapterId) ?? [];\n\n chapterRanges.push(range);\n rangesByChapterId.set(range.chapterId, chapterRanges);\n }\n\n for (const hit of indexResult.textHits) {\n if (hit.kind !== TEXT_SENTENCE_KIND.source) {\n continue;\n }\n\n for (const range of rangesByChapterId.get(hit.chapterId) ?? []) {\n if (\n hit.sentenceIndex < range.startSentenceIndex ||\n hit.sentenceIndex > range.endSentenceIndex\n ) {\n continue;\n }\n\n const key = formatSourceEvidenceRangeKey(range);\n const current = matchedRanges.get(key);\n\n matchedRanges.set(key, {\n ...range,\n score: Math.max(current?.score ?? 0, hit.score),\n });\n }\n }\n\n return [...matchedRanges.values()].sort((left, right) => {\n const scoreComparison = (right.score ?? 0) - (left.score ?? 0);\n\n if (scoreComparison !== 0) {\n return scoreComparison;\n }\n\n return compareSourceEvidenceRanges(left, right, documentOrders, \"doc-asc\");\n });\n}\n\nfunction formatSourceEvidenceRangeKey(range: SourceEvidenceRange): string {\n return `${range.chapterId}:${range.startSentenceIndex}:${range.endSentenceIndex}`;\n}\n\nfunction compareSourceEvidenceRanges(\n left: SourceEvidenceRange,\n right: SourceEvidenceRange,\n documentOrders: ReadonlyMap<number, number>,\n order: ArchiveFindOrder,\n): number {\n const direction = order === \"doc-asc\" ? 1 : -1;\n\n return (\n (compareNumbers(\n documentOrders.get(left.chapterId) ?? left.chapterId,\n documentOrders.get(right.chapterId) ?? right.chapterId,\n ) ||\n compareNumbers(left.chapterId, right.chapterId) ||\n compareNumbers(left.startSentenceIndex, right.startSentenceIndex) ||\n compareNumbers(left.endSentenceIndex, right.endSentenceIndex)) * direction\n );\n}\n\nasync function createSourceEvidencePreview(\n document: ReadonlyDocument,\n ranges: readonly SourceEvidenceRange[],\n limit: number,\n context: EvidenceReadContext = createEvidenceReadContext(),\n sourceContext = DEFAULT_SOURCE_CONTEXT,\n order: ArchiveFindOrder = \"doc-asc\",\n): Promise<ArchiveFindEvidencePreview> {\n const documentOrders = await document.serials.listDocumentOrders();\n const mergedRanges = mergeSourceEvidenceRanges(ranges).sort((left, right) =>\n compareSourceEvidenceRanges(left, right, documentOrders, order),\n );\n const displayRanges = await createExpandedSourceEvidenceRanges(\n document,\n mergedRanges.slice(0, limit),\n sourceContext,\n context,\n );\n const sources = await Promise.all(\n displayRanges.map(\n async (range) =>\n await createSourceEvidenceItem(\n document,\n range.chapterId,\n range.startSentenceIndex,\n range.endSentenceIndex,\n context,\n ),\n ),\n );\n\n return {\n nextCursor:\n Math.min(limit, mergedRanges.length) < mergedRanges.length\n ? encodeFindCursor(Math.min(limit, mergedRanges.length))\n : null,\n shown: sources.length,\n sources,\n total: mergedRanges.length,\n };\n}\n\nfunction mergeSourceEvidenceRanges(\n ranges: readonly SourceEvidenceRange[],\n): SourceEvidenceRange[] {\n const rangesBySource = new Map<\n string,\n Array<{\n readonly end: number;\n readonly score?: number;\n readonly start: number;\n }>\n >();\n\n for (const range of ranges) {\n const key = `${range.chapterId}`;\n const sourceRanges = rangesBySource.get(key) ?? [];\n\n sourceRanges.push({\n end: range.endSentenceIndex,\n ...(range.score === undefined ? {} : { score: range.score }),\n start: range.startSentenceIndex,\n });\n rangesBySource.set(key, sourceRanges);\n }\n\n return [...rangesBySource.entries()].flatMap(([key, ranges]) => {\n const chapterId = Number(key);\n\n return mergeEvidenceRanges(ranges).map(\n ({ end, score, start }) =>\n ({\n chapterId,\n endSentenceIndex: end,\n ...(score === undefined ? {} : { score }),\n startSentenceIndex: start,\n }) as const,\n );\n });\n}\n\nfunction mergeSourceEvidenceRangesInInputOrder(\n ranges: readonly SourceEvidenceRange[],\n): SourceEvidenceRange[] {\n const merged: SourceEvidenceRange[] = [];\n\n for (const range of ranges) {\n const overlappingIndexes = merged\n .map((existing, index) =>\n areMergeableSourceEvidenceRanges(existing, range) ? index : -1,\n )\n .filter((index) => index >= 0);\n\n if (overlappingIndexes.length === 0) {\n merged.push(range);\n continue;\n }\n\n const firstIndex = overlappingIndexes[0] ?? 0;\n const overlapping = overlappingIndexes.flatMap((index) => {\n const existing = merged[index];\n\n return existing === undefined ? [] : [existing];\n });\n const mergedRange = mergeSourceEvidenceRangeGroup([...overlapping, range]);\n\n merged[firstIndex] = mergedRange;\n for (const index of overlappingIndexes.slice(1).reverse()) {\n merged.splice(index, 1);\n }\n }\n\n return merged;\n}\n\nfunction areMergeableSourceEvidenceRanges(\n left: SourceEvidenceRange,\n right: SourceEvidenceRange,\n): boolean {\n return (\n left.chapterId === right.chapterId &&\n right.startSentenceIndex <= left.endSentenceIndex + 1 &&\n left.startSentenceIndex <= right.endSentenceIndex + 1\n );\n}\n\nfunction mergeSourceEvidenceRangeGroup(\n ranges: readonly SourceEvidenceRange[],\n): SourceEvidenceRange {\n const [first] = ranges;\n\n if (first === undefined) {\n throw new Error(\"Internal error: cannot merge empty evidence range group.\");\n }\n\n const scores = ranges\n .map((range) => range.score)\n .filter((score): score is number => score !== undefined);\n\n return {\n chapterId: first.chapterId,\n endSentenceIndex: Math.max(\n ...ranges.map((range) => range.endSentenceIndex),\n ),\n ...(scores.length === 0 ? {} : { score: Math.max(...scores) }),\n startSentenceIndex: Math.min(\n ...ranges.map((range) => range.startSentenceIndex),\n ),\n };\n}\n\nasync function createExpandedSourceEvidenceRanges(\n document: ReadonlyDocument,\n ranges: readonly SourceEvidenceRange[],\n sourceContext: number,\n context: EvidenceReadContext,\n): Promise<SourceEvidenceRange[]> {\n const expanded = await Promise.all(\n ranges.map(async (range) => {\n if (sourceContext <= 0) {\n return range;\n }\n\n const sourceIndex = await getTextStreamIndex(\n document,\n range.chapterId,\n \"source\",\n context,\n );\n const lastSentenceIndex = Math.max(0, sourceIndex.sentences.length - 1);\n\n return {\n ...range,\n endSentenceIndex: clampInteger(\n range.endSentenceIndex + sourceContext,\n range.startSentenceIndex,\n lastSentenceIndex,\n ),\n startSentenceIndex: clampInteger(\n range.startSentenceIndex - sourceContext,\n 0,\n lastSentenceIndex,\n ),\n };\n }),\n );\n\n return mergeSourceEvidenceRangesInInputOrder(expanded);\n}\n\nasync function createSourceEvidenceItem(\n document: ReadonlyDocument,\n chapterId: number,\n startSentenceIndex: number,\n endSentenceIndex: number,\n context: EvidenceReadContext = createEvidenceReadContext(),\n score?: number,\n): Promise<ArchiveEvidenceItem> {\n const chapter = await getEvidenceChapter(document, chapterId, context);\n const range = await readTextStreamRange(\n document,\n chapterId,\n \"source\",\n startSentenceIndex,\n endSentenceIndex,\n context,\n );\n\n return {\n chapterId,\n endSentenceIndex: range.endSentenceIndex,\n fragmentId: range.startSentenceIndex,\n id: formatTextStreamRangeUri(\n chapterId,\n \"source\",\n range.startSentenceIndex,\n range.endSentenceIndex,\n ),\n ...(score === undefined ? {} : { score }),\n source: range.text,\n startSentenceIndex: range.startSentenceIndex,\n title: chapter.title ?? `[chapter ${chapterId}]`,\n type: \"source\",\n };\n}\n\nfunction createEvidenceReadContext(): EvidenceReadContext {\n return {\n chapters: new Map(),\n streamIndexes: new Map(),\n };\n}\n\nasync function getEvidenceChapter(\n document: ReadonlyDocument,\n chapterId: number,\n context: EvidenceReadContext,\n): Promise<ChapterEntry> {\n let chapter = context.chapters.get(chapterId);\n\n if (chapter === undefined) {\n chapter = requireChapter(document, chapterId);\n context.chapters.set(chapterId, chapter);\n }\n\n return await chapter;\n}\n\nasync function findSentenceIndexAtOffset(\n document: ReadonlyDocument,\n chapterId: number,\n offset: number,\n): Promise<number> {\n const serial = document.getSerialFragments(chapterId);\n const sentences =\n serial.listSentences === undefined ? [] : await serial.listSentences();\n\n if (sentences.length === 0) {\n return 0;\n }\n let cursor = 0;\n\n for (let index = 0; index < sentences.length; index += 1) {\n const sentence = sentences[index];\n\n if (sentence === undefined) {\n continue;\n }\n\n const nextCursor = cursor + sentence.text.length;\n\n if (offset <= nextCursor) {\n return index;\n }\n\n cursor = nextCursor + 1;\n }\n\n return Math.max(0, sentences.length - 1);\n}\n\nfunction formatTextStreamRangeUri(\n chapterId: number,\n stream: ArchiveTextStreamKind,\n startSentenceIndex: number,\n endSentenceIndex: number,\n): string {\n const startSentenceNumber = startSentenceIndex + 1;\n const endSentenceNumber = endSentenceIndex + 1;\n const hash =\n startSentenceIndex === endSentenceIndex\n ? String(startSentenceNumber)\n : `${startSentenceNumber}..${endSentenceNumber}`;\n\n return `wikg://chapter/${chapterId}/${stream}#${hash}`;\n}\n\nfunction mergeEvidenceRanges(\n ranges: readonly {\n readonly end: number;\n readonly score?: number;\n readonly start: number;\n }[],\n): readonly {\n readonly end: number;\n readonly score?: number;\n readonly start: number;\n}[] {\n const sortedRanges = [...ranges]\n .map((range) => ({\n ...(range.score === undefined ? {} : { score: range.score }),\n end: Math.max(range.start, range.end),\n start: Math.min(range.start, range.end),\n }))\n .sort((left, right) =>\n left.start === right.start\n ? left.end - right.end\n : left.start - right.start,\n );\n const mergedRanges: Array<{\n end: number;\n score?: number;\n start: number;\n }> = [];\n\n for (const { end, score, start } of sortedRanges) {\n const last = mergedRanges.at(-1);\n\n if (last === undefined || start > last.end + 1) {\n mergedRanges.push({\n end,\n ...(score === undefined ? {} : { score }),\n start,\n });\n } else {\n last.end = Math.max(last.end, end);\n if (score !== undefined) {\n last.score = Math.max(last.score ?? score, score);\n }\n }\n }\n\n return mergedRanges;\n}\n\nfunction clampInteger(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, Math.floor(value)));\n}\n\ntype WikiGraphReference =\n | {\n readonly type: \"meta\";\n }\n | {\n readonly chapterId: number;\n readonly type: \"chapter\";\n }\n | {\n readonly chapterId: number;\n readonly type: \"chapter-title\";\n }\n | {\n readonly chapterId: number;\n readonly target?: ChapterStateTarget;\n readonly type: \"chapter-state\";\n }\n | {\n readonly type: \"chapter-tree\";\n }\n | {\n readonly chapterId?: number;\n readonly id: number;\n readonly type: \"chunk\";\n }\n | {\n readonly chapterId: number;\n readonly endSentenceIndex: number;\n readonly stream: ArchiveTextStreamKind;\n readonly startSentenceIndex: number;\n readonly type: \"text-stream\";\n }\n | {\n readonly chapterId?: number;\n readonly qid: string;\n readonly type: \"entity\";\n }\n | {\n readonly qid: string;\n readonly type: \"entity-wikipage\";\n }\n | {\n readonly chapterId?: number;\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n readonly type: \"triple\";\n };\n\nfunction parseWikiGraphReference(uri: string): WikiGraphReference {\n uri = normalizeWikiGraphObjectUri(uri);\n\n if (!isWikiGraphObjectUri(uri)) {\n const archiveReference = parseArchiveReference(uri);\n\n switch (archiveReference.type) {\n case \"node\":\n return { id: archiveReference.id, type: \"chunk\" };\n case \"chapter\":\n return { chapterId: archiveReference.id, type: \"chapter\" };\n case \"chapter-title\":\n return { chapterId: archiveReference.id, type: \"chapter-title\" };\n case \"summary\":\n return {\n chapterId: archiveReference.id,\n endSentenceIndex: Number.POSITIVE_INFINITY,\n stream: \"summary\",\n startSentenceIndex: 0,\n type: \"text-stream\",\n };\n case \"fragment\":\n case \"meta\":\n throw new Error(`Evidence is not available for ${uri}.`);\n }\n }\n\n if (uri === WIKI_GRAPH_URI_PREFIX) {\n return { type: \"meta\" };\n }\n\n const [rawPath = \"\", hash = \"\"] = uri\n .slice(WIKI_GRAPH_URI_PREFIX.length)\n .split(\"#\", 2);\n const pathParts = rawPath.split(\"/\").filter((part) => part !== \"\");\n\n if (pathParts.length === 0) {\n return { type: \"meta\" };\n }\n\n if (pathParts[0] === \"chapter-tree\" && pathParts.length === 1) {\n return { type: \"chapter-tree\" };\n }\n\n switch (pathParts[0]) {\n case \"chapter\":\n if (pathParts.length === 2) {\n return {\n chapterId: parsePositiveInteger(pathParts[1], uri),\n type: \"chapter\",\n };\n }\n if (pathParts[1] !== undefined) {\n const chapterId = parsePositiveInteger(pathParts[1], uri);\n\n switch (pathParts[2]) {\n case \"state\":\n if (pathParts.length === 3) {\n return { chapterId, type: \"chapter-state\" };\n }\n if (pathParts.length === 4) {\n return {\n chapterId,\n target: parseChapterStateTarget(pathParts[3], uri),\n type: \"chapter-state\",\n };\n }\n break;\n case \"title\":\n if (pathParts.length === 3) {\n return { chapterId, type: \"chapter-title\" };\n }\n break;\n case \"chunk\":\n if (pathParts.length === 4) {\n return {\n chapterId,\n id: parsePositiveInteger(pathParts[3], uri),\n type: \"chunk\",\n };\n }\n break;\n case \"entity\":\n if (pathParts.length === 4) {\n return {\n chapterId,\n qid: parseQid(pathParts[3], uri),\n type: \"entity\",\n };\n }\n break;\n case \"source\":\n if (pathParts.length === 3) {\n const [start, end] = parseSentenceRange(hash);\n\n return {\n chapterId,\n endSentenceIndex: end,\n stream: \"source\",\n startSentenceIndex: start,\n type: \"text-stream\",\n };\n }\n break;\n case \"summary\":\n if (pathParts.length === 3) {\n const [start, end] = parseSentenceRange(hash);\n\n return {\n chapterId,\n endSentenceIndex: end,\n stream: \"summary\",\n startSentenceIndex: start,\n type: \"text-stream\",\n };\n }\n break;\n case \"tree\":\n if (pathParts.length === 3) {\n return { chapterId, type: \"chapter\" };\n }\n break;\n case \"triple\":\n if (pathParts.length === 6) {\n return {\n chapterId,\n objectQid: parseQid(pathParts[5], uri),\n predicate: decodeURIComponent(pathParts[4] ?? \"\"),\n subjectQid: parseQid(pathParts[3], uri),\n type: \"triple\",\n };\n }\n break;\n }\n }\n break;\n case \"chunk\":\n if (pathParts.length === 2) {\n return {\n id: parsePositiveInteger(pathParts[1], uri),\n type: \"chunk\",\n };\n }\n break;\n case \"entity\":\n if (pathParts.length === 2) {\n return {\n qid: parseQid(pathParts[1], uri),\n type: \"entity\",\n };\n }\n if (pathParts.length === 3 && pathParts[2] === \"wikipage\") {\n return {\n qid: parseQid(pathParts[1], uri),\n type: \"entity-wikipage\",\n };\n }\n break;\n case \"triple\":\n if (pathParts.length === 4) {\n return {\n objectQid: parseQid(pathParts[3], uri),\n predicate: decodeURIComponent(pathParts[2] ?? \"\"),\n subjectQid: parseQid(pathParts[1], uri),\n type: \"triple\",\n };\n }\n break;\n }\n\n throw new Error(`Invalid Wiki Graph URI: ${uri}`);\n}\n\nfunction parseQid(value: string | undefined, uri: string): string {\n if (value !== undefined && /^Q[1-9][0-9]*$/u.test(value)) {\n return value;\n }\n\n throw new Error(`Invalid Wiki Graph URI: ${uri}`);\n}\n\nfunction parseChapterStateTarget(\n value: string | undefined,\n uri: string,\n): ChapterStateTarget {\n if (\n value === \"source\" ||\n value === \"reading-graph\" ||\n value === \"reading-summary\" ||\n value === \"knowledge-graph\"\n ) {\n return value;\n }\n\n throw new Error(`Invalid Wiki Graph URI: ${uri}`);\n}\n\nfunction parseSentenceRange(hash: string): readonly [number, number] {\n if (hash === \"\") {\n return [0, Number.POSITIVE_INFINITY];\n }\n\n const match = /^([1-9][0-9]*)(?:\\.\\.([1-9][0-9]*))?$/u.exec(hash);\n\n if (match?.[1] === undefined) {\n throw new Error(`Invalid source sentence range: ${hash}`);\n }\n\n const parsedStart = Number(match[1]) - 1;\n const parsedEnd = Number(match[2] ?? match[1]) - 1;\n\n if (\n Number.isInteger(parsedStart) &&\n parsedStart >= 0 &&\n Number.isInteger(parsedEnd) &&\n parsedEnd >= parsedStart\n ) {\n return [parsedStart, parsedEnd];\n }\n\n throw new Error(`Invalid source sentence range: ${hash}`);\n}\n\nfunction parseArchiveReference(id: string):\n | {\n readonly id: number;\n readonly type: \"chapter\" | \"chapter-title\" | \"summary\";\n }\n | {\n readonly id: number;\n readonly type: \"node\";\n }\n | {\n readonly fragmentId: number;\n readonly serialId: number;\n readonly type: \"fragment\";\n }\n | {\n readonly type: \"meta\";\n } {\n const normalized = id.trim();\n const [type, value] = normalized.split(\":\", 2);\n\n if (type === \"meta\" && (value === \"book\" || value === \"root\")) {\n return { type: \"meta\" };\n }\n if (type === \"chapter\" || type === \"chapter-title\" || type === \"summary\") {\n const parsedId = parsePositiveInteger(value, normalized);\n\n return { id: parsedId, type };\n }\n if (type === \"node\") {\n const parsedId = parsePositiveInteger(value, normalized);\n\n return {\n id: parsedId,\n type: \"node\",\n };\n }\n if (type === \"fragment\") {\n const parts = normalized.slice(\"fragment:\".length).split(\":\");\n\n if (parts.length !== 2) {\n throw new Error(`Invalid archive object id: ${id}`);\n }\n\n return {\n fragmentId: parseNonNegativeInteger(parts[1], normalized),\n serialId: parsePositiveInteger(parts[0], normalized),\n type: \"fragment\",\n };\n }\n throw new Error(`Invalid archive object id: ${id}`);\n}\n\nfunction parsePositiveInteger(value: string | undefined, id: string): number {\n const parsed = Number(value);\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new Error(`Invalid archive object id: ${id}`);\n }\n\n return parsed;\n}\n\nfunction parseNonNegativeInteger(\n value: string | undefined,\n id: string,\n): number {\n const parsed = Number(value);\n\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new Error(`Invalid archive object id: ${id}`);\n }\n\n return parsed;\n}\n\ninterface ArchiveTextSearch {\n readonly match: ArchiveFindMatch;\n readonly terms: readonly string[];\n}\n\ninterface ArchiveTextMatch {\n readonly matchCount: number;\n readonly matchedTerms: readonly string[];\n readonly missingTerms: readonly string[];\n readonly score: number;\n}\n\nconst DEFAULT_FIND_LIMIT = 20;\nconst GROUP_SCORE_EVIDENCE_LIMIT = 10;\nconst TEXT_ONLY_SEARCH_CACHE_WINDOW = 100;\nconst GROUP_SCORE_MAX_EQUAL_EVIDENCE_BONUS = 0.3;\nconst ARCHIVE_ROOT_ID = \"meta:root\";\n\nfunction isWikiGraphObjectUri(uri: string): boolean {\n return uri.startsWith(WIKI_GRAPH_URI_PREFIX);\n}\n\nfunction normalizeWikiGraphObjectUri(uri: string): string {\n return uri;\n}\n\nconst BROAD_FIND_LENS_HINT = {\n lenses: {\n chapter: \"book outline and chapter titles\",\n chunk: \"source text ranges\",\n entity: \"indexed entities\",\n node: \"topology / LLM Wiki structure\",\n triple: \"knowledge graph statements\",\n },\n message:\n \"Choose scope URI lenses such as /chapter, /chunk, /entity, or /triple for broad search.\",\n} satisfies ArchiveFindLensHint;\n\nfunction createPhraseSearch(query: string): ArchiveTextSearch | undefined {\n const needle = query.trim().toLowerCase();\n\n if (needle === \"\") {\n return undefined;\n }\n\n return {\n match: \"all\",\n terms: [needle],\n };\n}\n\nfunction matchText(\n value: string,\n search: ArchiveTextSearch,\n): ArchiveTextMatch | undefined {\n const lower = value.toLowerCase();\n const matchedTerms = search.terms.filter((term) => lower.includes(term));\n const missingTerms = search.terms.filter((term) => !lower.includes(term));\n\n if (search.match === \"all\" && missingTerms.length > 0) {\n return undefined;\n }\n if (search.match === \"any\" && matchedTerms.length === 0) {\n return undefined;\n }\n const [snippetNeedle] = matchedTerms;\n\n if (snippetNeedle === undefined) {\n return undefined;\n }\n\n return {\n matchCount: matchedTerms.length,\n matchedTerms,\n missingTerms,\n score: matchedTerms.length / search.terms.length,\n };\n}\n\nfunction createFindMatchFields(\n match: ArchiveTextMatch,\n): Pick<\n ArchiveFindHit,\n \"matchCount\" | \"matchedTerms\" | \"missingTerms\" | \"score\"\n> {\n return {\n matchCount: match.matchCount,\n matchedTerms: match.matchedTerms,\n missingTerms: match.missingTerms,\n score: match.score,\n };\n}\n\nfunction aggregateEvidenceScores(scores: readonly number[]): number {\n const rankedScores = [...scores]\n .filter((score) => score > 0)\n .sort((left, right) => right - left)\n .slice(0, GROUP_SCORE_EVIDENCE_LIMIT);\n const [bestScore] = rankedScores;\n\n if (bestScore === undefined) {\n return 0;\n }\n\n const evidenceDecayFactor =\n GROUP_SCORE_MAX_EQUAL_EVIDENCE_BONUS / calculateEvidenceDecayBase();\n\n return rankedScores.reduce(\n (total, score, index) =>\n total +\n score * (index === 0 ? 1 : evidenceDecayFactor / Math.log2(index + 2)),\n 0,\n );\n}\n\nfunction calculateEvidenceDecayBase(): number {\n let total = 0;\n\n for (let rank = 2; rank <= GROUP_SCORE_EVIDENCE_LIMIT; rank += 1) {\n total += 1 / Math.log2(rank + 1);\n }\n\n return total;\n}\n\nfunction compareFindEvidenceHits(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n): number {\n const scoreComparison = (right.score ?? 0) - (left.score ?? 0);\n\n if (scoreComparison !== 0) {\n return scoreComparison;\n }\n if (left.position === undefined) {\n return right.position === undefined ? 0 : 1;\n }\n if (right.position === undefined) {\n return -1;\n }\n return compareArchivePositions(left.position, right.position);\n}\n\nfunction getSnippetNeedle(match: ArchiveTextMatch): string {\n const [needle] = match.matchedTerms;\n\n if (needle === undefined) {\n throw new Error(\"Internal error: missing matched search term.\");\n }\n\n return needle;\n}\n\nfunction createFindResult(\n query: string,\n hits: readonly ArchiveFindHit[],\n options: ArchiveFindOptions,\n terms = createSearchTerms(query),\n lens: ArchiveFindLens = options.types === undefined ? \"broad\" : \"typed\",\n): ArchiveFindResult {\n const ranked = createRankedFindResult(query, hits, options, terms, lens);\n const start = decodeFindCursor(options.cursor);\n const items = ranked.items.slice(start, start + ranked.limit);\n const nextOffset = start + items.length;\n\n return {\n ...ranked,\n items,\n nextCursor:\n nextOffset < ranked.items.length ? encodeFindCursor(nextOffset) : null,\n };\n}\n\nfunction createRankedFindResult(\n query: string,\n hits: readonly ArchiveFindHit[],\n options: ArchiveFindOptions,\n terms = createSearchTerms(query),\n lens: ArchiveFindLens = options.types === undefined ? \"broad\" : \"typed\",\n): ArchiveFindResult {\n const order = options.order ?? \"doc-asc\";\n const limit = options.limit ?? DEFAULT_FIND_LIMIT;\n const chapters = options.chapters ?? null;\n const match = options.match ?? \"any\";\n const types = options.types ?? null;\n const ids = options.ids ?? null;\n const filtered = groupFindHitsByObject(hits)\n .filter((hit) => matchesFindId(hit, ids))\n .filter((hit) => matchesFindChapter(hit, chapters))\n .filter((hit) => matchesFindType(hit, types))\n .filter((hit) => matchesTriplePattern(hit, options.triplePattern))\n .sort((left, right) => compareSearchHits(left, right, order));\n\n return {\n chapters,\n items: filtered,\n lens,\n lensHint: lens === \"broad\" ? BROAD_FIND_LENS_HINT : null,\n limit,\n match,\n nextCursor: null,\n order,\n query,\n terms,\n types,\n };\n}\n\nfunction groupFindHitsByObject(\n hits: readonly ArchiveFindHit[],\n): readonly ArchiveFindHit[] {\n const hitsById = new Map<string, ArchiveFindHit[]>();\n\n for (const hit of hits) {\n const values = hitsById.get(hit.id) ?? [];\n\n values.push(hit);\n hitsById.set(hit.id, values);\n }\n\n return [...hitsById.values()].map(groupObjectEvidenceHits);\n}\n\nfunction groupObjectEvidenceHits(\n evidenceHits: readonly ArchiveFindHit[],\n): ArchiveFindHit {\n const rankedHits = [...evidenceHits].sort(compareFindEvidenceHits);\n const [best] = rankedHits;\n\n if (best === undefined) {\n throw new Error(\"Internal error: search result candidate is empty.\");\n }\n if (rankedHits.length === 1) {\n return best;\n }\n\n return {\n ...best,\n matchCount: Math.max(...rankedHits.map((hit) => hit.matchCount ?? 0)),\n matchedTerms: mergeStringLists(\n rankedHits.flatMap((hit) => hit.matchedTerms ?? []),\n ),\n missingTerms: mergeStringLists(\n rankedHits.flatMap((hit) => hit.missingTerms ?? []),\n ),\n score: aggregateEvidenceScores(rankedHits.map((hit) => hit.score ?? 0)),\n };\n}\n\nfunction mergeStringLists(values: readonly string[]): readonly string[] {\n return [...new Set(values)];\n}\n\nfunction createCollectionResult(\n hits: readonly ArchiveFindHit[],\n options: ArchiveCollectionOptions,\n): ArchiveCollectionResult {\n const order = options.order ?? \"doc-asc\";\n const limit = options.limit ?? DEFAULT_FIND_LIMIT;\n const chapters = options.chapters ?? null;\n const ids = options.ids ?? null;\n const types = options.types ?? null;\n const start = decodeFindCursor(options.cursor);\n const filtered = hits\n .filter((hit) => matchesFindId(hit, ids))\n .filter((hit) => matchesFindChapter(hit, chapters))\n .filter((hit) => matchesCollectionType(hit, types))\n .filter((hit) => matchesTriplePattern(hit, options.triplePattern))\n .sort((left, right) => compareListHits(left, right, order));\n const items = filtered.slice(start, start + limit);\n const nextOffset = start + items.length;\n\n return {\n chapters,\n ids,\n items,\n limit,\n nextCursor:\n nextOffset < filtered.length ? encodeFindCursor(nextOffset) : null,\n order,\n types,\n };\n}\n\nfunction matchesFindId(\n hit: ArchiveFindHit,\n ids: readonly string[] | null,\n): boolean {\n return ids === null || ids.includes(hit.id);\n}\n\nfunction matchesFindChapter(\n hit: ArchiveFindHit,\n chapters: readonly number[] | null,\n): boolean {\n if (chapters === null) {\n return true;\n }\n\n return hit.chapter !== undefined && chapters.includes(hit.chapter);\n}\n\nfunction matchesFindType(\n hit: ArchiveFindHit,\n types: readonly ArchiveFindFilterType[] | null,\n): boolean {\n if (types === null) {\n return true;\n }\n\n if (hit.type === \"chapter-title\" && types.includes(\"chapter\")) {\n return true;\n }\n\n return isFindFilterType(hit.type) && types.includes(hit.type);\n}\n\nfunction matchesCollectionType(\n hit: ArchiveFindHit,\n types: readonly ArchiveCollectionType[] | null,\n): boolean {\n return (\n types === null || (isCollectionType(hit.type) && types.includes(hit.type))\n );\n}\n\nfunction matchesTriplePattern(\n hit: ArchiveFindHit,\n pattern: ArchiveTriplePattern | undefined,\n): boolean {\n if (pattern === undefined || hit.type !== \"triple\") {\n return true;\n }\n\n const triple = parseTripleHitUri(hit.id);\n\n if (triple === undefined) {\n return false;\n }\n\n return (\n (pattern.subjectQid === undefined ||\n pattern.subjectQid === triple.subjectQid) &&\n (pattern.predicate === undefined ||\n pattern.predicate === triple.predicate) &&\n (pattern.objectQid === undefined || pattern.objectQid === triple.objectQid)\n );\n}\n\nfunction parseTripleHitUri(uri: string):\n | {\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n }\n | undefined {\n const match =\n /^wikg:\\/\\/triple\\/(Q[1-9][0-9]*)\\/([^/]+)\\/(Q[1-9][0-9]*)$/u.exec(uri);\n\n if (\n match?.[1] === undefined ||\n match[2] === undefined ||\n match[3] === undefined\n ) {\n return undefined;\n }\n\n return {\n objectQid: match[3],\n predicate: decodeURIComponent(match[2]),\n subjectQid: match[1],\n };\n}\n\nfunction compareSearchHits(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n order: ArchiveFindOrder,\n): number {\n const direction = order === \"doc-asc\" ? 1 : -1;\n const relevance =\n compareNumbers(getSearchBucket(left.type), getSearchBucket(right.type)) ||\n compareNumbers(right.score ?? 0, left.score ?? 0) ||\n compareNumbers(right.matchCount ?? 0, left.matchCount ?? 0);\n const position =\n compareNumbers(\n getPositionDocumentOrder(left),\n getPositionDocumentOrder(right),\n ) ||\n compareNumbers(getPositionChapter(left), getPositionChapter(right)) ||\n compareNumbers(getPositionFragment(left), getPositionFragment(right)) ||\n compareNumbers(getPositionSentence(left), getPositionSentence(right)) ||\n compareNumbers(getTypeOrder(left.type), getTypeOrder(right.type)) ||\n left.id.localeCompare(right.id);\n\n return relevance || position * direction;\n}\n\nfunction compareListHits(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n order: ArchiveFindOrder,\n): number {\n const direction = order === \"doc-asc\" ? 1 : -1;\n const bucketComparison =\n compareNumbers(getListBucket(left.type), getListBucket(right.type)) ||\n compareListBucketItems(left, right);\n\n if (bucketComparison !== 0) {\n return bucketComparison;\n }\n\n return compareListPosition(left, right) * direction;\n}\n\nfunction compareListBucketItems(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n): number {\n const leftBucket = getListBucket(left.type);\n\n if (leftBucket !== getListBucket(right.type)) {\n return 0;\n }\n if (leftBucket === 0) {\n return compareNumbers(right.score ?? 0, left.score ?? 0);\n }\n\n return 0;\n}\n\nfunction compareListPosition(\n left: ArchiveFindHit,\n right: ArchiveFindHit,\n): number {\n return (\n compareNumbers(\n getPositionDocumentOrder(left),\n getPositionDocumentOrder(right),\n ) ||\n compareNumbers(getPositionChapter(left), getPositionChapter(right)) ||\n compareNumbers(getPositionFragment(left), getPositionFragment(right)) ||\n compareNumbers(getPositionSentence(left), getPositionSentence(right)) ||\n compareNumbers(getTypeOrder(left.type), getTypeOrder(right.type)) ||\n left.id.localeCompare(right.id)\n );\n}\n\nfunction getListBucket(type: ArchiveFindObjectType): number {\n switch (type) {\n case \"entity\":\n case \"triple\":\n return 0;\n case \"node\":\n return 1;\n case \"summary\":\n return 2;\n case \"source\":\n case \"fragment\":\n return 3;\n case \"chapter-title\":\n case \"chapter\":\n case \"chapter-tree\":\n case \"meta\":\n return 4;\n }\n}\n\nfunction getSearchBucket(type: ArchiveFindObjectType): number {\n switch (type) {\n case \"chapter-title\":\n return 0;\n case \"entity\":\n case \"triple\":\n return 1;\n case \"node\":\n return 2;\n case \"source\":\n case \"summary\":\n return 3;\n case \"chapter\":\n case \"chapter-tree\":\n case \"meta\":\n case \"fragment\":\n throw new Error(`Unsupported search result bucket type: ${type}`);\n }\n}\n\nfunction createSearchTerms(query: string): readonly string[] {\n return query\n .trim()\n .toLowerCase()\n .split(/\\s+/u)\n .filter((term) => term !== \"\");\n}\n\nfunction getPositionChapter(hit: ArchiveFindHit): number {\n return hit.position?.chapter ?? Number.MAX_SAFE_INTEGER;\n}\n\nfunction getPositionDocumentOrder(hit: ArchiveFindHit): number {\n return (\n hit.position?.documentOrder ??\n hit.position?.chapter ??\n Number.MAX_SAFE_INTEGER\n );\n}\n\nfunction getPositionFragment(hit: ArchiveFindHit): number {\n return hit.position?.fragment ?? 0;\n}\n\nfunction getPositionSentence(hit: ArchiveFindHit): number {\n return hit.position?.sentence ?? 0;\n}\n\nfunction getTypeOrder(type: ArchiveFindObjectType): number {\n switch (type) {\n case \"chapter-title\":\n case \"chapter\":\n return 0;\n case \"chapter-tree\":\n return 1;\n case \"entity\":\n return 2;\n case \"triple\":\n return 3;\n case \"summary\":\n return 4;\n case \"node\":\n return 5;\n case \"source\":\n return 6;\n case \"fragment\":\n return 6;\n case \"meta\":\n return 7;\n }\n}\n\nfunction createNodePosition(\n sentenceIds: readonly SentenceId[],\n documentOrders?: ReadonlyMap<number, number>,\n): ArchiveFindPosition | undefined {\n const [first] = [...sentenceIds].sort(compareSentenceIds);\n\n return first === undefined\n ? undefined\n : createSentencePosition(first, documentOrders);\n}\n\nfunction createSentencePosition(\n sentenceId: SentenceId,\n documentOrders?: ReadonlyMap<number, number>,\n): ArchiveFindPosition {\n return {\n chapter: sentenceId[0],\n documentOrder: documentOrders?.get(sentenceId[0]) ?? sentenceId[0],\n fragment: sentenceId[1],\n sentence: sentenceId[1],\n };\n}\n\nfunction compareSentenceIds(\n left: SentenceId,\n right: SentenceId,\n documentOrders?: ReadonlyMap<number, number>,\n): number {\n return (\n compareNumbers(\n documentOrders?.get(left[0]) ?? left[0],\n documentOrders?.get(right[0]) ?? right[0],\n ) ||\n compareNumbers(left[0], right[0]) ||\n compareNumbers(left[1], right[1])\n );\n}\n\nfunction compareArchivePositions(\n left: ArchiveFindPosition,\n right: ArchiveFindPosition,\n): number {\n return (\n compareNumbers(\n left.documentOrder ?? left.chapter,\n right.documentOrder ?? right.chapter,\n ) ||\n compareNumbers(left.chapter, right.chapter) ||\n compareNumbers(left.fragment ?? 0, right.fragment ?? 0) ||\n compareNumbers(left.sentence ?? 0, right.sentence ?? 0)\n );\n}\n\nfunction compareNumbers(left: number, right: number): number {\n return left === right ? 0 : left < right ? -1 : 1;\n}\n\nfunction isFindFilterType(\n type: ArchiveFindObjectType,\n): type is ArchiveFindFilterType {\n return (\n type === \"chapter\" ||\n type === \"chapter-title\" ||\n type === \"entity\" ||\n type === \"fragment\" ||\n type === \"meta\" ||\n type === \"node\" ||\n type === \"source\" ||\n type === \"summary\" ||\n type === \"triple\"\n );\n}\n\nfunction isCollectionType(\n type: ArchiveFindObjectType,\n): type is ArchiveCollectionType {\n return (\n type === \"chapter\" ||\n type === \"chapter-title\" ||\n type === \"entity\" ||\n type === \"fragment\" ||\n type === \"meta\" ||\n type === \"node\" ||\n type === \"source\" ||\n type === \"summary\" ||\n type === \"triple\"\n );\n}\n\nfunction parseFindLens(value: string): ArchiveFindLens {\n if (value === \"broad\" || value === \"exact\" || value === \"typed\") {\n return value;\n }\n\n throw new Error(\"Invalid cached search session.\");\n}\n\nfunction parseFindMatch(value: string): ArchiveFindMatch {\n if (value === \"all\" || value === \"any\") {\n return value;\n }\n\n throw new Error(\"Invalid cached search session.\");\n}\n\nfunction parseFindTypes(\n values: readonly string[] | null,\n): readonly ArchiveFindFilterType[] | null {\n if (values === null) {\n return null;\n }\n\n return values.map((value) => {\n if (\n value === \"entity\" ||\n value === \"fragment\" ||\n value === \"meta\" ||\n value === \"node\" ||\n value === \"source\" ||\n value === \"summary\" ||\n value === \"chapter\" ||\n value === \"chapter-title\" ||\n value === \"triple\"\n ) {\n return value;\n }\n\n throw new Error(\"Invalid cached search session.\");\n });\n}\n\nfunction encodeFindCursor(offset: number): string {\n return Buffer.from(JSON.stringify({ offset, v: 1 })).toString(\"base64url\");\n}\n\nfunction decodeFindCursor(cursor: string | undefined): number {\n if (cursor === undefined) {\n return 0;\n }\n\n try {\n const parsed: unknown = JSON.parse(\n Buffer.from(cursor, \"base64url\").toString(\"utf8\"),\n );\n\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"v\" in parsed &&\n \"offset\" in parsed &&\n parsed.v === 1 &&\n Number.isInteger(parsed.offset) &&\n typeof parsed.offset === \"number\" &&\n parsed.offset >= 0\n ) {\n return parsed.offset;\n }\n } catch {\n throw new Error(\"Invalid search cursor.\");\n }\n\n throw new Error(\"Invalid search cursor.\");\n}\n\nfunction isFindCursor(cursor: string): boolean {\n try {\n decodeFindCursor(cursor);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction createSnippet(value: string, needle?: string): string {\n const collapsed = value.replace(/\\s+/g, \" \").trim();\n\n if (needle === undefined) {\n return collapsed.length > 180 ? `${collapsed.slice(0, 177)}...` : collapsed;\n }\n\n const index = collapsed.toLowerCase().indexOf(needle);\n\n if (index < 0) {\n return collapsed.length > 180 ? `${collapsed.slice(0, 177)}...` : collapsed;\n }\n\n const start = Math.max(0, index - 60);\n const end = Math.min(collapsed.length, index + needle.length + 120);\n const prefix = start === 0 ? \"\" : \"...\";\n const suffix = end === collapsed.length ? \"\" : \"...\";\n\n return `${prefix}${collapsed.slice(start, end)}${suffix}`;\n}\n\nfunction formatMetaSummary(meta: BookMeta | undefined): string {\n if (meta === undefined) {\n return \"[missing]\";\n }\n\n return [meta.title, meta.authors.join(\", \"), meta.publisher]\n .filter((value) => value !== null && value !== \"\")\n .join(\" / \");\n}\n\nfunction formatMetaTitle(meta: BookMeta | undefined): string {\n return meta?.title ?? \"Archive metadata\";\n}\n\nfunction createMetaPage(meta: BookMeta | undefined): {\n readonly authors?: readonly string[];\n readonly description?: string;\n readonly publisher?: string;\n readonly title: string;\n} {\n return {\n ...(meta?.authors === undefined || meta.authors.length === 0\n ? {}\n : { authors: meta.authors }),\n ...(meta?.description === undefined || meta.description === null\n ? {}\n : { description: meta.description }),\n ...(meta?.publisher === undefined || meta.publisher === null\n ? {}\n : { publisher: meta.publisher }),\n title: formatMetaTitle(meta),\n };\n}\n\nfunction formatMetaText(meta: BookMeta | undefined): string {\n const page = createMetaPage(meta);\n\n return [\n `title: ${page.title}`,\n page.authors === undefined\n ? undefined\n : `authors: ${page.authors.join(\", \")}`,\n page.publisher === undefined ? undefined : `publisher: ${page.publisher}`,\n page.description === undefined\n ? undefined\n : `description: ${page.description}`,\n ]\n .filter(isDefined)\n .join(\"\\n\");\n}\n\nfunction formatWeight(weight: number): string {\n return Number.isInteger(weight) ? String(weight) : weight.toFixed(3);\n}\n\nfunction isDefined<T>(value: T | undefined | null): value is T {\n return value !== undefined && value !== null;\n}\n","import { randomBytes } from \"crypto\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphCacheDirectoryPath } from \"../../common/wiki-graph-dir.js\";\nimport { getOptionalString, getString } from \"../../document/database.js\";\nimport { openSharedStateDatabase } from \"../../document/index.js\";\nimport type { Database } from \"../../document/index.js\";\n\nexport type ContinuationCursor =\n | {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly backlinks?: boolean;\n readonly chapters: readonly number[] | null;\n readonly cursor: string;\n readonly evidenceLimit?: number;\n readonly format: \"json\" | \"jsonl\" | \"text\";\n readonly ids: readonly string[] | null;\n readonly kind: \"collection\";\n readonly order: \"doc-asc\" | \"doc-desc\";\n readonly sourceContext?: number;\n readonly triplePattern?: {\n readonly objectQid?: string;\n readonly predicate?: string;\n readonly subjectQid?: string;\n };\n readonly types: readonly string[] | null;\n }\n | {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly backlinks?: boolean;\n readonly cursor: string;\n readonly evidenceLimit?: number;\n readonly format: \"json\" | \"jsonl\" | \"text\";\n readonly kind: \"search\";\n readonly query?: string;\n readonly sourceContext?: number;\n readonly triplePattern?: {\n readonly objectQid?: string;\n readonly predicate?: string;\n readonly subjectQid?: string;\n };\n readonly types: readonly string[] | null;\n }\n | {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly cursor: string;\n readonly format: \"json\" | \"jsonl\" | \"text\";\n readonly kind: \"evidence\";\n readonly order: \"doc-asc\" | \"doc-desc\";\n readonly query?: string;\n readonly sourceContext?: number;\n readonly targetUri: string;\n }\n | {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly cursor: string;\n readonly evidenceLimit?: number;\n readonly format: \"json\" | \"jsonl\" | \"text\";\n readonly kind: \"related\";\n readonly order: \"doc-asc\" | \"doc-desc\";\n readonly query?: string;\n readonly role?: \"any\" | \"object\" | \"self\" | \"subject\";\n readonly sourceContext?: number;\n readonly targetUri: string;\n };\n\nconst CONTINUATION_CURSOR_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS continuation_cursors (\n cursor_id TEXT PRIMARY KEY,\n archive_key TEXT NOT NULL,\n archive_path TEXT NOT NULL,\n kind TEXT NOT NULL,\n payload_json TEXT NOT NULL,\n format TEXT NOT NULL,\n limit_value INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n accessed_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_continuation_cursors_expires\nON continuation_cursors(expires_at);\n`;\n\nconst CURSOR_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport async function createContinuationCursor(\n input: ContinuationCursor,\n): Promise<string> {\n const database = await openContinuationCursorDatabase();\n\n try {\n await cleanExpiredContinuationCursors(database);\n\n const now = Date.now();\n const cursorId = await createUniqueCursorId(database);\n\n await database.run(\n `\n INSERT INTO continuation_cursors (\n cursor_id, archive_key, archive_path, kind, payload_json, format,\n limit_value, created_at, expires_at, accessed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `,\n [\n cursorId,\n input.archiveKey,\n input.archivePath,\n input.kind,\n JSON.stringify(createCursorPayload(input)),\n input.format,\n 1,\n now,\n now + CURSOR_TTL_MS,\n now,\n ],\n );\n\n return cursorId;\n } finally {\n await database.close();\n }\n}\n\nexport async function readContinuationCursor(\n cursorId: string,\n): Promise<ContinuationCursor> {\n const database = await openContinuationCursorDatabase();\n\n try {\n await cleanExpiredContinuationCursors(database);\n\n const record = await database.queryOne(\n `\n SELECT archive_key, archive_path, kind, payload_json, format\n FROM continuation_cursors\n WHERE cursor_id = ?\n `,\n [cursorId],\n (row) => ({\n archiveKey: getString(row, \"archive_key\"),\n archivePath: getString(row, \"archive_path\"),\n format: parseCursorFormat(getString(row, \"format\")),\n kind: getString(row, \"kind\"),\n payloadJSON: getString(row, \"payload_json\"),\n }),\n );\n\n if (record === undefined) {\n throw new Error(\n `Continuation cursor ${cursorId} was not found or has expired.`,\n );\n }\n\n await database.run(\n \"UPDATE continuation_cursors SET accessed_at = ? WHERE cursor_id = ?\",\n [Date.now(), cursorId],\n );\n\n return parseContinuationCursorRecord(record);\n } finally {\n await database.close();\n }\n}\n\nfunction createCursorPayload(input: ContinuationCursor): object {\n switch (input.kind) {\n case \"collection\":\n return {\n ...(input.backlinks === undefined\n ? {}\n : { backlinks: input.backlinks }),\n chapters: input.chapters,\n cursor: input.cursor,\n ...(input.evidenceLimit === undefined\n ? {}\n : { evidenceLimit: input.evidenceLimit }),\n ids: input.ids,\n order: input.order,\n ...(input.sourceContext === undefined\n ? {}\n : { sourceContext: input.sourceContext }),\n ...(input.triplePattern === undefined\n ? {}\n : { triplePattern: input.triplePattern }),\n types: input.types,\n };\n case \"search\":\n return {\n ...(input.backlinks === undefined\n ? {}\n : { backlinks: input.backlinks }),\n cursor: input.cursor,\n ...(input.evidenceLimit === undefined\n ? {}\n : { evidenceLimit: input.evidenceLimit }),\n ...(input.query === undefined ? {} : { query: input.query }),\n ...(input.sourceContext === undefined\n ? {}\n : { sourceContext: input.sourceContext }),\n ...(input.triplePattern === undefined\n ? {}\n : { triplePattern: input.triplePattern }),\n types: input.types,\n };\n case \"evidence\":\n return {\n cursor: input.cursor,\n order: input.order,\n ...(input.query === undefined ? {} : { query: input.query }),\n ...(input.sourceContext === undefined\n ? {}\n : { sourceContext: input.sourceContext }),\n targetUri: input.targetUri,\n };\n case \"related\":\n return {\n cursor: input.cursor,\n ...(input.evidenceLimit === undefined\n ? {}\n : { evidenceLimit: input.evidenceLimit }),\n order: input.order,\n ...(input.query === undefined ? {} : { query: input.query }),\n ...(input.role === undefined ? {} : { role: input.role }),\n ...(input.sourceContext === undefined\n ? {}\n : { sourceContext: input.sourceContext }),\n targetUri: input.targetUri,\n };\n }\n}\n\nfunction parseContinuationCursorRecord(record: {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly format: \"json\" | \"jsonl\" | \"text\";\n readonly kind: string;\n readonly payloadJSON: string;\n}): ContinuationCursor {\n const payload = parsePayload(record.payloadJSON);\n\n if (record.kind === \"collection\") {\n return {\n archiveKey: record.archiveKey,\n archivePath: record.archivePath,\n ...getPayloadOptionalBoolean(payload, \"backlinks\"),\n chapters: getPayloadNumberArrayOrNull(payload, \"chapters\"),\n cursor: getPayloadString(payload, \"cursor\"),\n ...getPayloadOptionalPositiveInteger(payload, \"evidenceLimit\"),\n format: record.format,\n ids: getPayloadStringArrayOrNull(payload, \"ids\"),\n kind: \"collection\",\n order: getPayloadOrder(payload),\n ...getPayloadOptionalInteger(payload, \"sourceContext\", \"sourceContext\"),\n ...getPayloadOptionalTriplePattern(payload),\n types: getPayloadStringArrayOrNull(payload, \"types\"),\n };\n }\n\n if (record.kind === \"search\") {\n return {\n archiveKey: record.archiveKey,\n archivePath: record.archivePath,\n ...getPayloadOptionalBoolean(payload, \"backlinks\"),\n cursor: getPayloadString(payload, \"cursor\"),\n ...getPayloadOptionalPositiveInteger(payload, \"evidenceLimit\"),\n format: record.format,\n kind: \"search\",\n ...getPayloadOptionalString(payload, \"query\"),\n ...getPayloadOptionalInteger(payload, \"sourceContext\", \"sourceContext\"),\n ...getPayloadOptionalTriplePattern(payload),\n types: getPayloadStringArrayOrNull(payload, \"types\"),\n };\n }\n\n if (record.kind === \"evidence\") {\n return {\n archiveKey: record.archiveKey,\n archivePath: record.archivePath,\n cursor: getPayloadString(payload, \"cursor\"),\n format: record.format,\n kind: \"evidence\",\n order: getPayloadOrder(payload),\n ...getPayloadOptionalString(payload, \"query\"),\n ...getPayloadOptionalInteger(payload, \"sourceContext\", \"sourceContext\"),\n targetUri: getPayloadString(payload, \"targetUri\"),\n };\n }\n\n if (record.kind === \"related\") {\n return {\n archiveKey: record.archiveKey,\n archivePath: record.archivePath,\n cursor: getPayloadString(payload, \"cursor\"),\n ...getPayloadOptionalPositiveInteger(payload, \"evidenceLimit\"),\n format: record.format,\n kind: \"related\",\n order: getPayloadOrder(payload),\n ...getPayloadOptionalString(payload, \"query\"),\n ...getPayloadOptionalRelatedRole(payload),\n ...getPayloadOptionalInteger(payload, \"sourceContext\", \"sourceContext\"),\n targetUri: getPayloadString(payload, \"targetUri\"),\n };\n }\n\n throw new Error(`Invalid continuation cursor kind: ${record.kind}.`);\n}\n\nfunction parsePayload(value: string): Record<string, unknown> {\n const parsed: unknown = JSON.parse(value);\n\n if (typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadString(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): string {\n const value = payload[key];\n\n if (typeof value === \"string\") {\n return value;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadStringArrayOrNull(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): readonly string[] | null {\n const value = payload[key];\n\n if (value === null) {\n return null;\n }\n if (Array.isArray(value) && value.every((item) => typeof item === \"string\")) {\n return value;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalString(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): { readonly query?: string } {\n const value = payload[key];\n\n if (value === undefined) {\n return {};\n }\n if (typeof value === \"string\") {\n return { query: value };\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadNumberArrayOrNull(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): readonly number[] | null {\n const value = payload[key];\n\n if (value === null) {\n return null;\n }\n if (Array.isArray(value)) {\n const numbers: number[] = [];\n\n for (const item of value) {\n if (typeof item !== \"number\" || !Number.isInteger(item)) {\n throw new Error(\"Invalid continuation cursor payload.\");\n }\n numbers.push(item);\n }\n\n return numbers;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOrder(\n payload: Readonly<Record<string, unknown>>,\n): \"doc-asc\" | \"doc-desc\" {\n const value = payload.order;\n\n if (value === \"doc-asc\" || value === \"doc-desc\") {\n return value;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalPositiveInteger(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): { readonly evidenceLimit?: number } {\n const value = payload[key];\n\n if (value === undefined) {\n return {};\n }\n if (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n return { evidenceLimit: value };\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalInteger<K extends string>(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n outputKey: K,\n): { readonly [P in K]?: number } {\n const value = payload[key];\n\n if (value === undefined) {\n return {};\n }\n if (typeof value === \"number\" && Number.isInteger(value) && value >= 0) {\n return { [outputKey]: value } as { readonly [P in K]?: number };\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalBoolean(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): { readonly backlinks?: boolean } {\n const value = payload[key];\n\n if (value === undefined) {\n return {};\n }\n if (typeof value === \"boolean\") {\n return { backlinks: value };\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalRelatedRole(\n payload: Readonly<Record<string, unknown>>,\n): { readonly role?: \"any\" | \"object\" | \"self\" | \"subject\" } {\n const value = payload.role;\n\n if (value === undefined) {\n return {};\n }\n if (\n value === \"any\" ||\n value === \"object\" ||\n value === \"self\" ||\n value === \"subject\"\n ) {\n return { role: value };\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nfunction getPayloadOptionalTriplePattern(\n payload: Readonly<Record<string, unknown>>,\n): {\n readonly triplePattern?: {\n readonly objectQid?: string;\n readonly predicate?: string;\n readonly subjectQid?: string;\n };\n} {\n const value = payload.triplePattern;\n\n if (value === undefined) {\n return {};\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"Invalid continuation cursor payload.\");\n }\n\n const pattern = value as Record<string, unknown>;\n const subjectQid = getOptionalPayloadStringProperty(pattern, \"subjectQid\");\n const predicate = getOptionalPayloadStringProperty(pattern, \"predicate\");\n const objectQid = getOptionalPayloadStringProperty(pattern, \"objectQid\");\n\n return {\n triplePattern: {\n ...(objectQid === undefined ? {} : { objectQid }),\n ...(predicate === undefined ? {} : { predicate }),\n ...(subjectQid === undefined ? {} : { subjectQid }),\n },\n };\n}\n\nfunction getOptionalPayloadStringProperty(\n payload: Readonly<Record<string, unknown>>,\n key: string,\n): string | undefined {\n const value = payload[key];\n\n if (value === undefined) {\n return undefined;\n }\n if (typeof value === \"string\") {\n return value;\n }\n\n throw new Error(\"Invalid continuation cursor payload.\");\n}\n\nasync function createUniqueCursorId(database: Database): Promise<string> {\n for (let attempt = 0; attempt < 10; attempt += 1) {\n const cursorId = `c_${randomBytes(6).toString(\"base64url\")}`;\n const existing = await database.queryOne(\n \"SELECT cursor_id FROM continuation_cursors WHERE cursor_id = ?\",\n [cursorId],\n (row) => getOptionalString(row, \"cursor_id\"),\n );\n\n if (existing === undefined) {\n return cursorId;\n }\n }\n\n throw new Error(\"Failed to create a unique continuation cursor.\");\n}\n\nasync function cleanExpiredContinuationCursors(\n database: Database,\n): Promise<void> {\n await database.run(\"DELETE FROM continuation_cursors WHERE expires_at < ?\", [\n Date.now(),\n ]);\n}\n\nasync function openContinuationCursorDatabase(): Promise<Database> {\n return await openSharedStateDatabase(\n join(getContinuationStateDirectoryPath(), \"continuation-cursors.sqlite\"),\n CONTINUATION_CURSOR_SCHEMA_SQL,\n );\n}\n\nfunction getContinuationStateDirectoryPath(): string {\n return resolveWikiGraphCacheDirectoryPath();\n}\n\nfunction parseCursorFormat(value: string): \"json\" | \"jsonl\" | \"text\" {\n if (value === \"json\" || value === \"jsonl\" || value === \"text\") {\n return value;\n }\n\n throw new Error(`Invalid continuation cursor format: ${value}.`);\n}\n","export {\n findArchiveObjects,\n formatChapterId,\n formatEdgeId,\n formatNodeId,\n formatSummaryId,\n getArchiveIndex,\n grepArchiveObjects,\n createArchiveSearchIndexFingerprint,\n clearDirtyArchiveSearchIndex,\n isArchiveSearchIndexCurrent,\n readArchiveSearchIndexStatus,\n listAllArchiveLinks,\n listArchiveCollection,\n listArchiveEvidence,\n listArchiveLinks,\n listArchiveObjects,\n listRelatedArchiveObjects,\n packArchiveContext,\n readArchivePage,\n readArchiveText,\n rebuildArchiveSearchIndex,\n} from \"./archive-view.js\";\nexport type {\n ArchiveBacklinkBucket,\n ArchiveBacklinks,\n ArchiveCollectionOptions,\n ArchiveCollectionResult,\n ArchiveCollectionType,\n ArchiveEvidence,\n ArchiveEvidenceItem,\n ArchiveFindEvidencePreview,\n ArchiveFindField,\n ArchiveFindFilterType,\n ArchiveFindHit,\n ArchiveFindObjectType,\n ArchiveFindOptions,\n ArchiveFindOrder,\n ArchiveFindPosition,\n ArchiveFindResult,\n ArchiveIndex,\n ArchiveListItem,\n ArchiveListKind,\n ArchiveNodeLabel,\n ArchiveNodeSourceFragment,\n ArchiveObjectType,\n ArchivePack,\n ArchivePage,\n ArchiveRelatedResult,\n ArchiveTriplePattern,\n} from \"./archive-view.js\";\nexport {\n createContinuationCursor,\n readContinuationCursor,\n} from \"./continuation-cursor.js\";\nexport type { ContinuationCursor } from \"./continuation-cursor.js\";\nexport {\n createSearchSession,\n deleteArchiveSearchSessions,\n readCachedSearchSessionPage,\n runSearchCacheGc,\n} from \"./search-cache.js\";\nexport type {\n SearchSessionDescriptor,\n SearchSessionInput,\n SearchSessionPage,\n} from \"./search-cache.js\";\n","import { existsSync, statSync } from \"fs\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join, parse, resolve } from \"path\";\n\nexport function resolveDataDirPath(): string {\n const injectedPath = (globalThis as { __WIKIGRAPH_DATA_DIR__?: unknown })\n .__WIKIGRAPH_DATA_DIR__;\n\n if (typeof injectedPath === \"string\" && injectedPath !== \"\") {\n return injectedPath;\n }\n\n const moduleDataDirPath = resolveDataDirPathFromModule();\n if (moduleDataDirPath !== undefined) {\n return moduleDataDirPath;\n }\n\n return resolveDataDirPathFromWorkingDirectory();\n}\n\nfunction resolveDataDirPathFromModule(): string | undefined {\n const moduleDirectoryPath = dirname(fileURLToPath(import.meta.url));\n\n for (const candidatePath of [\n resolve(moduleDirectoryPath, \"../../data\"),\n resolve(moduleDirectoryPath, \"../data\"),\n ]) {\n if (existsSync(candidatePath) && statSync(candidatePath).isDirectory()) {\n return candidatePath;\n }\n }\n\n return undefined;\n}\n\nfunction resolveDataDirPathFromWorkingDirectory(): string {\n let currentDirectoryPath = process.cwd();\n const rootDirectoryPath = parse(currentDirectoryPath).root;\n\n while (true) {\n for (const candidatePath of [\n join(currentDirectoryPath, \"data\"),\n join(currentDirectoryPath, \"packages\", \"core\", \"data\"),\n ]) {\n if (existsSync(candidatePath) && statSync(candidatePath).isDirectory()) {\n return candidatePath;\n }\n }\n\n if (currentDirectoryPath === rootDirectoryPath) {\n throw new Error(\"Could not locate data directory\");\n }\n\n currentDirectoryPath = dirname(currentDirectoryPath);\n }\n}\n","import { readFileSync, statSync } from \"fs\";\nimport { resolve, sep } from \"path\";\n\nimport { Environment, Loader, type LoaderSource } from \"nunjucks\";\n\nconst JINJA_EXTENSION_PATTERN = /\\.jinja$/i;\nconst LEADING_DOT_SEGMENT_PATTERN = /^\\.+\\//;\nconst LEADING_SLASH_PATTERN = /^\\/+/;\n\ntype LoaderSourceWithUpdateCheck = LoaderSource & {\n upToDate: () => boolean;\n};\n\nexport function createEnv(\n dirPath: string,\n options: {\n readonly autoescape?: boolean;\n readonly trimBlocks?: boolean;\n } = {},\n): Environment {\n return new Environment(new DSTemplateLoader(resolve(dirPath)), {\n autoescape: options.autoescape ?? true,\n trimBlocks: options.trimBlocks ?? true,\n });\n}\n\nclass DSTemplateLoader extends Loader {\n readonly #dirPath: string;\n\n public constructor(dirPath: string) {\n super();\n this.#dirPath = dirPath;\n }\n\n public getSource(templateName: string): LoaderSourceWithUpdateCheck {\n const normalizedTemplateName = normalizeTemplateName(templateName);\n const targetPath = resolve(this.#dirPath, normalizedTemplateName);\n\n assertWithinRoot(this.#dirPath, targetPath, normalizedTemplateName);\n\n const source = readFileSync(targetPath, \"utf8\");\n const modifiedTime = statSync(targetPath).mtimeMs;\n\n return {\n noCache: false,\n path: targetPath,\n src: source,\n upToDate: () => statSync(targetPath).mtimeMs === modifiedTime,\n };\n }\n}\n\nfunction normalizeTemplateName(templateName: string): string {\n if (LEADING_DOT_SEGMENT_PATTERN.test(templateName)) {\n throw new Error(`invalid path ${templateName}`);\n }\n\n const withoutLeadingSlash = templateName.replace(LEADING_SLASH_PATTERN, \"\");\n const withoutExtension = withoutLeadingSlash.replace(\n JINJA_EXTENSION_PATTERN,\n \"\",\n );\n\n return `${withoutExtension}.jinja`;\n}\n\nfunction assertWithinRoot(\n rootDirPath: string,\n targetPath: string,\n templateName: string,\n): void {\n const rootPrefix = rootDirPath.endsWith(sep)\n ? rootDirPath\n : `${rootDirPath}${sep}`;\n\n if (targetPath === rootDirPath || targetPath.startsWith(rootPrefix)) {\n return;\n }\n\n throw new Error(`cannot find ${templateName}`);\n}\n","import { readFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { isNodeError } from \"../utils/node-error.js\";\n\nexport interface PendingCacheEntry {\n cacheKey: string;\n response: string;\n}\n\nexport class LLMCache {\n readonly #cacheDirPath: string;\n\n public constructor(cacheDirPath: string) {\n this.#cacheDirPath = cacheDirPath;\n }\n\n public createEntry(cacheKey: string, response: string): PendingCacheEntry {\n return {\n cacheKey,\n response,\n };\n }\n\n public async read(cacheKey: string): Promise<string | undefined> {\n try {\n return await readFile(this.#getFilePath(cacheKey), \"utf8\");\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n }\n }\n\n public async write(entry: PendingCacheEntry): Promise<void> {\n await writeFile(this.#getFilePath(entry.cacheKey), entry.response, \"utf8\");\n }\n\n #getFilePath(cacheKey: string): string {\n return join(this.#cacheDirPath, `${cacheKey}.txt`);\n }\n}\n","import { getLogger } from \"../common/logging.js\";\nimport type { LLMCache, PendingCacheEntry } from \"./cache.js\";\nimport type { LLMessage, LLMRequestOptions } from \"./types.js\";\n\nexport interface LLMContextRequestInput<S extends string> {\n messages: readonly LLMessage[];\n options: LLMRequestOptions<S>;\n pendingCacheEntries?: Map<string, PendingCacheEntry>;\n logFiles?: string[];\n}\n\ntype LLMContextRequest<S extends string> = (\n input: LLMContextRequestInput<S>,\n) => Promise<string>;\n\nexport class LLMContext<S extends string> {\n readonly #cache: LLMCache | undefined;\n readonly #pendingCacheEntries = new Map<string, PendingCacheEntry>();\n readonly #requestFn: LLMContextRequest<S>;\n readonly #logFiles: string[] = [];\n #finalized = false;\n public readonly sessionId: number;\n\n public constructor(\n sessionId: number,\n requestFn: LLMContextRequest<S>,\n cache?: LLMCache,\n ) {\n this.sessionId = sessionId;\n this.#cache = cache;\n this.#requestFn = requestFn;\n }\n\n public async request(\n messages: readonly LLMessage[],\n options: LLMRequestOptions<S> = {},\n ): Promise<string> {\n this.#assertActive();\n\n return await this.#requestFn({\n logFiles: this.#logFiles,\n messages,\n options,\n pendingCacheEntries: this.#pendingCacheEntries,\n });\n }\n\n public async commit(): Promise<void> {\n if (this.#finalized) {\n return;\n }\n\n for (const entry of this.#pendingCacheEntries.values()) {\n if (this.#cache !== undefined) {\n await this.#cache.write(entry);\n }\n }\n\n this.#pendingCacheEntries.clear();\n this.#finalized = true;\n }\n\n public rollback(): Promise<void> {\n if (!this.#finalized) {\n this.#pendingCacheEntries.clear();\n this.#finalized = true;\n }\n\n return Promise.resolve();\n }\n\n public async run<T>(\n operation: (context: LLMContext<S>) => Promise<T>,\n ): Promise<T> {\n try {\n const result = await operation(this);\n await this.commit();\n return result;\n } catch (error) {\n await this.rollback();\n this.#printLogFiles();\n throw error;\n }\n }\n\n #assertActive(): void {\n if (this.#finalized) {\n throw new Error(\"LLMContext is already finalized\");\n }\n }\n\n #printLogFiles(): void {\n if (this.#logFiles.length === 0) {\n return;\n }\n\n const logger = getLogger({\n component: \"llm-context\",\n sessionId: this.sessionId,\n });\n\n logger.warn(\n `\\n[LLMContext] Failed with ${this.#logFiles.length} log file(s):`,\n );\n\n for (const [index, logFile] of this.#logFiles.entries()) {\n logger.warn(` ${index + 1}. ${logFile}`);\n }\n }\n}\n","export class LLMPaymentRequiredError extends Error {\n public readonly isRetryable = false;\n public readonly statusCode = 402;\n\n public constructor(\n message = \"LLM payment required.\",\n options?: ErrorOptions,\n ) {\n super(message, options);\n this.name = \"LLMPaymentRequiredError\";\n }\n}\n","import { appendFile } from \"fs/promises\";\nimport { allocateArtifactPath } from \"../common/logging.js\";\n\nexport class RequestLog {\n readonly #filePath: string | undefined;\n\n public constructor(filePath?: string) {\n this.#filePath = filePath;\n }\n\n public get filePath(): string | undefined {\n return this.#filePath;\n }\n\n public async append(content: string): Promise<void> {\n if (this.#filePath === undefined) {\n return;\n }\n await appendFile(this.#filePath, content, \"utf8\");\n }\n}\n\nexport function createRequestLog(logDirPath?: string): RequestLog {\n if (logDirPath === undefined) {\n return new RequestLog();\n }\n\n return new RequestLog(\n allocateArtifactPath({\n alwaysNumbered: true,\n category: \"llm\",\n logDirPath,\n prefix: \"request\",\n }),\n );\n}\n","import type { SamplingScopeConfig, TemperatureSetting } from \"./types.js\";\n\nexport function resolveSamplingSetting(\n value: TemperatureSetting | undefined,\n fieldName: string,\n retryIndex?: number,\n retryMax?: number,\n): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n if (typeof value === \"number\") {\n return value;\n }\n\n if (value.length === 1) {\n return value[0];\n }\n\n if (value.length !== 2) {\n throw new RangeError(\n `${fieldName} must be a number or a 2-item range like [0.6, 0.98]`,\n );\n }\n const [start, end] = value;\n\n if (start === undefined || end === undefined) {\n throw new RangeError(\n `${fieldName} must be a number or a 2-item range like [0.6, 0.98]`,\n );\n }\n\n if (retryIndex === undefined || retryMax === undefined || retryMax <= 0) {\n return start;\n }\n const boundedRetryIndex = Math.min(Math.max(retryIndex, 0), retryMax);\n const progress = boundedRetryIndex / retryMax;\n\n return start + (end - start) * progress;\n}\n\nexport function resolveTemperatureSetting(\n temperature: TemperatureSetting | undefined,\n retryIndex?: number,\n retryMax?: number,\n): number | undefined {\n return resolveSamplingSetting(\n temperature,\n \"temperature\",\n retryIndex,\n retryMax,\n );\n}\n\nexport function getScopeDefaults<S extends string>(\n scope: S | undefined,\n sampling: SamplingScopeConfig<S> | undefined,\n defaultTemperature: TemperatureSetting,\n defaultTopP: TemperatureSetting,\n): {\n temperature: TemperatureSetting;\n topP: TemperatureSetting;\n} {\n if (scope === undefined) {\n return {\n temperature: defaultTemperature,\n topP: defaultTopP,\n };\n }\n\n if (sampling === undefined) {\n return {\n temperature: defaultTemperature,\n topP: defaultTopP,\n };\n }\n\n const profile = sampling[scope];\n\n if (profile === undefined) {\n return {\n temperature: defaultTemperature,\n topP: defaultTopP,\n };\n }\n\n return {\n temperature: profile.temperature ?? defaultTemperature,\n topP: profile.topP ?? defaultTopP,\n };\n}\n","import { existsSync, mkdirSync, statSync } from \"fs\";\nimport { resolve } from \"path\";\nimport { setTimeout as sleep } from \"timers/promises\";\n\nimport {\n APICallError,\n generateText,\n streamText,\n type LanguageModel,\n type LanguageModelUsage,\n type ModelMessage,\n type SystemModelMessage,\n} from \"ai\";\nimport type { Environment } from \"nunjucks\";\n\nimport { getLogger } from \"../common/logging.js\";\nimport { createEnv } from \"../common/template.js\";\nimport { AsyncSemaphore } from \"../utils/async-semaphore.js\";\nimport { createHash } from \"../utils/hash.js\";\nimport { formatError } from \"../utils/node-error.js\";\nimport { LLMCache } from \"./cache.js\";\nimport { LLMContext, type LLMContextRequestInput } from \"./context.js\";\nimport { LLMPaymentRequiredError } from \"./errors.js\";\nimport { createRequestLog } from \"./request-log.js\";\nimport { getScopeDefaults, resolveSamplingSetting } from \"./sampling.js\";\nimport type {\n LLMessage,\n LLMModel,\n LLMOptions,\n LLMLazyRequestOperation,\n LLMRequestOptions,\n LLMRequestFunction,\n LLMStreamProgressCallback,\n LLMTokenUsageCallback,\n SamplingScopeConfig,\n TemperatureSetting,\n} from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 360_000;\nconst DEFAULT_CONCURRENT_REQUESTS = 6;\nconst ABORT_ERROR_NAMES = new Set([\n \"AbortError\",\n \"ResponseAborted\",\n \"TimeoutError\",\n]);\nconst RETRYABLE_HTTP_STATUS_CODES = new Set([\n 408, 409, 425, 429, 500, 502, 503, 504, 520, 522, 524, 529,\n]);\n// undici commonly reports transient transport failures through low-level error\n// codes and generic `terminated` fetch errors instead of retryable HTTP\n// responses. The issues below are representative cases we want to treat as\n// retryable at our LLM boundary:\n// https://github.com/nodejs/undici/issues/1490\n// https://github.com/nodejs/undici/issues/1414\n// https://github.com/nodejs/undici/issues/1531\n// https://github.com/nodejs/undici/issues/1864\n// https://github.com/nodejs/undici/issues/2362\n// https://github.com/nodejs/undici/issues/3410\n// https://github.com/nodejs/undici/issues/4215\nconst RETRYABLE_ERROR_CODES = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"EAI_AGAIN\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\nconst RETRYABLE_ERROR_KEYWORDS = [\n \"connection\",\n \"terminated\",\n \"timeout\",\n \"network\",\n \"rate limit\",\n];\n\nlet contextIdCounter = 0;\n\ntype LLMRequestSessionInput<S extends string> = Omit<\n LLMContextRequestInput<S>,\n \"options\"\n> &\n LLMRequestOptions<S> & {\n sessionId?: number;\n };\n\nexport class LLM<S extends string> {\n readonly #cache: LLMCache | undefined;\n readonly #lazyRequestLimiter: AsyncSemaphore;\n readonly #logDirPath: string | undefined;\n readonly #model: LLMModel;\n readonly #modelProvider: string | undefined;\n readonly #modelId: string;\n readonly #modelIdentity: string;\n readonly #onStreamProgress: LLMStreamProgressCallback | undefined;\n readonly #onTokenUsage: LLMTokenUsageCallback | undefined;\n readonly #requestLimiter: AsyncSemaphore;\n readonly #retryIntervalSeconds: number;\n readonly #retryTimes: number;\n readonly #sampling: SamplingScopeConfig<S> | undefined;\n readonly #stream: boolean;\n readonly #templateEnvironment: Environment;\n readonly #temperature: TemperatureSetting;\n readonly #timeoutMs: number;\n readonly #topP: TemperatureSetting;\n\n public readonly config: Readonly<{\n concurrent: number;\n provider?: string;\n modelId: string;\n sampling?: SamplingScopeConfig<S>;\n stream: boolean;\n timeout: number;\n temperature: TemperatureSetting;\n topP: TemperatureSetting;\n }>;\n\n public constructor(options: LLMOptions<S>) {\n const concurrent = options.concurrent ?? DEFAULT_CONCURRENT_REQUESTS;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n const temperature = options.temperature ?? 0.6;\n const topP = options.topP ?? 0.6;\n const sampling = options.sampling;\n const stream = options.stream ?? false;\n const modelInfo = resolveModelInfo(options.model);\n\n this.config = Object.freeze({\n concurrent,\n modelId: modelInfo.modelId,\n temperature,\n stream,\n timeout,\n topP,\n ...(modelInfo.provider === undefined\n ? {}\n : { provider: modelInfo.provider }),\n ...(sampling === undefined ? {} : { sampling }),\n });\n this.#cache = createCache(options.cacheDirPath);\n this.#lazyRequestLimiter = new AsyncSemaphore(concurrent);\n this.#logDirPath = ensureDirectoryPath(options.logDirPath);\n this.#model = options.model;\n this.#modelProvider = modelInfo.provider;\n this.#modelId = modelInfo.modelId;\n this.#modelIdentity = modelInfo.identity;\n this.#onStreamProgress = options.onStreamProgress;\n this.#onTokenUsage = options.onTokenUsage;\n this.#requestLimiter = new AsyncSemaphore(concurrent);\n this.#retryIntervalSeconds = options.retryIntervalSeconds ?? 6;\n this.#retryTimes = options.retryTimes ?? 5;\n this.#sampling = sampling;\n this.#stream = stream;\n this.#templateEnvironment = createEnv(options.dataDirPath);\n this.#temperature = temperature;\n this.#timeoutMs = timeout;\n this.#topP = topP;\n }\n\n public context(): LLMContext<S> {\n contextIdCounter += 1;\n const sessionId = contextIdCounter;\n\n return new LLMContext(\n sessionId,\n async (input) =>\n await this.#requestWithSession({\n ...input.options,\n messages: input.messages,\n sessionId,\n ...(input.logFiles === undefined ? {} : { logFiles: input.logFiles }),\n ...(input.pendingCacheEntries === undefined\n ? {}\n : { pendingCacheEntries: input.pendingCacheEntries }),\n }),\n this.#cache,\n );\n }\n\n public async withContext<T>(\n operation: (context: LLMContext<S>) => Promise<T>,\n ): Promise<T> {\n return await this.context().run(operation);\n }\n\n public async request(\n messages: readonly LLMessage[],\n options?: LLMRequestOptions<S>,\n ): Promise<string>;\n public async request<T>(operation: LLMLazyRequestOperation<S, T>): Promise<T>;\n public async request<T>(\n input: readonly LLMessage[] | LLMLazyRequestOperation<S, T>,\n options: LLMRequestOptions<S> = {},\n ): Promise<string | T> {\n if (typeof input === \"function\") {\n return await this.#lazyRequestLimiter.use(\n async () => await input(this.#requestOnce),\n );\n }\n\n return await this.#requestOnce(input, options);\n }\n\n readonly #requestOnce: LLMRequestFunction<S> = async (\n messages,\n options = {},\n ) =>\n await this.#requestWithSession({\n messages,\n ...options,\n });\n\n public loadSystemPrompt(\n templateName: string,\n templateContext: Record<string, unknown> = {},\n ): string {\n return this.#templateEnvironment.render(templateName, templateContext);\n }\n\n async #requestWithSession(input: LLMRequestSessionInput<S>): Promise<string> {\n const defaultSampling = getScopeDefaults(\n input.scope,\n this.#sampling,\n this.#temperature,\n this.#topP,\n );\n const temperature = input.temperature ?? defaultSampling.temperature;\n const topP = input.topP ?? defaultSampling.topP;\n const resolvedTemperature = resolveSamplingSetting(\n temperature,\n \"temperature\",\n input.retryIndex,\n input.retryMax,\n );\n const resolvedTopP = resolveSamplingSetting(\n topP,\n \"top_p\",\n input.retryIndex,\n input.retryMax,\n );\n const useCache =\n (input.useCache ?? true) && hasVisibleNonSystemContent(input.messages);\n const cacheKey =\n this.#cache !== undefined && useCache\n ? createHash({\n messages: input.messages.map((message) => ({\n content: message.content,\n role: message.role,\n })),\n modelId: this.#modelId,\n provider: this.#modelProvider ?? null,\n temperature: resolvedTemperature ?? null,\n topP: resolvedTopP ?? null,\n })\n : undefined;\n const requestLog = createRequestLog(this.#logDirPath);\n\n if (requestLog.filePath !== undefined && input.logFiles !== undefined) {\n input.logFiles.push(requestLog.filePath);\n }\n\n await requestLog.append(\n formatRequestParameters({\n cacheKey,\n modelId: this.#modelId,\n modelIdentity: this.#modelIdentity,\n modelProvider: this.#modelProvider,\n resolvedTemperature,\n resolvedTopP,\n retryIndex: input.retryIndex,\n retryMax: input.retryMax,\n scope: input.scope,\n sessionId: input.sessionId,\n temperature,\n topP,\n }),\n );\n await requestLog.append(formatRequestMessages(input.messages));\n\n if (cacheKey !== undefined && this.#cache !== undefined && useCache) {\n const cachedResponse = await this.#cache.read(cacheKey);\n\n if (cachedResponse !== undefined) {\n getLogger({\n component: \"llm\",\n scope: input.scope,\n sessionId: input.sessionId,\n }).info(\n `[Cache Hit] Using cached response (key: ${cacheKey.slice(0, 12)}...)`,\n );\n await requestLog.append(\n `[[Response]] (from cache):\\n${cachedResponse}\\n\\n`,\n );\n await requestLog.append(formatRequestResultLog(\"cache-hit\"));\n return cachedResponse;\n }\n }\n\n let response: string | undefined;\n let tokenUsage: LanguageModelUsage | undefined;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.#retryTimes; attempt += 1) {\n try {\n response = await this.#requestLimiter.use(async () => {\n const generationInput: {\n abortSignal?: AbortSignal;\n maxRetries: number;\n messages: ModelMessage[];\n model: LanguageModel;\n system?: string | SystemModelMessage | SystemModelMessage[];\n temperature?: number;\n timeout?: number;\n topP?: number;\n } = normalizeGenerationInput({\n maxRetries: 0,\n messages: [...input.messages],\n model: this.#model,\n timeout: this.#timeoutMs,\n });\n\n if (input.signal !== undefined) {\n generationInput.abortSignal = input.signal;\n }\n\n if (resolvedTemperature !== undefined) {\n generationInput.temperature = resolvedTemperature;\n }\n\n if (resolvedTopP !== undefined) {\n generationInput.topP = resolvedTopP;\n }\n\n if (this.#stream) {\n const textChunks: string[] = [];\n const result = streamText(generationInput);\n\n for await (const chunk of result.textStream) {\n textChunks.push(chunk);\n await this.#emitStreamProgress(chunk.length);\n }\n tokenUsage = await result.totalUsage;\n await this.#emitTokenUsage(tokenUsage);\n return textChunks.join(\"\");\n } else {\n const result = await generateText(generationInput);\n\n tokenUsage = result.usage;\n await this.#emitStreamProgress(result.text.length);\n await this.#emitTokenUsage(tokenUsage);\n return result.text;\n }\n });\n\n await requestLog.append(`[[Response]]:\\n${response}\\n\\n`);\n break;\n } catch (error) {\n lastError = error;\n\n if (isPaymentRequiredError(error)) {\n const paymentError = new LLMPaymentRequiredError(undefined, {\n cause: error,\n });\n\n await requestLog.append(\n `[[Error]]:\\n${formatError(paymentError)}\\n\\n`,\n );\n await requestLog.append(formatRequestResultLog(tokenUsage, error));\n throw paymentError;\n }\n\n if (!isRetryableError(error)) {\n await requestLog.append(`[[Error]]:\\n${formatError(error)}\\n\\n`);\n await requestLog.append(formatRequestResultLog(tokenUsage, error));\n throw error;\n }\n\n await requestLog.append(\n `[[Warning]]:\\nRequest failed with connection error, retrying... (${attempt + 1} times)\\n\\n`,\n );\n\n if (attempt < this.#retryTimes && this.#retryIntervalSeconds > 0) {\n await sleep(this.#retryIntervalSeconds * 1000, undefined, {\n signal: input.signal,\n });\n }\n }\n }\n\n if (response === undefined) {\n const failureMessage =\n lastError === undefined\n ? `LLM request failed after ${this.#retryTimes + 1} attempts`\n : `LLM request failed after ${this.#retryTimes + 1} attempts: ${formatError(lastError)}`;\n\n await requestLog.append(`[[Error]]:\\n${failureMessage}\\n\\n`);\n await requestLog.append(formatRequestResultLog(tokenUsage, lastError));\n\n throw new Error(failureMessage, {\n ...(lastError === undefined ? {} : { cause: lastError }),\n });\n }\n\n if (cacheKey !== undefined && this.#cache !== undefined && useCache) {\n const entry = this.#cache.createEntry(cacheKey, response);\n\n if (\n input.sessionId !== undefined &&\n input.pendingCacheEntries !== undefined\n ) {\n input.pendingCacheEntries.set(entry.cacheKey, entry);\n } else {\n await this.#cache.write(entry);\n }\n }\n\n await requestLog.append(formatRequestResultLog(tokenUsage));\n return response;\n }\n\n async #emitStreamProgress(outputCharacters: number): Promise<void> {\n if (this.#onStreamProgress === undefined) {\n return;\n }\n\n try {\n await this.#onStreamProgress({ outputCharacters });\n } catch {\n return;\n }\n }\n\n async #emitTokenUsage(usage: LanguageModelUsage | undefined): Promise<void> {\n if (this.#onTokenUsage === undefined || usage === undefined) {\n return;\n }\n\n try {\n await this.#onTokenUsage({\n ...(usage.inputTokenDetails.cacheReadTokens === undefined\n ? {}\n : { cacheReadTokens: usage.inputTokenDetails.cacheReadTokens }),\n ...(usage.inputTokens === undefined\n ? {}\n : { inputTokens: usage.inputTokens }),\n ...(usage.outputTokens === undefined\n ? {}\n : { outputTokens: usage.outputTokens }),\n });\n } catch {\n return;\n }\n }\n}\n\nfunction normalizeGenerationInput(input: {\n maxRetries: number;\n messages: ModelMessage[];\n model: LanguageModel;\n timeout: number;\n}): {\n maxRetries: number;\n messages: ModelMessage[];\n model: LanguageModel;\n system?: string | SystemModelMessage | SystemModelMessage[];\n timeout: number;\n} {\n const systemMessages: SystemModelMessage[] = [];\n let firstNonSystemIndex = 0;\n\n while (firstNonSystemIndex < input.messages.length) {\n const message = input.messages[firstNonSystemIndex];\n\n if (message === undefined || message.role !== \"system\") {\n break;\n }\n\n systemMessages.push(message);\n firstNonSystemIndex += 1;\n }\n\n if (systemMessages.length === 0) {\n return input;\n }\n\n if (systemMessages.length === 1) {\n const [systemMessage] = systemMessages;\n\n if (systemMessage === undefined) {\n return input;\n }\n\n return {\n ...input,\n messages: input.messages.slice(firstNonSystemIndex),\n system: systemMessage,\n };\n }\n\n return {\n ...input,\n messages: input.messages.slice(firstNonSystemIndex),\n system: systemMessages,\n };\n}\n\nfunction ensureDirectoryPath(dirPath?: string): string | undefined {\n if (dirPath === undefined) {\n return undefined;\n }\n\n const resolvedDirPath = resolve(dirPath);\n\n if (!existsSync(resolvedDirPath)) {\n mkdirSync(resolvedDirPath, { recursive: true });\n return resolvedDirPath;\n }\n\n if (!statSync(resolvedDirPath).isDirectory()) {\n return undefined;\n }\n\n return resolvedDirPath;\n}\n\nfunction createCache(cacheDirPath?: string): LLMCache | undefined {\n const resolvedCacheDirPath = ensureDirectoryPath(cacheDirPath);\n\n if (resolvedCacheDirPath === undefined) {\n return undefined;\n }\n\n return new LLMCache(resolvedCacheDirPath);\n}\n\nfunction hasVisibleNonSystemContent(messages: readonly LLMessage[]): boolean {\n return messages.some(\n (message) =>\n message.role !== \"system\" &&\n (typeof message.content !== \"string\" || message.content.trim() !== \"\"),\n );\n}\n\nfunction formatRequestParameters(input: {\n cacheKey?: string | undefined;\n modelId: string;\n modelIdentity: string;\n modelProvider?: string | undefined;\n resolvedTemperature: number | undefined;\n resolvedTopP: number | undefined;\n retryIndex?: number | undefined;\n retryMax?: number | undefined;\n scope?: string | undefined;\n sessionId?: number | undefined;\n temperature: TemperatureSetting;\n topP: TemperatureSetting;\n}): string {\n const lines = [\n \"[[Parameters]]:\",\n `\\tmodel=${input.modelIdentity}`,\n `\\ttemperature=${String(input.resolvedTemperature)}`,\n `\\ttop_p=${String(input.resolvedTopP)}`,\n ];\n\n if (input.modelProvider !== undefined) {\n lines.push(`\\tprovider=${input.modelProvider}`);\n }\n\n lines.push(`\\tmodel_id=${input.modelId}`);\n\n if (input.scope !== undefined) {\n lines.push(`\\tscope=${input.scope}`);\n }\n\n if (Array.isArray(input.temperature)) {\n lines.push(`\\ttemperature_schedule=${JSON.stringify(input.temperature)}`);\n }\n\n if (Array.isArray(input.topP)) {\n lines.push(`\\ttop_p_schedule=${JSON.stringify(input.topP)}`);\n }\n\n if (input.retryIndex !== undefined && input.retryMax !== undefined) {\n lines.push(`\\tretry_progress=${input.retryIndex}/${input.retryMax}`);\n }\n\n if (input.cacheKey !== undefined) {\n lines.push(`\\tcache_key=${input.cacheKey}`);\n }\n\n if (input.sessionId !== undefined) {\n lines.push(`\\tsession_id=${input.sessionId}`);\n }\n\n return `${lines.join(\"\\n\")}\\n\\n`;\n}\n\nfunction formatRequestMessages(messages: readonly LLMessage[]): string {\n const body = messages\n .map(\n (message) =>\n `${capitalize(message.role)}:\\n${formatMessageContent(message.content)}`,\n )\n .join(\"\\n\\n\");\n\n return `[[Request]]:\\n${body}\\n\\n`;\n}\n\nfunction formatRequestResultLog(\n usage: LanguageModelUsage | \"cache-hit\" | undefined,\n error?: unknown,\n): string {\n return [\n formatRequestUsageLog(usage),\n ...(error === undefined ? [] : [formatRequestErrorStackLog(error)]),\n ].join(\"\");\n}\n\nfunction formatRequestUsageLog(\n usage: LanguageModelUsage | \"cache-hit\" | undefined,\n): string {\n if (usage === \"cache-hit\") {\n return \"[[Usage]]:\\ncache-hit\\n\\n\";\n }\n\n return [\n \"[[Usage]]:\",\n `input: ${formatTokenCount(usage?.inputTokens)}`,\n `cache: ${formatTokenCount(usage?.inputTokenDetails.cacheReadTokens)}`,\n `output: ${formatTokenCount(usage?.outputTokens)}`,\n \"\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatTokenCount(value: number | undefined): string {\n return value === undefined ? \"unavailable\" : String(value);\n}\n\nfunction formatRequestErrorStackLog(error: unknown): string {\n return `[[Error Stack]]:\\n${formatErrorStack(error)}\\n\\n`;\n}\n\nfunction formatErrorStack(error: unknown): string {\n if (error instanceof Error) {\n return error.stack ?? formatError(error);\n }\n\n return formatError(error);\n}\n\nfunction formatMessageContent(content: LLMessage[\"content\"]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n const serializedContent = JSON.stringify(content, null, 2);\n\n if (typeof serializedContent === \"string\") {\n return serializedContent;\n }\n\n return \"\";\n}\n\nfunction capitalize(value: string): string {\n if (value.length === 0) {\n return value;\n }\n\n return `${value[0]?.toUpperCase() ?? \"\"}${value.slice(1)}`;\n}\n\nfunction isRetryableError(error: unknown): boolean {\n if (APICallError.isInstance(error)) {\n if (isPaymentRequiredError(error)) {\n return false;\n }\n\n if (error.isRetryable) {\n return true;\n }\n\n return isRetryableStatusCode(error.statusCode);\n }\n\n return !isAbortLikeError(error) && isRetryableTransportError(error);\n}\n\nfunction isPaymentRequiredError(error: unknown): boolean {\n return APICallError.isInstance(error) && error.statusCode === 402;\n}\n\nfunction isRetryableStatusCode(statusCode: number | undefined): boolean {\n return (\n typeof statusCode === \"number\" &&\n RETRYABLE_HTTP_STATUS_CODES.has(statusCode)\n );\n}\n\nfunction isAbortLikeError(error: unknown): boolean {\n return someErrorInChain(error, (currentError) =>\n ABORT_ERROR_NAMES.has(currentError.name),\n );\n}\n\nfunction isRetryableTransportError(error: unknown): boolean {\n return someErrorInChain(error, (currentError) => {\n const nodeError = currentError as NodeJS.ErrnoException;\n const errorCode =\n typeof nodeError.code === \"string\"\n ? nodeError.code.toUpperCase()\n : undefined;\n\n if (errorCode !== undefined && RETRYABLE_ERROR_CODES.has(errorCode)) {\n return true;\n }\n\n const errorMessage = currentError.message.toLowerCase();\n\n return RETRYABLE_ERROR_KEYWORDS.some((keyword) =>\n errorMessage.includes(keyword),\n );\n });\n}\n\nfunction someErrorInChain(\n error: unknown,\n matcher: (error: Error) => boolean,\n): boolean {\n const visited = new Set<unknown>();\n let current: unknown = error;\n\n while (current instanceof Error && !visited.has(current)) {\n if (matcher(current)) {\n return true;\n }\n\n visited.add(current);\n current = current.cause;\n }\n\n return false;\n}\n\nfunction resolveModelInfo(model: LLMModel): {\n readonly identity: string;\n readonly modelId: string;\n readonly provider?: string;\n} {\n if (typeof model === \"string\") {\n return {\n identity: model,\n modelId: model,\n };\n }\n\n if (hasModelMetadata(model)) {\n return {\n identity:\n model.provider === undefined\n ? model.modelId\n : `${model.provider}:${model.modelId}`,\n modelId: model.modelId,\n ...(model.provider === undefined ? {} : { provider: model.provider }),\n };\n }\n\n return {\n identity: \"unknown-model\",\n modelId: \"unknown-model\",\n };\n}\n\nfunction hasModelMetadata(\n model: LLMModel,\n): model is LLMModel & { modelId: string; provider?: string } {\n return (\n typeof model === \"object\" &&\n model !== null &&\n \"modelId\" in model &&\n typeof model.modelId === \"string\" &&\n (!(\"provider\" in model) || typeof model.provider === \"string\")\n );\n}\n","export { LLM } from \"./client.js\";\nexport { LLMContext } from \"./context.js\";\nexport { LLMPaymentRequiredError } from \"./errors.js\";\nexport {\n getScopeDefaults,\n resolveSamplingSetting,\n resolveTemperatureSetting,\n} from \"./sampling.js\";\nexport type {\n LLMessage,\n LLMLazyRequestOperation,\n LLMModel,\n LLMOptions,\n LLMRequestFunction,\n LLMRequestOptions,\n LLMStreamProgressCallback,\n LLMTokenUsage,\n LLMTokenUsageCallback,\n SamplingProfile,\n SamplingScopeConfig,\n TemperatureSetting,\n} from \"./types.js\";\n","import { getLogger } from \"../common/logging.js\";\n\nimport type {\n DigestProgressEvent,\n SerialProgressEvent,\n WikiGraphOperation,\n WikiGraphProgressCallback,\n WikiGraphProgressEvent,\n} from \"./types.js\";\n\nexport class ProgressReporter {\n readonly #callback: WikiGraphProgressCallback | undefined;\n readonly #operation: WikiGraphOperation;\n\n public constructor(\n operation: WikiGraphOperation,\n callback?: WikiGraphProgressCallback,\n ) {\n this.#callback = callback;\n this.#operation = operation;\n }\n\n public async emit(event: WikiGraphProgressEvent): Promise<void> {\n getLogger({\n component: \"progress\",\n eventType: event.type,\n operation: this.#operation,\n ...buildLogBindings(event),\n }).info(buildLogMessage(event));\n\n if (this.#callback === undefined) {\n return;\n }\n\n try {\n await this.#callback(event);\n } catch (error) {\n getLogger({\n component: \"progress\",\n operation: this.#operation,\n }).warn(\n {\n error:\n error instanceof Error\n ? { message: error.message, name: error.name }\n : String(error),\n },\n \"Progress callback failed\",\n );\n }\n }\n}\n\nexport function createProgressReporter(\n operation: WikiGraphOperation,\n callback?: WikiGraphProgressCallback,\n): ProgressReporter {\n return new ProgressReporter(operation, callback);\n}\n\nfunction buildLogBindings(\n event: WikiGraphProgressEvent,\n): Record<string, number> {\n switch (event.type) {\n case \"serials-discovered\":\n return {\n available: Number(event.available),\n serials: event.serials.length,\n totalFragments: event.serials.reduce(\n (sum, serial) => sum + (serial.fragments ?? 0),\n 0,\n ),\n totalWords: event.serials.reduce(\n (sum, serial) => sum + serial.words,\n 0,\n ),\n } satisfies Record<string, number>;\n case \"serial-progress\":\n return {\n completedFragments: event.completedFragments,\n completedWords: event.completedWords,\n id: event.id,\n } satisfies Record<keyof Omit<SerialProgressEvent, \"type\">, number>;\n case \"digest-progress\":\n return {\n completedWords: event.completedWords,\n totalWords: event.totalWords,\n } satisfies Record<keyof Omit<DigestProgressEvent, \"type\">, number>;\n }\n}\n\nfunction buildLogMessage(event: WikiGraphProgressEvent): string {\n switch (event.type) {\n case \"serials-discovered\":\n return event.available\n ? `Discovered ${event.serials.length} serials`\n : \"Serial discovery unavailable\";\n case \"serial-progress\":\n return `Serial ${event.id} progressed`;\n case \"digest-progress\":\n return \"Digest progressed\";\n }\n}\n","import { createProgressReporter, type ProgressReporter } from \"./reporter.js\";\nimport type {\n SerialDiscoveryItem,\n WikiGraphOperation,\n WikiGraphProgressCallback,\n} from \"./types.js\";\n\nexport interface CreateDigestProgressTrackerOptions {\n readonly onProgress?: WikiGraphProgressCallback;\n readonly operation: WikiGraphOperation;\n}\n\nexport class DigestProgressTracker {\n readonly #reporter: ProgressReporter;\n #completedWords = 0;\n readonly #completedWordsBySerial = new Map<number, number>();\n #totalWords = 0;\n #discoverySettled = false;\n\n public constructor(options: CreateDigestProgressTrackerOptions) {\n this.#reporter = createProgressReporter(\n options.operation,\n options.onProgress,\n );\n }\n\n public createSerialTracker(input: {\n readonly id: number;\n }): SerialProgressTracker {\n return new SerialProgressTracker(this, input.id);\n }\n\n public async discoverSerials(\n serials: readonly SerialDiscoveryItem[],\n ): Promise<void> {\n this.#ensureDiscoveryOpen();\n this.#discoverySettled = true;\n this.#totalWords = serials.reduce((sum, serial) => sum + serial.words, 0);\n\n await this.#reporter.emit({\n available: true,\n serials,\n type: \"serials-discovered\",\n });\n await this.#emitDigestProgress();\n }\n\n public async markDiscoveryUnavailable(): Promise<void> {\n this.#ensureDiscoveryOpen();\n this.#discoverySettled = true;\n\n await this.#reporter.emit({\n available: false,\n serials: [],\n type: \"serials-discovered\",\n });\n }\n\n public async completeSerial(input: {\n readonly id: number;\n readonly words: number;\n }): Promise<void> {\n const previousCompletedWords =\n this.#completedWordsBySerial.get(input.id) ?? 0;\n\n if (input.words !== previousCompletedWords) {\n this.#completedWordsBySerial.set(input.id, input.words);\n this.#completedWords += input.words - previousCompletedWords;\n }\n\n const nextTotalWords = Math.max(\n this.#totalWords,\n this.#completedWords,\n input.words,\n );\n\n if (\n nextTotalWords === this.#totalWords &&\n input.words === previousCompletedWords\n ) {\n return;\n }\n\n this.#totalWords = nextTotalWords;\n await this.#emitDigestProgress();\n }\n\n public async emitSerialProgress(input: {\n readonly completedFragments: number;\n readonly completedWords: number;\n readonly id: number;\n }): Promise<void> {\n await this.#reporter.emit({\n completedFragments: input.completedFragments,\n completedWords: input.completedWords,\n id: input.id,\n type: \"serial-progress\",\n });\n }\n\n async #emitDigestProgress(): Promise<void> {\n await this.#reporter.emit({\n completedWords: this.#completedWords,\n totalWords: this.#totalWords,\n type: \"digest-progress\",\n });\n }\n\n #ensureDiscoveryOpen(): void {\n if (this.#discoverySettled) {\n throw new Error(\"Serial discovery has already been reported\");\n }\n }\n}\n\nexport class SerialProgressTracker {\n #completedFragments = 0;\n readonly #digestTracker: DigestProgressTracker;\n #completedWords = 0;\n readonly #id: number;\n\n public constructor(digestTracker: DigestProgressTracker, id: number) {\n this.#digestTracker = digestTracker;\n this.#id = id;\n }\n\n public async begin(_input?: {\n readonly fragments: number;\n readonly words: number;\n }): Promise<void> {}\n\n public async advance(wordsCount: number): Promise<void> {\n this.#completedWords += wordsCount;\n this.#completedFragments += 1;\n await this.#digestTracker.emitSerialProgress({\n completedFragments: this.#completedFragments,\n completedWords: this.#completedWords,\n id: this.#id,\n });\n }\n\n public async complete(finalWordsCount = 0): Promise<void> {\n this.#completedWords += finalWordsCount;\n if (finalWordsCount > 0) {\n this.#completedFragments += 1;\n }\n\n await this.#digestTracker.emitSerialProgress({\n completedFragments: this.#completedFragments,\n completedWords: this.#completedWords,\n id: this.#id,\n });\n await this.#digestTracker.completeSerial({\n id: this.#id,\n words: this.#completedWords,\n });\n }\n}\n\nexport function createDigestProgressTracker(\n options: CreateDigestProgressTrackerOptions,\n): DigestProgressTracker {\n return new DigestProgressTracker(options);\n}\n","export {\n createDigestProgressTracker,\n DigestProgressTracker,\n SerialProgressTracker,\n type CreateDigestProgressTrackerOptions,\n} from \"./tracker.js\";\nexport { createProgressReporter, ProgressReporter } from \"./reporter.js\";\nexport type {\n DigestProgressEvent,\n SerialDiscoveryItem,\n SerialsDiscoveredEvent,\n SerialProgressEvent,\n WikiGraphOperation,\n WikiGraphProgressCallback,\n WikiGraphProgressEvent,\n WikiGraphProgressEventType,\n} from \"./types.js\";\n","import type { Document } from \"../document/index.js\";\nimport type { DigestProgressTracker } from \"../progress/index.js\";\nimport { AsyncSemaphore } from \"../utils/async-semaphore.js\";\nimport {\n SerialGeneration,\n discoverSerial,\n type GenerateSerialOptions,\n type SerialGenerationOptions,\n writeSerialSource,\n} from \"../serial.js\";\nimport {\n TOC_FILE_VERSION,\n type BookMeta,\n type SourceAdapter,\n type SourceAsset,\n type SourceDocument,\n type SourceSection,\n type TocFile,\n type TocItem,\n} from \"../source/index.js\";\nimport type { ChapterStage } from \"./chapter.js\";\n\nexport interface ImportSourceOptions\n extends\n GenerateSerialOptions,\n Omit<SerialGenerationOptions, \"document\" | \"llm\"> {\n readonly adapter: SourceAdapter;\n readonly digestProgressTracker?: DigestProgressTracker;\n readonly document: Document;\n readonly llm?: SerialGenerationOptions[\"llm\"];\n readonly path: string;\n readonly targetStage?: ImportSourceStage;\n}\n\nexport interface ImportedSource {\n readonly cover: SourceAsset | undefined;\n readonly meta: BookMeta;\n readonly serials: readonly ImportedSerial[];\n readonly toc: TocFile;\n}\n\nexport interface ImportedSerial {\n readonly id: number;\n}\n\nexport type ImportSourceStage = ChapterStage;\n\ninterface PlannedSection {\n readonly section: SourceSection;\n readonly serialId: number;\n}\n\ninterface SerialIdAllocator {\n nextSerialId: number;\n}\n\nexport async function importSource(\n options: ImportSourceOptions,\n): Promise<ImportedSource> {\n return await options.adapter.openSession(\n options.path,\n async (sourceDocument) => {\n return await importSourceDocument(sourceDocument, options);\n },\n );\n}\n\nexport async function importSourceDocument(\n sourceDocument: SourceDocument,\n options: Omit<ImportSourceOptions, \"adapter\" | \"path\">,\n): Promise<ImportedSource> {\n await assertImportTargetIsEmpty(options.document);\n\n const meta = await sourceDocument.readMeta();\n const cover = await sourceDocument.readCover();\n const sections = await sourceDocument.readSections();\n const plannedSections: PlannedSection[] = [];\n const serialIdAllocator = {\n nextSerialId: await options.document.peekNextSerialId(),\n } satisfies SerialIdAllocator;\n const toc = {\n version: TOC_FILE_VERSION,\n items: [\n ...planTocItems({\n fallbackTitle: meta.title,\n plannedSections,\n sections,\n serialIdAllocator,\n }),\n ],\n } satisfies TocFile;\n const targetStage = options.targetStage ?? \"summarized\";\n const generation =\n targetStage === \"planned\" || targetStage === \"sourced\"\n ? undefined\n : new SerialGeneration({\n document: options.document,\n llm: requireImportLLM(options.llm, targetStage),\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n\n if (\n options.digestProgressTracker !== undefined &&\n targetStage !== \"planned\"\n ) {\n const discoveries = await discoverPlannedSections(plannedSections, {\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n\n await options.digestProgressTracker.discoverSerials(discoveries);\n }\n\n const serials = await generatePlannedSerials(plannedSections, {\n document: options.document,\n extractionPrompt: options.extractionPrompt,\n ...(generation === undefined ? {} : { generation }),\n serialConcurrency: resolveSerialGenerationConcurrency(options.llm),\n targetStage,\n ...(options.digestProgressTracker === undefined\n ? {}\n : { digestProgressTracker: options.digestProgressTracker }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n\n await options.document.openSession(async (document) => {\n await document.writeBookMeta(meta);\n await document.writeToc(toc);\n\n if (cover !== undefined) {\n await document.writeCover(cover);\n }\n });\n\n return {\n cover,\n meta,\n serials,\n toc,\n };\n}\n\nasync function generatePlannedSerials(\n plannedSections: readonly PlannedSection[],\n options: {\n readonly digestProgressTracker?: DigestProgressTracker;\n readonly document: Document;\n readonly extractionPrompt: string;\n readonly generation?: SerialGeneration;\n readonly serialConcurrency: number;\n readonly targetStage: ImportSourceStage;\n readonly userLanguage?: GenerateSerialOptions[\"userLanguage\"];\n },\n): Promise<ImportedSerial[]> {\n if (options.targetStage === \"planned\") {\n await options.document.openSession(async (document) => {\n for (const plannedSection of plannedSections) {\n await document.serials.createWithId(plannedSection.serialId);\n }\n });\n\n return plannedSections.map((plannedSection) => ({\n id: plannedSection.serialId,\n }));\n }\n\n const limiter = new AsyncSemaphore(options.serialConcurrency);\n const serials = await Promise.all(\n plannedSections.map(async (plannedSection) => {\n return await limiter.use(async () => {\n const context = options.document.createContext();\n\n context.ownSerial(plannedSection.serialId);\n\n try {\n const serialProgressTracker =\n options.digestProgressTracker?.createSerialTracker({\n id: plannedSection.serialId,\n });\n const serial = await context.run(async () => {\n if (options.targetStage === \"sourced\") {\n await options.document.serials.createWithId(\n plannedSection.serialId,\n );\n await writeSerialSource(\n options.document,\n plannedSection.serialId,\n await plannedSection.section.open(),\n );\n\n return {\n id: plannedSection.serialId,\n } satisfies ImportedSerial;\n }\n\n if (options.targetStage === \"graphed\") {\n await options.document.serials.createWithId(\n plannedSection.serialId,\n );\n await writeSerialSource(\n options.document,\n plannedSection.serialId,\n await plannedSection.section.open(),\n );\n await requireSerialGeneration(\n options.generation,\n options.targetStage,\n ).buildTopologyInto(\n plannedSection.serialId,\n {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n },\n serialProgressTracker,\n );\n\n return {\n id: plannedSection.serialId,\n } satisfies ImportedSerial;\n }\n\n return await requireSerialGeneration(\n options.generation,\n options.targetStage,\n ).generateInto(\n plannedSection.serialId,\n await plannedSection.section.open(),\n {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n },\n serialProgressTracker,\n );\n });\n\n context.complete();\n return serial;\n } finally {\n await context.dispose();\n }\n });\n }),\n );\n\n return [...serials].sort((left, right) => left.id - right.id);\n}\n\nfunction planTocItems(input: {\n readonly fallbackTitle: string | null | undefined;\n readonly plannedSections: PlannedSection[];\n readonly sections: readonly SourceSection[];\n readonly serialIdAllocator: SerialIdAllocator;\n}): TocItem[] {\n return input.sections.map((section, index) =>\n planTocItem({\n fallbackTitle:\n input.sections.length === 1 ? input.fallbackTitle : undefined,\n indexPath: [index],\n plannedSections: input.plannedSections,\n section,\n serialIdAllocator: input.serialIdAllocator,\n }),\n );\n}\n\nfunction planTocItem(input: {\n readonly fallbackTitle: string | null | undefined;\n readonly indexPath: readonly number[];\n readonly plannedSections: PlannedSection[];\n readonly section: SourceSection;\n readonly serialIdAllocator: SerialIdAllocator;\n}): TocItem {\n const serialId = input.section.hasContent\n ? input.serialIdAllocator.nextSerialId++\n : undefined;\n const children = input.section.children.map((child, index) =>\n planTocItem({\n fallbackTitle: undefined,\n indexPath: [...input.indexPath, index],\n plannedSections: input.plannedSections,\n section: child,\n serialIdAllocator: input.serialIdAllocator,\n }),\n );\n\n if (serialId !== undefined) {\n input.plannedSections.push({\n section: input.section,\n serialId,\n });\n }\n\n const title =\n normalizeTitle(input.section.title) ?? normalizeTitle(input.fallbackTitle);\n\n return {\n ...(title === undefined ? {} : { title }),\n ...(serialId === undefined ? {} : { serialId }),\n children,\n };\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | undefined {\n const normalized = title?.trim();\n\n return normalized === undefined || normalized === \"\" ? undefined : normalized;\n}\n\nasync function assertImportTargetIsEmpty(document: Document): Promise<void> {\n const [bookMeta, cover, toc, serialIds] = await Promise.all([\n document.readBookMeta(),\n document.readCover(),\n document.readToc(),\n document.serials.listIds(),\n ]);\n\n if (bookMeta !== undefined) {\n throw new Error(\"Archive metadata already exists\");\n }\n if (cover !== undefined) {\n throw new Error(\"Document cover already exists\");\n }\n if (toc !== undefined) {\n throw new Error(\"Document TOC already exists\");\n }\n if (serialIds.length > 0) {\n throw new Error(\"Document already contains serials\");\n }\n}\n\nasync function discoverPlannedSections(\n plannedSections: readonly PlannedSection[],\n options: {\n readonly segmenter?: NonNullable<ImportSourceOptions[\"segmenter\"]>;\n },\n): Promise<\n readonly {\n readonly fragments?: number;\n readonly id: number;\n readonly title?: string | undefined;\n readonly words: number;\n }[]\n> {\n if (\n plannedSections.every(\n (plannedSection) => plannedSection.section.wordsCount !== undefined,\n )\n ) {\n return plannedSections.map((plannedSection) => ({\n id: plannedSection.serialId,\n title: plannedSection.section.title?.trim() || undefined,\n words: plannedSection.section.wordsCount ?? 0,\n }));\n }\n\n const discoveries = [];\n\n for (const plannedSection of plannedSections) {\n discoveries.push({\n id: plannedSection.serialId,\n title: plannedSection.section.title?.trim() || undefined,\n ...(await discoverSerial({\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n stream: await plannedSection.section.open(),\n })),\n });\n }\n\n return discoveries;\n}\n\nfunction resolveSerialGenerationConcurrency(\n llm: Pick<ImportSourceOptions, \"llm\">[\"llm\"],\n): number {\n if (\n typeof llm !== \"object\" ||\n llm === null ||\n !(\"config\" in llm) ||\n typeof llm.config !== \"object\" ||\n llm.config === null ||\n !(\"concurrent\" in llm.config) ||\n typeof llm.config.concurrent !== \"number\" ||\n !Number.isInteger(llm.config.concurrent) ||\n llm.config.concurrent < 1\n ) {\n return 1;\n }\n\n return llm.config.concurrent;\n}\n\nfunction requireImportLLM(\n llm: ImportSourceOptions[\"llm\"],\n targetStage: ImportSourceStage,\n): NonNullable<ImportSourceOptions[\"llm\"]> {\n if (llm === undefined) {\n throw new Error(`LLM is required to import source to ${targetStage}.`);\n }\n\n return llm;\n}\n\nfunction requireSerialGeneration(\n generation: SerialGeneration | undefined,\n targetStage: ImportSourceStage,\n): SerialGeneration {\n if (generation === undefined) {\n throw new Error(\n `Internal error: serial generation is required for ${targetStage}.`,\n );\n }\n\n return generation;\n}\n","import { mkdir, writeFile } from \"fs/promises\";\nimport { dirname } from \"path\";\n\nimport type { ReadonlyDocument } from \"../document/index.js\";\nimport type { TocItem } from \"../source/index.js\";\n\nexport interface WritePlainTextOptions {\n readonly document: ReadonlyDocument;\n readonly path: string;\n}\n\nexport async function writePlainText(\n options: WritePlainTextOptions,\n): Promise<void> {\n const text = await options.document.openSession(\n async (document) => await buildPlainText(document),\n );\n\n await mkdir(dirname(options.path), { recursive: true });\n await writeFile(options.path, text, \"utf8\");\n}\n\nasync function buildPlainText(document: ReadonlyDocument): Promise<string> {\n const toc = await document.readToc();\n\n if (toc === undefined) {\n throw new Error(\"Document TOC is missing\");\n }\n\n const blocks = await renderTocItems(document, toc.items);\n\n if (blocks.length === 0) {\n return \"\";\n }\n\n return `${blocks.join(\"\\n\\n\")}\\n`;\n}\n\nasync function renderTocItems(\n document: ReadonlyDocument,\n items: readonly TocItem[],\n): Promise<string[]> {\n const blocks: string[] = [];\n\n for (const item of items) {\n const block = await renderTocItem(document, item);\n\n if (block !== undefined) {\n blocks.push(block);\n }\n }\n\n return blocks;\n}\n\nasync function renderTocItem(\n document: ReadonlyDocument,\n item: TocItem,\n): Promise<string | undefined> {\n const parts = [item.title?.trim()].filter(\n (value): value is string => value !== undefined && value !== \"\",\n );\n\n if (item.serialId !== undefined) {\n const summary = await document.readSummary(item.serialId);\n\n if (summary === undefined) {\n throw new Error(\n `Chapter ${item.serialId} summary is missing. Run \\`wg wikg://local/job add --input <chapter-uri> --task reading-summary --accept-cost\\` before export, or inspect the archive with \\`wg <archive-uri>/chapter/tree get\\`.`,\n );\n }\n if (summary.trim() !== \"\") {\n parts.push(summary.trim());\n }\n }\n\n const childBlocks = await renderTocItems(document, item.children);\n\n if (parts.length === 0 && childBlocks.length === 0) {\n return undefined;\n }\n\n return [...parts, ...childBlocks].join(\"\\n\\n\");\n}\n","export function escapeXml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\nexport function normalizeLanguage(language: string | null): string {\n const normalized = language?.trim();\n\n return normalized === undefined || normalized === \"\" ? \"und\" : normalized;\n}\n","import { resolveDataDirPath } from \"../../common/data-dir.js\";\nimport { createEnv } from \"../../common/template.js\";\n\nlet templateEnvironment: ReturnType<typeof createEnv> | undefined;\n\nfunction getTemplateEnvironment(): ReturnType<typeof createEnv> {\n templateEnvironment ??= createEnv(resolveDataDirPath());\n\n return templateEnvironment;\n}\n\nexport function renderCoverPage(input: {\n readonly coverImageHref: string;\n readonly language: string;\n readonly title: string;\n}): string {\n return getTemplateEnvironment().render(\"output/epub/cover.xhtml\", input);\n}\n\nexport function renderNavDocument(input: {\n readonly itemsMarkup: string;\n readonly language: string;\n readonly title: string;\n}): string {\n return getTemplateEnvironment().render(\"output/epub/nav.xhtml\", input);\n}\n\nexport function renderPackageOpf(input: {\n readonly authors: readonly string[];\n readonly coverImageHref: string | undefined;\n readonly coverMediaType: string | undefined;\n readonly coverPageHref: string | undefined;\n readonly description: string | null;\n readonly identifier: string;\n readonly language: string;\n readonly modifiedAt: string;\n readonly publishedAt: string | null;\n readonly publisher: string | null;\n readonly sections: readonly {\n readonly href: string;\n readonly id: string;\n }[];\n readonly title: string;\n readonly version: string;\n}): string {\n return getTemplateEnvironment().render(\"output/epub/package.opf.xml\", input);\n}\n\nexport function renderSectionDocument(input: {\n readonly language: string;\n readonly paragraphs: readonly string[];\n readonly title: string;\n}): string {\n return getTemplateEnvironment().render(\"output/epub/section.xhtml\", input);\n}\n","import { createWriteStream } from \"fs\";\nimport { dirname, extname } from \"path\";\nimport { finished } from \"stream/promises\";\n\nimport { mkdir } from \"fs/promises\";\nimport { ZipFile } from \"yazl\";\n\nimport type { BookMeta, SourceAsset } from \"../../source/index.js\";\n\nimport type { EpubBook } from \"./model.js\";\nimport { normalizeLanguage } from \"./shared.js\";\nimport { renderCoverPage } from \"./templates.js\";\n\nconst EPUB_CONTAINER_XML = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/package.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>\n`;\n\nexport async function writeEpubArchive(\n path: string,\n book: EpubBook,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n\n const zip = new ZipFile();\n\n zip.addBuffer(Buffer.from(\"application/epub+zip\"), \"mimetype\", {\n compress: false,\n });\n zip.addBuffer(\n Buffer.from(EPUB_CONTAINER_XML, \"utf8\"),\n \"META-INF/container.xml\",\n );\n zip.addBuffer(Buffer.from(book.packageOpf, \"utf8\"), \"OEBPS/package.opf\");\n zip.addBuffer(Buffer.from(book.navXhtml, \"utf8\"), \"OEBPS/nav.xhtml\");\n\n for (const section of book.sections) {\n zip.addBuffer(Buffer.from(section.xhtml, \"utf8\"), `OEBPS/${section.href}`);\n }\n\n if (book.cover !== undefined) {\n const coverImageHref = createCoverImageHref(book.cover);\n const language = normalizeLanguage(book.meta.language);\n\n zip.addBuffer(Buffer.from(book.cover.data), `OEBPS/${coverImageHref}`);\n zip.addBuffer(\n Buffer.from(createCoverPage(book.meta, coverImageHref, language), \"utf8\"),\n \"OEBPS/text/cover.xhtml\",\n );\n }\n\n zip.end();\n\n const output = createWriteStream(path);\n const outputDone = finished(output);\n const zipDone = finished(zip.outputStream);\n\n zip.outputStream.pipe(output);\n await Promise.all([outputDone, zipDone]);\n}\n\nexport function createCoverImageHref(cover: SourceAsset): string {\n return `images/cover${normalizeCoverExtension(cover)}`;\n}\n\nfunction createCoverPage(\n meta: BookMeta,\n coverImageHref: string,\n language: string,\n): string {\n const title = meta.title?.trim() || \"Untitled\";\n\n return renderCoverPage({\n coverImageHref,\n language,\n title,\n });\n}\n\nfunction normalizeCoverExtension(cover: SourceAsset): string {\n const pathExtension = extname(cover.path).toLowerCase();\n\n if (pathExtension !== \"\") {\n return pathExtension;\n }\n\n switch (cover.mediaType) {\n case \"image/gif\":\n return \".gif\";\n case \"image/jpeg\":\n return \".jpg\";\n case \"image/png\":\n return \".png\";\n case \"image/svg+xml\":\n return \".svg\";\n case \"image/webp\":\n return \".webp\";\n default:\n return \".bin\";\n }\n}\n","import type { BookMeta } from \"../../source/index.js\";\n\nimport type { EpubSection } from \"./model.js\";\nimport { renderSectionDocument } from \"./templates.js\";\n\nexport function createFallbackSection(\n meta: BookMeta,\n language: string,\n): EpubSection {\n const title = meta.title?.trim() || \"Untitled\";\n\n return {\n href: \"text/section-1.xhtml\",\n id: \"section-1\",\n title,\n xhtml: renderSectionDocument({\n language,\n paragraphs: [],\n title,\n }),\n };\n}\n\nexport function createSectionDocument(\n serialId: number,\n language: string,\n title: string | null | undefined,\n summary: string,\n): EpubSection {\n const normalizedTitle = title?.trim() || `Section ${serialId}`;\n\n return {\n href: `text/serial-${serialId}.xhtml`,\n id: `serial-${serialId}`,\n title: normalizedTitle,\n xhtml: renderSectionDocument({\n language,\n paragraphs: splitParagraphs(summary.trim()),\n title: normalizedTitle,\n }),\n };\n}\n\nfunction splitParagraphs(summary: string): string[] {\n return summary\n .split(/\\n\\s*\\n/gu)\n .map((paragraph) => paragraph.replace(/\\s*\\n\\s*/gu, \" \").trim())\n .filter((paragraph) => paragraph !== \"\");\n}\n","import type { BookMeta, TocItem } from \"../../source/index.js\";\n\nimport type { EpubNavItem, EpubSection } from \"./model.js\";\nimport { escapeXml } from \"./shared.js\";\nimport { renderNavDocument } from \"./templates.js\";\n\nexport function buildNavItems(\n items: readonly TocItem[],\n sectionMap: ReadonlyMap<number, EpubSection>,\n): EpubNavItem[] {\n return items.map((item) => ({\n children: buildNavItems(item.children, sectionMap),\n href:\n item.serialId === undefined\n ? undefined\n : sectionMap.get(item.serialId)?.href,\n title: item.title?.trim() || `Section ${item.serialId ?? 1}`,\n }));\n}\n\nexport function createNavDocument(\n meta: BookMeta,\n language: string,\n items: readonly EpubNavItem[],\n): string {\n const title = meta.title?.trim() || \"Untitled\";\n\n return renderNavDocument({\n itemsMarkup: renderNavItems(items),\n language,\n title,\n });\n}\n\nfunction renderNavItems(items: readonly EpubNavItem[]): string {\n if (items.length === 0) {\n return \"<ol></ol>\";\n }\n\n return `<ol>\\n${items.map((item) => renderNavItem(item, 1)).join(\"\\n\")}\\n</ol>`;\n}\n\nfunction renderNavItem(item: EpubNavItem, depth: number): string {\n const indent = \" \".repeat(depth);\n const label =\n item.href === undefined\n ? `<span>${escapeXml(item.title)}</span>`\n : `<a href=\"${escapeXml(item.href)}\">${escapeXml(item.title)}</a>`;\n const children =\n item.children.length === 0\n ? \"\"\n : `\\n${indent} <ol>\\n${item.children\n .map((child) => renderNavItem(child, depth + 2))\n .join(\"\\n\")}\\n${indent} </ol>`;\n\n return `${indent}<li>${label}${children}</li>`;\n}\n","import type { BookMeta } from \"../../source/index.js\";\n\nimport type { EpubSection } from \"./model.js\";\nimport { renderPackageOpf } from \"./templates.js\";\n\nexport const EPUB_OUTPUT_VERSION = \"3.0\";\n\nexport function createPackageOpf(input: {\n readonly coverImageHref: string | undefined;\n readonly coverMediaType: string | undefined;\n readonly coverPageHref: string | undefined;\n readonly language: string;\n readonly meta: BookMeta;\n readonly modifiedAt: string;\n readonly sections: readonly EpubSection[];\n}): string {\n const identifier =\n input.meta.identifier?.trim() || `urn:uuid:${crypto.randomUUID()}`;\n const title = input.meta.title?.trim() || \"Untitled\";\n\n return renderPackageOpf({\n authors: input.meta.authors,\n coverImageHref: input.coverImageHref,\n coverMediaType: input.coverMediaType,\n coverPageHref: input.coverPageHref,\n description: input.meta.description,\n identifier,\n language: input.language,\n modifiedAt: input.modifiedAt,\n publishedAt: input.meta.publishedAt,\n publisher: input.meta.publisher,\n sections: input.sections.map((section) => ({\n href: section.href,\n id: section.id,\n })),\n title,\n version: EPUB_OUTPUT_VERSION,\n });\n}\n","import type { ReadonlyDocument } from \"../../document/index.js\";\nimport type { TocItem } from \"../../source/index.js\";\n\nimport { createCoverImageHref } from \"./archive.js\";\nimport { createFallbackSection, createSectionDocument } from \"./content.js\";\nimport type { EpubBook, EpubSection } from \"./model.js\";\nimport { buildNavItems, createNavDocument } from \"./navigation.js\";\nimport { createPackageOpf } from \"./package.js\";\nimport { normalizeLanguage } from \"./shared.js\";\n\nexport async function buildEpubBook(\n document: ReadonlyDocument,\n): Promise<EpubBook> {\n const [meta, toc, cover] = await Promise.all([\n document.readBookMeta(),\n document.readToc(),\n document.readCover(),\n ]);\n\n if (meta === undefined) {\n throw new Error(\"Archive metadata is missing\");\n }\n if (toc === undefined) {\n throw new Error(\"Document TOC is missing\");\n }\n\n const sectionMap = new Map<number, EpubSection>();\n const language = normalizeLanguage(meta.language);\n const sections = await collectSections(\n document,\n language,\n toc.items,\n sectionMap,\n );\n const navItems = buildNavItems(toc.items, sectionMap);\n\n if (sections.length === 0) {\n sections.push(createFallbackSection(meta, language));\n }\n\n const coverImageHref =\n cover === undefined ? undefined : createCoverImageHref(cover);\n const coverPageHref = cover === undefined ? undefined : \"text/cover.xhtml\";\n\n return {\n cover,\n meta,\n navXhtml: createNavDocument(meta, language, navItems),\n packageOpf: createPackageOpf({\n coverImageHref,\n coverMediaType: cover?.mediaType,\n coverPageHref,\n language,\n meta,\n modifiedAt: new Date().toISOString().replace(/\\.\\d{3}Z$/u, \"Z\"),\n sections,\n }),\n sections,\n };\n}\n\nasync function collectSections(\n document: ReadonlyDocument,\n language: string,\n items: readonly TocItem[],\n sectionMap: Map<number, EpubSection>,\n): Promise<EpubSection[]> {\n const sections: EpubSection[] = [];\n\n for (const item of items) {\n if (item.serialId !== undefined) {\n const summary = await document.readSummary(item.serialId);\n\n if (summary === undefined) {\n throw new Error(\n `Chapter ${item.serialId} summary is missing. Run \\`wg wikg://local/job add --input <chapter-uri> --task reading-summary --accept-cost\\` before export, or inspect the archive with \\`wg <archive-uri>/chapter/tree get\\`.`,\n );\n }\n\n const section = createSectionDocument(\n item.serialId,\n language,\n item.title,\n summary,\n );\n\n sectionMap.set(item.serialId, section);\n sections.push(section);\n }\n\n sections.push(\n ...(await collectSections(document, language, item.children, sectionMap)),\n );\n }\n\n return sections;\n}\n","import type { ReadonlyDocument } from \"../../document/index.js\";\n\nimport { writeEpubArchive } from \"./archive.js\";\nimport { buildEpubBook } from \"./book.js\";\nimport { EPUB_OUTPUT_VERSION } from \"./package.js\";\n\nexport interface WriteEpubOptions {\n readonly document: ReadonlyDocument;\n readonly path: string;\n}\n\nexport async function writeEpub(options: WriteEpubOptions): Promise<void> {\n const book = await options.document.openSession(async (document) => {\n return await buildEpubBook(document);\n });\n\n await writeEpubArchive(options.path, book);\n}\n\nexport { EPUB_OUTPUT_VERSION };\n","export { writePlainText, type WritePlainTextOptions } from \"./plain-text.js\";\nexport {\n EPUB_OUTPUT_VERSION,\n writeEpub,\n type WriteEpubOptions,\n} from \"./epub/index.js\";\n","import { randomBytes } from \"crypto\";\nimport { createWriteStream } from \"fs\";\nimport {\n mkdir,\n open as openFile,\n readFile,\n readdir,\n writeFile,\n type FileHandle,\n} from \"fs/promises\";\nimport { dirname, join, posix, relative, resolve, sep } from \"path\";\nimport { finished } from \"stream/promises\";\nimport { inflateRaw } from \"zlib\";\n\nimport { z } from \"zod\";\nimport {\n open as openZip,\n type Entry,\n type ZipFile as YauzlZipFile,\n} from \"yauzl\";\nimport { ZipFile as YazlZipFile } from \"yazl\";\n\nimport { Database } from \"../document/database.js\";\n\nexport const WIKG_FORMAT_VERSION = 1;\nconst WIKG_MUTATION_TOKEN_PATH = \".wikg-mutation-token\";\nconst WIKG_MANIFEST_PATH = \"manifest.json\";\nconst SEARCH_INDEX_DATABASE_PATH = \"fts.db\";\nconst WIKG_MANIFEST_CONTENT = `${JSON.stringify({\n formatVersion: WIKG_FORMAT_VERSION,\n})}\\n`;\nconst WIKG_MUTATION_TOKEN_MAGIC = \"wikg-mutation-token:v1\";\nconst WIKG_MUTATION_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/u;\n\nconst WIKG_ARCHIVE_PATTERNS = [\n /^\\.wikg-mutation-token$/u,\n /^manifest\\.json$/u,\n /^database\\.db$/u,\n /^fts\\.db$/u,\n /^toc\\.json$/u,\n /^cover\\/(?:data\\.bin|info\\.json)$/u,\n /^texts\\/(?:source|summary)\\/\\d+\\.txt$/u,\n] as const;\n\nconst wikgManifestSchema = z.object({\n formatVersion: z.literal(WIKG_FORMAT_VERSION),\n});\n\nexport type WikgArchiveOverlay =\n | {\n readonly entryPath: string;\n readonly kind: \"deleted\";\n }\n | {\n readonly entryPath: string;\n readonly kind: \"file\";\n readonly workspacePath: string;\n };\n\nexport class WikgArchiveReader {\n readonly #entryByPath: Map<string, Entry>;\n readonly #entries: readonly string[];\n #file: Promise<FileHandle> | undefined;\n readonly #path: string;\n readonly #zipFile: YauzlZipFile;\n\n public constructor(\n path: string,\n zipFile: YauzlZipFile,\n entries: readonly Entry[],\n ) {\n this.#path = path;\n this.#zipFile = zipFile;\n this.#entryByPath = new Map(\n entries\n .map((entry) => [normalizeArchivePath(entry.fileName), entry] as const)\n .filter(([entryPath]) => entryPath !== \"\")\n .filter(([entryPath]) => isWikgArchivePath(entryPath)),\n );\n this.#entries = [...this.#entryByPath.keys()].sort((left, right) =>\n left.localeCompare(right),\n );\n }\n\n public static async open(inputPath: string): Promise<WikgArchiveReader> {\n const { entries, zipFile } = await openIndexedArchive(inputPath);\n\n return new WikgArchiveReader(inputPath, zipFile, entries);\n }\n\n public close(): void {\n this.#zipFile.close();\n if (this.#file !== undefined) {\n void this.#file.then(async (file) => {\n await file.close();\n });\n this.#file = undefined;\n }\n }\n\n public listEntries(): readonly string[] {\n return this.#entries;\n }\n\n public async readEntry(entryPath: string): Promise<Buffer | undefined> {\n const entry = this.#entryByPath.get(normalizeArchivePath(entryPath));\n\n if (entry === undefined) {\n return undefined;\n }\n\n return await readArchiveEntryBufferFromFile(await this.#getFile(), entry);\n }\n\n async #getFile(): Promise<FileHandle> {\n this.#file ??= openFile(this.#path, \"r\");\n return await this.#file;\n }\n}\n\nexport async function extractWikgArchive(\n inputPath: string,\n outputDirectoryPath: string,\n): Promise<void> {\n const { entries, zipFile } = await openIndexedArchive(inputPath);\n const file = await openFile(inputPath, \"r\");\n\n try {\n for (const entry of entries) {\n const archivePath = normalizeArchivePath(entry.fileName);\n\n if (archivePath === \"\") {\n throw new Error(`Invalid archive entry path: ${entry.fileName}`);\n }\n if (!isWikgArchivePath(archivePath)) {\n continue;\n }\n\n const targetPath = resolve(outputDirectoryPath, archivePath);\n\n assertWithinDirectory(outputDirectoryPath, targetPath, archivePath);\n await mkdir(dirname(targetPath), { recursive: true });\n await writeFile(\n targetPath,\n await readArchiveEntryBufferFromFile(file, entry),\n );\n }\n } finally {\n await file.close();\n zipFile.close();\n }\n}\n\nexport async function writeWikgArchive(\n documentDirectoryPath: string,\n outputPath: string,\n): Promise<void> {\n await mkdir(dirname(outputPath), { recursive: true });\n\n const zipFile = new YazlZipFile();\n const files = await listDocumentFiles(documentDirectoryPath);\n const includeSearchIndex = await shouldEmbedSearchIndex(\n documentDirectoryPath,\n );\n const entries = sortArchiveEntriesForWrite([\n {\n archivePath: WIKG_MUTATION_TOKEN_PATH,\n content: createWikgMutationTokenContent(),\n },\n {\n archivePath: WIKG_MANIFEST_PATH,\n content: Buffer.from(WIKG_MANIFEST_CONTENT, \"utf8\"),\n },\n ...files.filter((file) => {\n if (file.archivePath === WIKG_MANIFEST_PATH) {\n return false;\n }\n if (file.archivePath === WIKG_MUTATION_TOKEN_PATH) {\n return false;\n }\n\n return (\n file.archivePath !== SEARCH_INDEX_DATABASE_PATH || includeSearchIndex\n );\n }),\n ]);\n\n for (const entry of entries) {\n if (\"content\" in entry) {\n zipFile.addBuffer(entry.content, entry.archivePath, {\n compress: false,\n });\n } else {\n zipFile.addFile(entry.absolutePath, entry.archivePath, {\n compress: false,\n });\n }\n }\n\n zipFile.end();\n\n const output = createWriteStream(outputPath);\n const outputDone = finished(output);\n const zipDone = finished(zipFile.outputStream);\n\n zipFile.outputStream.pipe(output);\n await Promise.all([outputDone, zipDone]);\n}\n\nexport async function listWikgArchiveEntries(\n inputPath: string,\n): Promise<readonly string[]> {\n const reader = await WikgArchiveReader.open(inputPath);\n\n try {\n return reader.listEntries();\n } finally {\n reader.close();\n }\n}\n\nexport async function readWikgArchiveEntry(\n inputPath: string,\n entryPath: string,\n): Promise<Buffer | undefined> {\n const reader = await WikgArchiveReader.open(inputPath);\n\n try {\n return await reader.readEntry(entryPath);\n } finally {\n reader.close();\n }\n}\n\nexport async function readWikgArchiveMutationToken(\n inputPath: string,\n): Promise<string> {\n const reader = await WikgArchiveReader.open(inputPath);\n\n try {\n const content = await reader.readEntry(WIKG_MUTATION_TOKEN_PATH);\n\n if (content === undefined) {\n throw new Error(\n `Missing WIKG mutation token: ${WIKG_MUTATION_TOKEN_PATH}.`,\n );\n }\n\n return parseWikgMutationToken(content.toString(\"utf8\"));\n } finally {\n reader.close();\n }\n}\n\nexport async function writeWikgArchiveWithOverlays(\n inputPath: string,\n outputPath: string,\n overlays: readonly WikgArchiveOverlay[],\n): Promise<void> {\n await mkdir(dirname(outputPath), { recursive: true });\n\n const { entries: sourceEntries, zipFile } =\n await openIndexedArchive(inputPath);\n const overlayByPath = new Map(\n overlays.map((overlay) => [\n normalizeArchivePath(overlay.entryPath),\n overlay,\n ]),\n );\n const entryPaths = new Set<string>();\n\n for (const entry of sourceEntries) {\n const archivePath = normalizeArchivePath(entry.fileName);\n\n if (archivePath !== \"\" && isWikgArchivePath(archivePath)) {\n entryPaths.add(archivePath);\n }\n }\n for (const overlay of overlayByPath.values()) {\n const archivePath = normalizeArchivePath(overlay.entryPath);\n\n if (archivePath !== \"\" && isWikgArchivePath(archivePath)) {\n entryPaths.add(archivePath);\n }\n }\n entryPaths.add(WIKG_MUTATION_TOKEN_PATH);\n entryPaths.add(WIKG_MANIFEST_PATH);\n\n const outputZipFile = new YazlZipFile();\n const sourceFile = await openFile(inputPath, \"r\");\n\n try {\n for (const entryPath of sortArchiveEntryPathsForWrite(entryPaths)) {\n const overlay = overlayByPath.get(entryPath);\n\n if (entryPath === WIKG_MUTATION_TOKEN_PATH) {\n outputZipFile.addBuffer(createWikgMutationTokenContent(), entryPath, {\n compress: false,\n });\n continue;\n }\n if (entryPath === WIKG_MANIFEST_PATH) {\n outputZipFile.addBuffer(\n Buffer.from(WIKG_MANIFEST_CONTENT, \"utf8\"),\n entryPath,\n { compress: false },\n );\n continue;\n }\n if (overlay?.kind === \"deleted\") {\n continue;\n }\n if (overlay?.kind === \"file\") {\n outputZipFile.addFile(overlay.workspacePath, entryPath, {\n compress: false,\n });\n continue;\n }\n\n const sourceEntry = sourceEntries.find(\n (candidate) => normalizeArchivePath(candidate.fileName) === entryPath,\n );\n\n if (sourceEntry === undefined) {\n continue;\n }\n\n outputZipFile.addBuffer(\n await readArchiveEntryBufferFromFile(sourceFile, sourceEntry),\n entryPath,\n { compress: false },\n );\n }\n } finally {\n await sourceFile.close();\n zipFile.close();\n }\n\n outputZipFile.end();\n\n const output = createWriteStream(outputPath);\n const outputDone = finished(output);\n const zipDone = finished(outputZipFile.outputStream);\n\n outputZipFile.outputStream.pipe(output);\n await Promise.all([outputDone, zipDone]);\n}\n\nexport async function readWikgArchiveFormatVersion(\n documentDirectoryPath: string,\n): Promise<number> {\n return parseWikgManifest(\n await readFile(join(documentDirectoryPath, WIKG_MANIFEST_PATH), \"utf8\"),\n ).formatVersion;\n}\n\nasync function listDocumentFiles(\n rootDirectoryPath: string,\n currentDirectoryPath = rootDirectoryPath,\n): Promise<Array<{ absolutePath: string; archivePath: string }>> {\n const entries = await readdir(currentDirectoryPath, { withFileTypes: true });\n const files: Array<{ absolutePath: string; archivePath: string }> = [];\n\n for (const entry of [...entries].sort(compareDirEntryName)) {\n const absolutePath = join(currentDirectoryPath, entry.name);\n\n if (entry.isDirectory()) {\n files.push(...(await listDocumentFiles(rootDirectoryPath, absolutePath)));\n continue;\n }\n if (!entry.isFile()) {\n continue;\n }\n\n files.push({\n absolutePath,\n archivePath: relative(rootDirectoryPath, absolutePath)\n .split(sep)\n .join(posix.sep),\n });\n }\n\n return files.filter((file) => isWikgArchivePath(file.archivePath));\n}\n\nasync function shouldEmbedSearchIndex(\n documentDirectoryPath: string,\n): Promise<boolean> {\n const database = await Database.open(\n join(documentDirectoryPath, \"database.db\"),\n \"\",\n {\n readonly: true,\n },\n ).catch(() => undefined);\n\n if (database === undefined) {\n return false;\n }\n\n try {\n const row = await database.queryOne(\n `\n SELECT fts_embedded\n FROM archive_index_settings\n WHERE id = 1\n `,\n undefined,\n (value) => Number(value.fts_embedded) !== 0,\n );\n\n return row ?? false;\n } catch {\n return false;\n } finally {\n await database.close();\n }\n}\n\nasync function openIndexedArchive(inputPath: string): Promise<{\n readonly entries: readonly Entry[];\n readonly zipFile: YauzlZipFile;\n}> {\n const zipFile = await openArchive(inputPath);\n\n try {\n const entries = await indexArchiveEntries(zipFile);\n\n await validateArchiveManifest(inputPath, entries);\n return { entries, zipFile };\n } catch (error) {\n zipFile.close();\n throw error;\n }\n}\n\nasync function indexArchiveEntries(\n zipFile: YauzlZipFile,\n): Promise<readonly Entry[]> {\n return await new Promise((resolve, reject) => {\n const entries: Entry[] = [];\n\n zipFile.on(\"entry\", (entry: Entry) => {\n if (entry.fileName.endsWith(\"/\")) {\n zipFile.readEntry();\n return;\n }\n\n entries.push(entry);\n zipFile.readEntry();\n });\n zipFile.once(\"end\", () => {\n resolve(entries);\n });\n zipFile.once(\"error\", (error: Error) => {\n reject(error);\n });\n\n zipFile.readEntry();\n });\n}\n\nfunction compareDirEntryName(\n left: { readonly name: string },\n right: { readonly name: string },\n): number {\n return left.name.localeCompare(right.name);\n}\n\nfunction sortArchiveEntriesForWrite<T extends { readonly archivePath: string }>(\n entries: readonly T[],\n): T[] {\n return [...entries].sort((left, right) =>\n compareArchiveEntryPathsForWrite(left.archivePath, right.archivePath),\n );\n}\n\nfunction sortArchiveEntryPathsForWrite(paths: Iterable<string>): string[] {\n return [...paths].sort(compareArchiveEntryPathsForWrite);\n}\n\nfunction compareArchiveEntryPathsForWrite(left: string, right: string): number {\n if (left === WIKG_MUTATION_TOKEN_PATH) {\n return right === WIKG_MUTATION_TOKEN_PATH ? 0 : -1;\n }\n if (right === WIKG_MUTATION_TOKEN_PATH) {\n return 1;\n }\n\n return left.localeCompare(right);\n}\n\nfunction isWikgArchivePath(archivePath: string): boolean {\n return WIKG_ARCHIVE_PATTERNS.some((pattern) => pattern.test(archivePath));\n}\n\nasync function validateArchiveManifest(\n inputPath: string,\n entries: readonly Entry[],\n): Promise<void> {\n await validateArchiveMutationToken(inputPath, entries);\n\n const entry = entries.find(\n (candidate) =>\n normalizeArchivePath(candidate.fileName) === WIKG_MANIFEST_PATH,\n );\n\n if (entry === undefined) {\n throw new Error(`Missing WIKG manifest: ${WIKG_MANIFEST_PATH}.`);\n }\n\n parseWikgManifest(await readArchiveEntryText(inputPath, entry));\n}\n\nasync function validateArchiveMutationToken(\n inputPath: string,\n entries: readonly Entry[],\n): Promise<void> {\n const firstEntryPath = normalizeArchivePath(entries[0]?.fileName ?? \"\");\n\n if (firstEntryPath !== WIKG_MUTATION_TOKEN_PATH) {\n throw new Error(\n `Missing WIKG mutation token: ${WIKG_MUTATION_TOKEN_PATH}.`,\n );\n }\n\n parseWikgMutationToken(await readArchiveEntryText(inputPath, entries[0]!));\n}\n\nfunction parseWikgManifest(\n content: string,\n): z.infer<typeof wikgManifestSchema> {\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`Invalid WIKG manifest: ${WIKG_MANIFEST_PATH}.`);\n }\n\n const result = wikgManifestSchema.safeParse(parsed);\n\n if (!result.success) {\n throw new Error(\n `Unsupported WIKG format version in ${WIKG_MANIFEST_PATH}.`,\n );\n }\n\n return result.data;\n}\n\nfunction createWikgMutationTokenContent(): Buffer {\n const token = randomBytes(32).toString(\"base64url\");\n\n return Buffer.from(`${WIKG_MUTATION_TOKEN_MAGIC}\\n${token}\\n`, \"utf8\");\n}\n\nfunction parseWikgMutationToken(content: string): string {\n const lines = content.split(/\\r?\\n/u);\n const magic = lines[0];\n const token = lines[1];\n\n if (\n magic !== WIKG_MUTATION_TOKEN_MAGIC ||\n token === undefined ||\n !WIKG_MUTATION_TOKEN_PATTERN.test(token)\n ) {\n throw new Error(\n `Invalid WIKG mutation token: ${WIKG_MUTATION_TOKEN_PATH}.`,\n );\n }\n\n return token;\n}\n\nfunction assertWithinDirectory(\n rootDirectoryPath: string,\n targetPath: string,\n archivePath: string,\n): void {\n const resolvedRootDirectoryPath = resolve(rootDirectoryPath);\n const rootPrefix = resolvedRootDirectoryPath.endsWith(sep)\n ? resolvedRootDirectoryPath\n : `${resolvedRootDirectoryPath}${sep}`;\n\n if (\n targetPath === resolvedRootDirectoryPath ||\n targetPath.startsWith(rootPrefix)\n ) {\n return;\n }\n\n throw new Error(`Invalid archive entry path: ${archivePath}`);\n}\n\nfunction normalizeArchivePath(path: string): string {\n const normalized = path.replaceAll(\"\\\\\", \"/\").trim();\n const withoutLeadingSlash = normalized.startsWith(\"/\")\n ? normalized.slice(1)\n : normalized;\n\n return posix\n .normalize(withoutLeadingSlash)\n .replace(/^(\\.\\/)+/u, \"\")\n .replace(/^\\/+/u, \"\");\n}\n\nasync function openArchive(path: string): Promise<YauzlZipFile> {\n return await new Promise((resolve, reject) => {\n openZip(path, { autoClose: false, lazyEntries: true }, (error, zipFile) => {\n if (error !== null || zipFile === undefined) {\n reject(error ?? new Error(`Cannot open archive: ${path}`));\n return;\n }\n\n resolve(zipFile);\n });\n });\n}\n\nasync function readArchiveEntryText(\n inputPath: string,\n entry: Entry,\n): Promise<string> {\n return (await readArchiveEntryBuffer(inputPath, entry)).toString(\"utf8\");\n}\n\nasync function readArchiveEntryBuffer(\n inputPath: string,\n entry: Entry,\n): Promise<Buffer> {\n const file = await openFile(inputPath, \"r\");\n\n try {\n return await readArchiveEntryBufferFromFile(file, entry);\n } finally {\n await file.close();\n }\n}\n\nasync function readArchiveEntryBufferFromFile(\n file: FileHandle,\n entry: Entry,\n): Promise<Buffer> {\n const compressed = await readCompressedArchiveEntryBuffer(file, entry);\n\n if (entry.compressionMethod === 0) {\n return compressed;\n }\n if (entry.compressionMethod === 8) {\n return await inflateRawBuffer(compressed);\n }\n\n throw new Error(`Unsupported ZIP compression method: ${entry.fileName}`);\n}\n\nasync function readCompressedArchiveEntryBuffer(\n file: FileHandle,\n entry: Entry,\n): Promise<Buffer> {\n const header = Buffer.alloc(30);\n\n await file.read(header, 0, header.length, entry.relativeOffsetOfLocalHeader);\n if (header.readUInt32LE(0) !== 0x04034b50) {\n throw new Error(`Invalid ZIP local file header: ${entry.fileName}`);\n }\n\n const fileNameLength = header.readUInt16LE(26);\n const extraFieldLength = header.readUInt16LE(28);\n const dataOffset =\n entry.relativeOffsetOfLocalHeader + 30 + fileNameLength + extraFieldLength;\n const compressed = Buffer.alloc(entry.compressedSize);\n\n await file.read(compressed, 0, compressed.length, dataOffset);\n return compressed;\n}\n\nasync function inflateRawBuffer(input: Buffer): Promise<Buffer> {\n return await new Promise((resolveInflate, rejectInflate) => {\n inflateRaw(input, (error, output) => {\n if (error !== null) {\n rejectInflate(error);\n return;\n }\n\n resolveInflate(output);\n });\n });\n}\n","export {\n formatLocatedChapterResourceUri,\n formatLocatedChapterSourceCollectionUri,\n formatLocatedChapterUri,\n formatLocatedWikiGraphUri,\n formatWikiGraphCommandUri,\n formatWikiGraphObjectUri,\n formatWikiGraphUriExpectedError,\n isWikiGraphJobUri,\n isWikiGraphUri,\n parseLocatedWikiGraphUri,\n requireArchiveUri,\n requireLocatedObjectOrArchiveUri,\n requireLocatedObjectUri,\n WIKI_GRAPH_ARCHIVE_EXTENSION,\n WIKI_GRAPH_JOB_URI_PREFIX,\n WIKI_GRAPH_URI_PREFIX,\n type LocatedWikiGraphUri,\n} from \"../common/wiki-graph-uri.js\";\n","export {\n createSearchIndexFingerprint,\n ensureSearchIndex,\n isSearchIndexCurrent,\n querySearchIndex,\n readArchiveIndexSettings,\n readSearchIndexFingerprintFromDatabase,\n readSearchIndexStatus,\n SEARCH_INDEX_FTS_HIT_LIMIT,\n SEARCH_OBJECT_PROPERTY_KIND,\n SEARCH_OBJECT_PROPERTY_OWNER_KIND,\n setFtsIndexEmbedded,\n TEXT_SENTENCE_KIND,\n} from \"./search-index.js\";\nexport type {\n ArchiveIndexSettings,\n SearchIndexInput,\n SearchIndexObjectHit,\n SearchIndexProgressEvent,\n SearchIndexProgressPhase,\n SearchIndexProgressReporter,\n SearchIndexQueryResult,\n SearchIndexStatus,\n SearchIndexTextHit,\n SearchObjectPropertyKind,\n SearchObjectPropertyOwnerKind,\n TextSentenceKind,\n} from \"./search-index.js\";\n","import { readdir, rm, rmdir, stat } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { isNodeError } from \"../utils/node-error.js\";\n\nconst DISPOSABLE_DIRECTORY_ENTRIES = new Set([\".DS_Store\"]);\nconst DISPOSABLE_DIRECTORY_TTL_MS = 60_000;\n\nexport function isDisposableDirectoryEntry(name: string): boolean {\n return DISPOSABLE_DIRECTORY_ENTRIES.has(name);\n}\n\nexport async function removeDisposableDirectory(\n directoryPath: string,\n): Promise<number> {\n const entries = await readdir(directoryPath).catch((error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n });\n\n if (entries === undefined) {\n return 0;\n }\n if (entries.some((entry) => !isDisposableDirectoryEntry(entry))) {\n return 0;\n }\n\n let freedBytes = 0;\n\n for (const entry of entries) {\n const path = join(directoryPath, entry);\n\n freedBytes += await readPathSize(path);\n await rm(path, { force: true, recursive: true });\n }\n\n await rmdir(directoryPath).catch((error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n });\n return freedBytes;\n}\n\nexport async function removeDisposableChildDirectories(\n rootPath: string,\n): Promise<{\n readonly freedBytes: number;\n readonly removed: number;\n readonly scanned: number;\n}> {\n const entries = await readdir(rootPath, { withFileTypes: true }).catch(\n (error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return [];\n }\n\n throw error;\n },\n );\n let freedBytes = 0;\n let removed = 0;\n let scanned = 0;\n\n for (const entry of entries) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n scanned += 1;\n const directoryPath = join(rootPath, entry.name);\n const stats = await stat(directoryPath).catch((error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n });\n\n if (\n stats === undefined ||\n Date.now() - stats.mtimeMs < DISPOSABLE_DIRECTORY_TTL_MS\n ) {\n continue;\n }\n\n const removedBytes = await removeDisposableDirectory(directoryPath);\n\n if (await pathExists(directoryPath)) {\n freedBytes += removedBytes;\n continue;\n }\n\n freedBytes += removedBytes;\n removed += 1;\n }\n\n return { freedBytes, removed, scanned };\n}\n\nexport async function removeDisposableDescendantDirectories(\n rootPath: string,\n): Promise<{\n readonly freedBytes: number;\n readonly removed: number;\n readonly scanned: number;\n}> {\n const entries = await readdir(rootPath, { withFileTypes: true }).catch(\n (error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return [];\n }\n\n throw error;\n },\n );\n let freedBytes = 0;\n let removed = 0;\n let scanned = 0;\n\n for (const entry of entries) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const directoryPath = join(rootPath, entry.name);\n const childResult =\n await removeDisposableDescendantDirectories(directoryPath);\n\n freedBytes += childResult.freedBytes;\n removed += childResult.removed;\n scanned += childResult.scanned + 1;\n\n const removedBytes = await removeDisposableDirectory(directoryPath);\n\n if (await pathExists(directoryPath)) {\n freedBytes += removedBytes;\n continue;\n }\n\n freedBytes += removedBytes;\n removed += 1;\n }\n\n return { freedBytes, removed, scanned };\n}\n\nexport async function readPathSize(path: string): Promise<number> {\n try {\n const stats = await stat(path);\n\n if (stats.isDirectory()) {\n const entries = await readdir(path);\n let size = 0;\n\n for (const entry of entries) {\n size += await readPathSize(join(path, entry));\n }\n\n return size;\n }\n\n return stats.size;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return 0;\n }\n\n throw error;\n }\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return false;\n }\n\n throw error;\n }\n}\n","import { createHash, randomUUID } from \"crypto\";\nimport {\n mkdir,\n mkdtemp,\n readFile,\n readdir,\n rename,\n rm,\n stat,\n writeFile,\n} from \"fs/promises\";\nimport { tmpdir } from \"os\";\nimport {\n basename,\n dirname,\n isAbsolute,\n join,\n posix,\n relative,\n resolve,\n} from \"path\";\n\nimport { resolveWikiGraphStagingDirectoryPath } from \"../common/wiki-graph-dir.js\";\nimport { createWikiGraphTempDirectory } from \"../common/wiki-graph-temp.js\";\nimport {\n Database as DocumentDatabase,\n DirectoryDocument,\n openSharedStateDatabase,\n} from \"../document/index.js\";\nimport type { Database } from \"../document/index.js\";\nimport type { DocumentFileStore } from \"../document/document.js\";\nimport { createArchiveSearchIndexFingerprint } from \"../archive/query/index.js\";\nimport { readSearchIndexFingerprintFromDatabase } from \"../archive/search-index/index.js\";\nimport {\n isDisposableDirectoryEntry,\n readPathSize,\n removeDisposableChildDirectories,\n removeDisposableDescendantDirectories,\n removeDisposableDirectory,\n} from \"../gc/files.js\";\nimport type { GcContext, GcJobResult } from \"../gc/index.js\";\nimport { AsyncSemaphore } from \"../utils/async-semaphore.js\";\nimport { isNodeError } from \"../utils/node-error.js\";\n\nimport {\n extractWikgArchive,\n listWikgArchiveEntries,\n readWikgArchiveMutationToken,\n readWikgArchiveEntry,\n WikgArchiveReader,\n writeWikgArchiveWithOverlays,\n type WikgArchiveOverlay,\n} from \"./archive.js\";\n\nconst STATE_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS entry_overlays (\n archive_key TEXT NOT NULL,\n archive_path TEXT NOT NULL,\n entry_path TEXT NOT NULL,\n kind TEXT NOT NULL,\n workspace_path TEXT,\n archive_signature TEXT,\n mutation_token TEXT,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (archive_key, entry_path)\n);\n\nCREATE TABLE IF NOT EXISTS entry_locks (\n archive_key TEXT NOT NULL,\n entry_path TEXT NOT NULL,\n mode TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n owner_pid INTEGER NOT NULL,\n heartbeat_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (archive_key, entry_path, owner_id)\n);\n\nCREATE TABLE IF NOT EXISTS archive_owners (\n archive_key TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n owner_pid INTEGER NOT NULL,\n heartbeat_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (archive_key, owner_id)\n);\n\nCREATE TABLE IF NOT EXISTS entry_sqlite_leases (\n archive_key TEXT NOT NULL,\n entry_path TEXT NOT NULL,\n mode TEXT NOT NULL DEFAULT 'read',\n owner_id TEXT NOT NULL,\n owner_pid INTEGER NOT NULL,\n heartbeat_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (archive_key, entry_path, owner_id)\n);\n\nCREATE TABLE IF NOT EXISTS archive_commit_locks (\n archive_key TEXT PRIMARY KEY,\n owner_id TEXT NOT NULL,\n owner_pid INTEGER NOT NULL,\n heartbeat_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL\n);\n`;\n\nconst DATABASE_ENTRY_PATH = \"database.db\";\nconst SEARCH_INDEX_DATABASE_ENTRY_PATH = \"fts.db\";\nconst LOCK_POLL_INTERVAL_MS = 100;\nconst LOCK_STALE_TIMEOUT_MS = 60_000;\nconst OWNER_HEARTBEAT_INTERVAL_MS = 20_000;\nconst SQLITE_CACHE_TTL_MS = 60 * 60 * 1000;\nconst STATE_DATABASE_SEMAPHORE = new AsyncSemaphore(1);\nconst ARCHIVE_SESSION_CONSTRUCTOR_TOKEN = Symbol(\n \"WikgArchiveSession constructor token\",\n);\n\ntype EntryLockMode = \"read\" | \"state\" | \"write\";\ntype SqliteLeaseMode = \"read\" | \"write\";\ntype WorkspaceWritebackPolicy = \"archive\" | \"cache\";\n\nexport class WikgCoordinator {\n public createFileStore(\n archivePath: string,\n options: {\n readonly readonlyDatabase?: boolean;\n readonly searchIndexWritebackPolicy?: WorkspaceWritebackPolicy;\n readonly session?: WikgArchiveSession;\n } = {},\n ): DocumentFileStore {\n return new WikgDocumentFileStore(resolve(archivePath), options);\n }\n\n public async withArchiveSession<T>(\n archivePath: string,\n operation: (session: WikgArchiveSession) => Promise<T> | T,\n ): Promise<T> {\n const session = await WikgArchiveSession.open(resolve(archivePath));\n\n try {\n return await operation(session);\n } finally {\n await session.close();\n }\n }\n\n public async withReadWorkspace<T>(\n archivePath: string,\n operation: (documentDirectoryPath: string) => Promise<T> | T,\n options: {\n readonly documentDirPath?: string;\n } = {},\n ): Promise<T> {\n const directoryPath =\n options.documentDirPath === undefined\n ? await createWikiGraphTempDirectory(\"archive-open\")\n : resolve(options.documentDirPath);\n\n try {\n await extractWikgArchive(resolve(archivePath), directoryPath);\n return await operation(directoryPath);\n } finally {\n if (options.documentDirPath === undefined) {\n await rm(directoryPath, { force: true, recursive: true });\n }\n }\n }\n\n public async withWriteWorkspace<T>(\n archivePath: string,\n operation: (documentDirectoryPath: string) => Promise<T> | T,\n ): Promise<T> {\n const directoryPath = await createWikiGraphTempDirectory(\"archive-write\");\n\n try {\n await extractWikgArchive(resolve(archivePath), directoryPath);\n return await operation(directoryPath);\n } finally {\n await rm(directoryPath, { force: true, recursive: true });\n }\n }\n}\n\nexport class WikgArchiveSession {\n readonly #archiveKey: string;\n readonly #archivePath: string;\n readonly #ownerId = createOwnerId();\n readonly #heartbeat: NodeJS.Timeout;\n readonly #observedDirtyEntryPaths = new Set<string>();\n readonly #modifiedEntryPaths = new Set<string>();\n #closed = false;\n\n public constructor(archivePath: string, token?: symbol) {\n if (token !== ARCHIVE_SESSION_CONSTRUCTOR_TOKEN) {\n throw new Error(\"Use WikgCoordinator.withArchiveSession().\");\n }\n\n this.#archivePath = resolve(archivePath);\n this.#archiveKey = createArchiveKey(this.#archivePath);\n this.#heartbeat = setInterval(() => {\n void heartbeatArchiveOwner({\n archiveKey: this.#archiveKey,\n ownerId: this.#ownerId,\n }).catch(() => undefined);\n }, OWNER_HEARTBEAT_INTERVAL_MS);\n }\n\n public static async open(archivePath: string): Promise<WikgArchiveSession> {\n const session = new WikgArchiveSession(\n archivePath,\n ARCHIVE_SESSION_CONSTRUCTOR_TOKEN,\n );\n\n try {\n await registerArchiveOwner({\n archiveKey: session.#archiveKey,\n ownerId: session.#ownerId,\n });\n return session;\n } catch (error) {\n clearInterval(session.#heartbeat);\n throw error;\n }\n }\n\n public get archiveKey(): string {\n return this.#archiveKey;\n }\n\n public get archivePath(): string {\n return this.#archivePath;\n }\n\n public get ownerId(): string {\n return this.#ownerId;\n }\n\n public observeDirtyEntry(entryPath: string): void {\n this.#observedDirtyEntryPaths.add(entryPath);\n }\n\n public modifyEntry(entryPath: string): void {\n this.#modifiedEntryPaths.add(entryPath);\n }\n\n public createFileStore(\n options: {\n readonly readonlyDatabase?: boolean;\n readonly searchIndexWritebackPolicy?: WorkspaceWritebackPolicy;\n } = {},\n ): DocumentFileStore {\n return new WikgDocumentFileStore(this.#archivePath, {\n ...options,\n session: this,\n });\n }\n\n public async materializeReadWorkspace<T>(\n directoryPath: string | undefined,\n operation: (documentDirectoryPath: string) => Promise<T> | T,\n ): Promise<T> {\n const ownsDirectoryPath = directoryPath === undefined;\n const resolvedDirectoryPath = ownsDirectoryPath\n ? await createWikiGraphTempDirectory(\"archive-open\")\n : resolve(directoryPath);\n\n if (!ownsDirectoryPath) {\n await ensureEmptyDirectory(resolvedDirectoryPath);\n }\n\n const fileStore = this.createFileStore({ readonlyDatabase: true });\n const entries = await listVisibleEntryPaths(\n await listWikgArchiveEntries(this.#archivePath),\n {\n archiveKey: this.#archiveKey,\n prefix: \"\",\n },\n );\n\n try {\n for (const entryPath of entries) {\n const content = await fileStore.readFile(entryPath);\n\n if (content === undefined) {\n continue;\n }\n\n const targetPath = resolve(resolvedDirectoryPath, entryPath);\n const relativeTargetPath = relative(resolvedDirectoryPath, targetPath);\n\n if (\n relativeTargetPath.startsWith(\"..\") ||\n isAbsolute(relativeTargetPath)\n ) {\n throw new Error(`Archive entry escapes read workspace: ${entryPath}`);\n }\n\n await mkdir(dirname(targetPath), { recursive: true });\n await writeFile(targetPath, content);\n }\n\n return await operation(resolvedDirectoryPath);\n } finally {\n await fileStore.close();\n if (ownsDirectoryPath) {\n await rm(resolvedDirectoryPath, { force: true, recursive: true });\n }\n }\n }\n\n public async close(): Promise<void> {\n if (this.#closed) {\n return;\n }\n\n this.#closed = true;\n clearInterval(this.#heartbeat);\n\n try {\n await this.#settle();\n await reapArchive(this.#archiveKey);\n } finally {\n await unregisterArchiveOwner({\n archiveKey: this.#archiveKey,\n ownerId: this.#ownerId,\n });\n }\n\n await import(\"../gc/index.js\")\n .then(async ({ tryRunWikiGraphGc }) => {\n await tryRunWikiGraphGc({ opportunistic: true });\n })\n .catch(() => undefined);\n }\n\n async #settle(): Promise<void> {\n const entryPaths = new Set([\n ...this.#observedDirtyEntryPaths,\n ...this.#modifiedEntryPaths,\n ]);\n\n if (entryPaths.size === 0) {\n return;\n }\n\n await flushArchiveOverlays(this.#archiveKey, entryPaths);\n }\n}\n\nexport async function tryStartWikgFlusher(archivePath?: string): Promise<void> {\n if (archivePath !== undefined) {\n await flushArchiveOverlays(createArchiveKey(resolve(archivePath)));\n return;\n }\n\n const archiveKeys = await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.queryAll(\n `\nSELECT DISTINCT archive_key\nFROM entry_overlays\nORDER BY archive_key\n`,\n undefined,\n (row) => getString(row, \"archive_key\"),\n );\n });\n\n for (const archiveKey of archiveKeys) {\n await flushArchiveOverlays(archiveKey);\n }\n}\n\nexport async function runWikgCoordinatorGc(\n context: GcContext,\n): Promise<GcJobResult> {\n const candidates = await listSqliteCacheGcCandidates();\n let removed = 0;\n let freedBytes = 0;\n\n for (const candidate of candidates) {\n const result = await tryRemoveSqliteCacheCandidate(candidate, context);\n\n if (result.removed) {\n removed += 1;\n freedBytes += result.freedBytes;\n }\n }\n\n const orphanedFiles = await removeOrphanedWorkspaceFiles(context);\n const childDirectories = context.dryRun\n ? { freedBytes: 0, removed: 0, scanned: 0 }\n : await removeDisposableChildDirectories(getCoordinatorWorkspaceRootPath());\n\n return {\n freedBytes:\n freedBytes + orphanedFiles.freedBytes + childDirectories.freedBytes,\n removed: removed + orphanedFiles.removed + childDirectories.removed,\n scanned:\n candidates.length + orphanedFiles.scanned + childDirectories.scanned,\n };\n}\n\nasync function listSqliteCacheGcCandidates(): Promise<readonly EntryOverlay[]> {\n return await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.queryAll(\n `\nSELECT overlay.*\nFROM entry_overlays AS overlay\nWHERE overlay.entry_path IN (?, ?)\n AND overlay.kind = 'file'\n AND overlay.workspace_path IS NOT NULL\nORDER BY overlay.updated_at ASC\n`,\n [DATABASE_ENTRY_PATH, SEARCH_INDEX_DATABASE_ENTRY_PATH],\n mapEntryOverlay,\n );\n });\n}\n\nasync function tryRemoveSqliteCacheCandidate(\n candidate: EntryOverlay,\n context: GcContext,\n): Promise<{ readonly freedBytes: number; readonly removed: boolean }> {\n const releaseWriteLock = await acquireEntryLock(\n candidate.archiveKey,\n candidate.entryPath,\n \"write\",\n );\n\n try {\n const releaseStateLock = await acquireEntryLock(\n candidate.archiveKey,\n candidate.entryPath,\n \"state\",\n );\n\n try {\n const overlay = await readOverlay(\n candidate.archiveKey,\n candidate.entryPath,\n );\n\n if (\n overlay?.workspacePath === undefined ||\n overlay.workspacePath !== candidate.workspacePath ||\n !(await canRemoveSqliteCacheOverlay(overlay, context))\n ) {\n return { freedBytes: 0, removed: false };\n }\n\n let freedBytes = await readPathSize(overlay.workspacePath);\n\n if (!context.dryRun) {\n await deleteOverlay(overlay.archiveKey, overlay.entryPath);\n await rm(overlay.workspacePath, { force: true });\n freedBytes += await removeDisposableDirectory(\n dirname(overlay.workspacePath),\n );\n }\n\n return { freedBytes, removed: true };\n } finally {\n await releaseStateLock();\n }\n } finally {\n await releaseWriteLock();\n }\n}\n\nasync function canRemoveSqliteCacheOverlay(\n overlay: EntryOverlay,\n context: GcContext,\n): Promise<boolean> {\n if (\n await hasActiveArchiveOwnerOrSqliteLease(\n overlay.archiveKey,\n overlay.entryPath,\n )\n ) {\n return false;\n }\n if (overlay.workspacePath === undefined) {\n return false;\n }\n\n if (await isSqliteCacheOverlayDirty(overlay)) {\n return true;\n }\n if (!context.force && context.now - overlay.updatedAt < SQLITE_CACHE_TTL_MS) {\n return false;\n }\n if (\n overlay.entryPath === SEARCH_INDEX_DATABASE_ENTRY_PATH &&\n !context.force\n ) {\n return false;\n }\n\n return true;\n}\n\nasync function isSqliteCacheOverlayDirty(\n overlay: EntryOverlay,\n): Promise<boolean> {\n if (overlay.workspacePath === undefined) {\n return true;\n }\n if (!(await pathExists(overlay.workspacePath))) {\n return true;\n }\n if (!(await pathExists(overlay.archivePath))) {\n return true;\n }\n if (overlay.entryPath !== SEARCH_INDEX_DATABASE_ENTRY_PATH) {\n return false;\n }\n\n return (await readSearchIndexCacheStatus(overlay)) !== \"current\";\n}\n\nasync function readSearchIndexCacheStatus(\n overlay: EntryOverlay,\n): Promise<\"current\" | \"dirty\" | \"missing\"> {\n if (overlay.workspacePath === undefined) {\n return \"missing\";\n }\n if (!(await pathExists(overlay.workspacePath))) {\n return \"missing\";\n }\n if (!(await pathExists(overlay.archivePath))) {\n return \"dirty\";\n }\n\n try {\n const [indexedFingerprint, currentFingerprint] = await Promise.all([\n readSearchIndexCacheFingerprint(overlay.workspacePath),\n createCurrentArchiveSearchIndexFingerprint(overlay.archivePath),\n ]);\n\n if (indexedFingerprint === undefined) {\n return \"dirty\";\n }\n\n return indexedFingerprint === currentFingerprint ? \"current\" : \"dirty\";\n } catch {\n return \"dirty\";\n }\n}\n\nasync function readSearchIndexCacheFingerprint(\n databasePath: string,\n): Promise<string | undefined> {\n const database = await DocumentDatabase.open(databasePath, \"\", {\n readonly: true,\n });\n\n try {\n return await readSearchIndexFingerprintFromDatabase(database);\n } finally {\n await database.close();\n }\n}\n\nasync function createCurrentArchiveSearchIndexFingerprint(\n archivePath: string,\n): Promise<string> {\n const directoryPath = await createWikiGraphTempDirectory(\"archive-open\");\n\n try {\n await extractWikgArchive(archivePath, directoryPath);\n const document = await DirectoryDocument.open(directoryPath);\n\n try {\n return await createArchiveSearchIndexFingerprint(document);\n } finally {\n await document.release();\n }\n } finally {\n await rm(directoryPath, { force: true, recursive: true });\n }\n}\n\nasync function hasActiveArchiveOwnerOrSqliteLease(\n archiveKey: string,\n entryPath: string,\n): Promise<boolean> {\n return await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n const ownerCount = await state.queryOne(\n \"SELECT COUNT(*) AS count FROM archive_owners WHERE archive_key = ?\",\n [archiveKey],\n (row) => getNumber(row, \"count\"),\n );\n const leaseCount = await state.queryOne(\n `\nSELECT COUNT(*) AS count\nFROM entry_sqlite_leases\nWHERE archive_key = ?\n AND entry_path = ?\n`,\n [archiveKey, entryPath],\n (row) => getNumber(row, \"count\"),\n );\n\n return (ownerCount ?? 0) > 0 || (leaseCount ?? 0) > 0;\n });\n}\n\nasync function hasActiveWorkspaceUse(archiveKey: string): Promise<boolean> {\n return await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n const ownerCount = await state.queryOne(\n \"SELECT COUNT(*) AS count FROM archive_owners WHERE archive_key = ?\",\n [archiveKey],\n (row) => getNumber(row, \"count\"),\n );\n const leaseCount = await state.queryOne(\n \"SELECT COUNT(*) AS count FROM entry_sqlite_leases WHERE archive_key = ?\",\n [archiveKey],\n (row) => getNumber(row, \"count\"),\n );\n const lockCount = await state.queryOne(\n \"SELECT COUNT(*) AS count FROM entry_locks WHERE archive_key = ?\",\n [archiveKey],\n (row) => getNumber(row, \"count\"),\n );\n\n return (\n (ownerCount ?? 0) > 0 || (leaseCount ?? 0) > 0 || (lockCount ?? 0) > 0\n );\n });\n}\n\nasync function removeOrphanedWorkspaceFiles(\n context: GcContext,\n): Promise<Pick<GcJobResult, \"freedBytes\" | \"removed\" | \"scanned\">> {\n const rootPath = getCoordinatorWorkspaceRootPath();\n let archiveKeyEntries: readonly WorkspaceDirectoryEntry[];\n\n try {\n archiveKeyEntries = await readdir(rootPath, { withFileTypes: true });\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return { freedBytes: 0, removed: 0, scanned: 0 };\n }\n\n throw error;\n }\n\n let freedBytes = 0;\n let removed = 0;\n let scanned = 0;\n\n for (const archiveKeyEntry of archiveKeyEntries) {\n if (!archiveKeyEntry.isDirectory()) {\n continue;\n }\n\n const archiveKey = archiveKeyEntry.name;\n\n if (await hasActiveWorkspaceUse(archiveKey)) {\n continue;\n }\n\n const referencedWorkspacePaths = new Set(\n (await listOverlays(archiveKey))\n .map((overlay) => overlay.workspacePath)\n .filter((path): path is string => path !== undefined),\n );\n\n for (const workspacePath of await listWorkspaceFiles(\n join(rootPath, archiveKey),\n )) {\n scanned += 1;\n\n if (isDisposableDirectoryEntry(basename(workspacePath))) {\n continue;\n }\n if (referencedWorkspacePaths.has(workspacePath)) {\n continue;\n }\n\n const size = await readPathSize(workspacePath);\n\n if (!context.dryRun) {\n await rm(workspacePath, { force: true });\n }\n\n freedBytes += size;\n removed += 1;\n }\n\n if (!context.dryRun) {\n const disposableDirectories = await removeDisposableDescendantDirectories(\n join(rootPath, archiveKey),\n );\n\n freedBytes += disposableDirectories.freedBytes;\n removed += disposableDirectories.removed;\n scanned += disposableDirectories.scanned;\n }\n }\n\n return { freedBytes, removed, scanned };\n}\n\nclass WikgDocumentFileStore implements DocumentFileStore {\n readonly #archiveKey: string;\n readonly #archivePath: string;\n #archiveReader: Promise<WikgArchiveReader> | undefined;\n #entrySourceByPath:\n | Map<\n string,\n | { readonly kind: \"archive\" }\n | { readonly kind: \"deleted\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n >\n | undefined;\n readonly #readonlyDatabase: boolean;\n readonly #searchIndexWritebackPolicy: WorkspaceWritebackPolicy;\n readonly #sqliteLeaseOwnerId = createOwnerId();\n\n public constructor(\n archivePath: string,\n options: {\n readonly readonlyDatabase?: boolean;\n readonly searchIndexWritebackPolicy?: WorkspaceWritebackPolicy;\n readonly session?: WikgArchiveSession;\n },\n ) {\n this.#archivePath = resolve(archivePath);\n this.#archiveKey = createArchiveKey(this.#archivePath);\n this.#readonlyDatabase = options.readonlyDatabase === true;\n this.#searchIndexWritebackPolicy =\n options.searchIndexWritebackPolicy ?? \"archive\";\n this.#session = options.session;\n }\n\n async #readOverlay(entryPath: string): Promise<EntryOverlay | undefined> {\n const overlay = await readOverlay(this.#archiveKey, entryPath);\n\n if (overlay === undefined) {\n return undefined;\n }\n\n const signature = await createArchiveSignature(this.#archivePath);\n\n if (overlay.archiveSignature === signature) {\n return overlay;\n }\n\n await deleteOverlay(this.#archiveKey, entryPath);\n if (overlay.workspacePath !== undefined) {\n await rm(overlay.workspacePath, { force: true }).catch(() => undefined);\n }\n this.#entrySourceByPath?.delete(entryPath);\n return undefined;\n }\n\n readonly #session: WikgArchiveSession | undefined;\n\n public async close(): Promise<void> {\n if (this.#archiveReader !== undefined) {\n (await this.#archiveReader).close();\n this.#archiveReader = undefined;\n }\n await releaseSqliteLease({\n archiveKey: this.#archiveKey,\n entryPath: DATABASE_ENTRY_PATH,\n ownerId: this.#sqliteLeaseOwnerId,\n });\n await releaseSqliteLease({\n archiveKey: this.#archiveKey,\n entryPath: SEARCH_INDEX_DATABASE_ENTRY_PATH,\n ownerId: this.#sqliteLeaseOwnerId,\n });\n }\n\n public async deleteFile(path: string): Promise<void> {\n const entryPath = this.#toEntryPath(path);\n\n await withEntryLock(this.#archiveKey, entryPath, \"write\", async () => {\n await withEntryLock(this.#archiveKey, entryPath, \"state\", async () => {\n const overlay = await this.#readOverlay(entryPath);\n\n await upsertOverlay({\n archiveKey: this.#archiveKey,\n archivePath: this.#archivePath,\n entryPath,\n kind: \"deleted\",\n });\n if (overlay?.workspacePath !== undefined) {\n await rm(overlay.workspacePath, { force: true }).catch(\n () => undefined,\n );\n }\n this.#entrySourceByPath?.set(entryPath, { kind: \"deleted\" });\n this.#session?.modifyEntry(entryPath);\n });\n });\n }\n\n public async deleteTree(path: string): Promise<void> {\n const rootEntryPath = this.#toEntryPath(path);\n const entries = await listVisibleEntryPaths(\n await this.#listArchiveEntries(),\n {\n archiveKey: this.#archiveKey,\n prefix: `${rootEntryPath}/`,\n },\n );\n\n for (const entryPath of entries) {\n await this.deleteFile(entryPath);\n }\n }\n\n public ensureDirectory(): Promise<void> {\n return Promise.resolve();\n }\n\n public initializeDatabaseSchema(): boolean {\n return false;\n }\n\n public markDatabaseDirty(): void {\n if (!this.#readonlyDatabase) {\n this.#session?.observeDirtyEntry(DATABASE_ENTRY_PATH);\n }\n }\n\n public markSearchIndexDatabaseDirty(): void {\n if (\n !this.#readonlyDatabase &&\n this.#searchIndexWritebackPolicy === \"archive\"\n ) {\n this.#session?.observeDirtyEntry(SEARCH_INDEX_DATABASE_ENTRY_PATH);\n }\n }\n\n public openDatabaseReadonly(): boolean {\n return this.#readonlyDatabase;\n }\n\n public async listFiles(path: string): Promise<readonly string[]> {\n const directoryEntryPath = this.#toEntryPath(path);\n const prefix = directoryEntryPath === \"\" ? \"\" : `${directoryEntryPath}/`;\n const entries = (await this.#listDirectoryEntryPaths(prefix)).map(\n ([entryPath]) => entryPath,\n );\n\n return entries\n .map((entryPath) => entryPath.slice(prefix.length))\n .filter((entryPath) => !entryPath.includes(\"/\"))\n .map((entryPath) => posix.basename(entryPath))\n .sort((left, right) => left.localeCompare(right));\n }\n\n public async listFileContents(\n path: string,\n ): Promise<ReadonlyMap<string, Uint8Array>> {\n const directoryEntryPath = this.#toEntryPath(path);\n const prefix = directoryEntryPath === \"\" ? \"\" : `${directoryEntryPath}/`;\n return await withEntryLock(\n this.#archiveKey,\n normalizeEntryDirectoryPrefix(prefix),\n \"read\",\n async () => {\n const contents = new Map<string, Uint8Array>();\n\n for (const [entryPath, source] of await this.#listDirectoryEntryPaths(\n prefix,\n )) {\n const name = entryPath.slice(prefix.length);\n\n if (name.includes(\"/\")) {\n continue;\n }\n if (source.kind === \"workspace\") {\n contents.set(name, await readFile(source.path));\n continue;\n }\n\n const content = await this.#readArchiveEntry(entryPath);\n\n if (content !== undefined) {\n contents.set(name, content);\n }\n }\n\n return contents;\n },\n );\n }\n\n public async readFile(path: string): Promise<Uint8Array | undefined> {\n const entryPath = this.#toEntryPath(path);\n\n return await withEntryLock(\n this.#archiveKey,\n entryPath,\n \"read\",\n async () => {\n const source =\n (await this.#getEntrySources()).get(entryPath) ??\n (await withEntryLock(\n this.#archiveKey,\n entryPath,\n \"state\",\n async () =>\n await resolveEntrySource({\n archiveKey: this.#archiveKey,\n entryPath,\n }),\n ));\n\n if (source.kind === \"deleted\") {\n this.#session?.observeDirtyEntry(entryPath);\n return undefined;\n }\n if (source.kind === \"workspace\") {\n this.#session?.observeDirtyEntry(entryPath);\n return await readFile(source.path);\n }\n\n return await this.#readArchiveEntry(entryPath);\n },\n );\n }\n\n public async resolveDatabasePath(): Promise<string> {\n return await this.#resolveSqliteDatabasePath(DATABASE_ENTRY_PATH, {\n createIfMissing: true,\n readonly: this.#readonlyDatabase,\n });\n }\n\n public async resolveSearchIndexDatabasePath(): Promise<string> {\n return await this.#resolveSqliteDatabasePath(\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n {\n createIfMissing: !this.#readonlyDatabase,\n readonly: this.#readonlyDatabase,\n },\n );\n }\n\n async #resolveSqliteDatabasePath(\n entryPath: string,\n options: { readonly createIfMissing: boolean; readonly readonly: boolean },\n ): Promise<string> {\n return await withEntryLock(\n this.#archiveKey,\n entryPath,\n \"state\",\n async () => {\n let overlay = await this.#readOverlay(entryPath);\n\n if (\n entryPath === SEARCH_INDEX_DATABASE_ENTRY_PATH &&\n overlay?.kind !== \"file\"\n ) {\n await tryAdoptSearchIndexCacheOverlay({\n targetArchiveKey: this.#archiveKey,\n targetArchivePath: this.#archivePath,\n });\n overlay = await this.#readOverlay(entryPath);\n }\n\n if (overlay?.kind !== \"file\") {\n const content = await readWikgArchiveEntry(\n this.#archivePath,\n entryPath,\n );\n\n if (content === undefined && !options.createIfMissing) {\n throw new Error(`Archive SQLite entry is missing: ${entryPath}`);\n }\n const workspacePath = await createWorkspaceFilePath(\n this.#archiveKey,\n entryPath,\n );\n\n await mkdir(dirname(workspacePath), { recursive: true });\n await writeFile(workspacePath, content ?? new Uint8Array());\n await upsertOverlay({\n archiveKey: this.#archiveKey,\n archivePath: this.#archivePath,\n entryPath,\n kind: \"file\",\n workspacePath,\n });\n overlay = await this.#readOverlay(entryPath);\n }\n\n if (overlay?.workspacePath === undefined) {\n throw new Error(\"Could not materialize SQLite database.\");\n }\n\n await acquireSqliteLease({\n archiveKey: this.#archiveKey,\n entryPath,\n mode: options.readonly ? \"read\" : \"write\",\n ownerId: this.#sqliteLeaseOwnerId,\n });\n return overlay.workspacePath;\n },\n );\n }\n\n public async writeFile(\n path: string,\n content: string | Uint8Array,\n options: { readonly overwrite?: boolean },\n ): Promise<void> {\n const entryPath = this.#toEntryPath(path);\n\n await withEntryLock(this.#archiveKey, entryPath, \"write\", async () => {\n const source = await withEntryLock(\n this.#archiveKey,\n entryPath,\n \"state\",\n async () =>\n await resolveEntrySource({\n archiveKey: this.#archiveKey,\n entryPath,\n }),\n );\n const archiveEntryExists =\n source.kind === \"archive\" &&\n (await this.#readArchiveEntry(entryPath)) !== undefined;\n\n if (\n options.overwrite !== true &&\n (source.kind === \"workspace\" || archiveEntryExists)\n ) {\n throw new Error(`File already exists: ${path}`);\n }\n\n await withEntryLock(this.#archiveKey, entryPath, \"state\", async () => {\n const workspacePath = await createWorkspaceFilePath(\n this.#archiveKey,\n entryPath,\n );\n\n await mkdir(dirname(workspacePath), { recursive: true });\n const temporaryWorkspacePath = `${workspacePath}.${process.pid}.${Date.now()}.tmp`;\n\n try {\n await writeFile(temporaryWorkspacePath, content);\n await rename(temporaryWorkspacePath, workspacePath);\n } finally {\n await rm(temporaryWorkspacePath, { force: true }).catch(\n () => undefined,\n );\n }\n await upsertOverlay({\n archiveKey: this.#archiveKey,\n archivePath: this.#archivePath,\n entryPath,\n kind: \"file\",\n workspacePath,\n });\n this.#entrySourceByPath?.set(entryPath, {\n kind: \"workspace\",\n path: workspacePath,\n });\n if (source.kind === \"workspace\" && source.path !== workspacePath) {\n await rm(source.path, { force: true }).catch(() => undefined);\n }\n this.#session?.modifyEntry(entryPath);\n });\n });\n }\n\n #toEntryPath(path: string): string {\n const resolvedPath = resolve(path);\n const rootPrefix = this.#archivePath.endsWith(\"/\")\n ? this.#archivePath\n : `${this.#archivePath}/`;\n\n if (resolvedPath.startsWith(rootPrefix)) {\n return normalizeEntryPath(resolvedPath.slice(rootPrefix.length));\n }\n\n return normalizeEntryPath(path);\n }\n\n async #listArchiveEntries(): Promise<readonly string[]> {\n return (await this.#getArchiveReader()).listEntries();\n }\n\n async #readArchiveEntry(entryPath: string): Promise<Buffer | undefined> {\n return await (await this.#getArchiveReader()).readEntry(entryPath);\n }\n\n async #getArchiveReader(): Promise<WikgArchiveReader> {\n this.#archiveReader ??= WikgArchiveReader.open(this.#archivePath);\n return await this.#archiveReader;\n }\n\n async #getEntrySources(): Promise<\n Map<\n string,\n | { readonly kind: \"archive\" }\n | { readonly kind: \"deleted\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n >\n > {\n if (this.#entrySourceByPath !== undefined) {\n return this.#entrySourceByPath;\n }\n\n const entries = new Map<\n string,\n | { readonly kind: \"archive\" }\n | { readonly kind: \"deleted\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n >();\n\n for (const entryPath of await this.#listArchiveEntries()) {\n entries.set(entryPath, { kind: \"archive\" });\n }\n for (const overlay of await listOverlays(this.#archiveKey)) {\n if (overlay.kind === \"deleted\") {\n entries.set(overlay.entryPath, { kind: \"deleted\" });\n } else if (overlay.workspacePath !== undefined) {\n entries.set(overlay.entryPath, {\n kind: \"workspace\",\n path: overlay.workspacePath,\n });\n }\n }\n\n this.#entrySourceByPath = entries;\n return entries;\n }\n\n async #listDirectoryEntryPaths(\n prefix: string,\n ): Promise<\n Array<\n readonly [\n string,\n (\n | { readonly kind: \"archive\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n ),\n ]\n >\n > {\n const entries: Array<\n readonly [\n string,\n (\n | { readonly kind: \"archive\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n ),\n ]\n > = [];\n\n for (const [entryPath, source] of await this.#getEntrySources()) {\n if (source.kind === \"deleted\" || !entryPath.startsWith(prefix)) {\n continue;\n }\n\n entries.push([entryPath, source]);\n }\n\n return entries.sort(([left], [right]) => left.localeCompare(right));\n }\n}\n\nasync function flushArchiveOverlays(\n archiveKey: string,\n requestedEntryPaths?: ReadonlySet<string>,\n): Promise<void> {\n const overlays = (await listOverlays(archiveKey)).filter(\n (overlay) =>\n requestedEntryPaths === undefined ||\n requestedEntryPaths.has(overlay.entryPath),\n );\n const archivePath = await resolveArchivePathFromKey(archiveKey);\n\n if (overlays.length === 0 || archivePath === undefined) {\n return;\n }\n\n const entryPaths = overlays\n .map((overlay) => overlay.entryPath)\n .sort((left, right) => left.localeCompare(right));\n const lockedEntryPaths = new Set(entryPaths);\n const releaseLocks: Array<() => Promise<void>> = [];\n\n try {\n for (const entryPath of entryPaths) {\n releaseLocks.push(await acquireEntryLock(archiveKey, entryPath, \"write\"));\n }\n for (const entryPath of entryPaths) {\n releaseLocks.push(await acquireEntryLock(archiveKey, entryPath, \"state\"));\n }\n\n if (entryPaths.includes(DATABASE_ENTRY_PATH)) {\n await waitForSqliteLeasesToDrain(archiveKey, DATABASE_ENTRY_PATH);\n }\n if (entryPaths.includes(SEARCH_INDEX_DATABASE_ENTRY_PATH)) {\n await waitForSqliteLeasesToDrain(\n archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n );\n }\n\n const currentOverlays = (await listOverlays(archiveKey)).filter((overlay) =>\n lockedEntryPaths.has(overlay.entryPath),\n );\n\n if (currentOverlays.length === 0) {\n return;\n }\n\n const releaseCommit = await acquireArchiveCommitLock(archiveKey);\n\n try {\n const temporaryDirectoryPath = await mkdtemp(\n join(tmpdir(), \"wikigraph-flush-\"),\n );\n const temporaryArchivePath = join(\n temporaryDirectoryPath,\n basename(archivePath),\n );\n\n try {\n await writeWikgArchiveWithOverlays(\n archivePath,\n temporaryArchivePath,\n currentOverlays.map(toArchiveOverlay),\n );\n await mkdir(dirname(archivePath), { recursive: true });\n await rename(temporaryArchivePath, archivePath);\n } finally {\n await rm(temporaryDirectoryPath, { force: true, recursive: true });\n }\n } finally {\n await releaseCommit();\n }\n\n for (const overlay of currentOverlays) {\n await deleteOverlay(archiveKey, overlay.entryPath);\n if (overlay.workspacePath !== undefined) {\n await rm(overlay.workspacePath, { force: true });\n }\n }\n } finally {\n for (const release of releaseLocks.reverse()) {\n await release();\n }\n }\n}\n\nasync function acquireArchiveCommitLock(\n archiveKey: string,\n): Promise<() => Promise<void>> {\n const ownerId = createOwnerId();\n\n while (true) {\n const acquired = await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.transaction(async () => {\n const existing = await state.queryOne(\n \"SELECT * FROM archive_commit_locks WHERE archive_key = ?\",\n [archiveKey],\n mapArchiveCommitLock,\n );\n\n if (existing !== undefined) {\n return false;\n }\n\n await state.run(\n `\nINSERT INTO archive_commit_locks (\n archive_key, owner_id, owner_pid, heartbeat_at, created_at\n) VALUES (?, ?, ?, ?, ?)\n`,\n [archiveKey, ownerId, process.pid, Date.now(), Date.now()],\n );\n return true;\n });\n });\n\n if (acquired) {\n return async () => {\n await withStateDatabase(async (releaseState) => {\n await releaseState.run(\n \"DELETE FROM archive_commit_locks WHERE archive_key = ? AND owner_id = ?\",\n [archiveKey, ownerId],\n );\n });\n };\n }\n\n await delay(LOCK_POLL_INTERVAL_MS);\n }\n}\n\nasync function registerArchiveOwner(input: {\n readonly archiveKey: string;\n readonly ownerId: string;\n}): Promise<void> {\n await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n await state.run(\n `\nINSERT OR REPLACE INTO archive_owners (\n archive_key, owner_id, owner_pid, heartbeat_at, created_at\n) VALUES (?, ?, ?, ?, ?)\n`,\n [input.archiveKey, input.ownerId, process.pid, Date.now(), Date.now()],\n );\n });\n}\n\nasync function heartbeatArchiveOwner(input: {\n readonly archiveKey: string;\n readonly ownerId: string;\n}): Promise<void> {\n await withStateDatabase(async (state) => {\n await state.run(\n `\nUPDATE archive_owners\nSET heartbeat_at = ?, owner_pid = ?\nWHERE archive_key = ? AND owner_id = ?\n`,\n [Date.now(), process.pid, input.archiveKey, input.ownerId],\n );\n });\n}\n\nasync function unregisterArchiveOwner(input: {\n readonly archiveKey: string;\n readonly ownerId: string;\n}): Promise<void> {\n await withStateDatabase(async (state) => {\n await state.transaction(async () => {\n await state.run(\n \"DELETE FROM entry_sqlite_leases WHERE archive_key = ? AND owner_id = ?\",\n [input.archiveKey, input.ownerId],\n );\n await state.run(\n \"DELETE FROM archive_owners WHERE archive_key = ? AND owner_id = ?\",\n [input.archiveKey, input.ownerId],\n );\n });\n });\n}\n\nasync function reapArchive(archiveKey: string): Promise<void> {\n await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n });\n\n const staleOwnerIds = await withStateDatabase(\n async (state) =>\n await state.queryAll(\n `\nSELECT owner_id\nFROM archive_owners\nWHERE archive_key = ?\n AND heartbeat_at <= ?\n`,\n [archiveKey, Date.now() - LOCK_STALE_TIMEOUT_MS],\n (row) => getString(row, \"owner_id\"),\n ),\n );\n\n if (staleOwnerIds.length === 0) {\n return;\n }\n\n await withStateDatabase(async (state) => {\n await state.transaction(async () => {\n await state.run(\n `\nDELETE FROM entry_sqlite_leases\nWHERE archive_key = ?\n AND owner_id IN (${createPlaceholders(staleOwnerIds.length)})\n`,\n [archiveKey, ...staleOwnerIds],\n );\n await state.run(\n `\nDELETE FROM archive_owners\nWHERE archive_key = ?\n AND owner_id IN (${createPlaceholders(staleOwnerIds.length)})\n`,\n [archiveKey, ...staleOwnerIds],\n );\n });\n });\n\n await flushArchiveOverlays(archiveKey);\n}\n\nasync function acquireEntryLock(\n archiveKey: string,\n entryPath: string,\n mode: EntryLockMode,\n): Promise<() => Promise<void>> {\n const ownerId = createOwnerId();\n\n while (true) {\n const acquired = await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.transaction(async () => {\n const conflicts = await state.queryAll(\n `\nSELECT *\nFROM entry_locks\nWHERE archive_key = ?\n`,\n [archiveKey],\n mapEntryLock,\n );\n\n if (\n conflicts.some(\n (lock) =>\n lock.ownerId !== ownerId &&\n lockPathsConflict(entryPath, lock.entryPath) &&\n locksConflict(mode, lock.mode),\n )\n ) {\n return false;\n }\n\n await state.run(\n `\nINSERT INTO entry_locks (\n archive_key, entry_path, mode, owner_id, owner_pid, heartbeat_at, created_at\n) VALUES (?, ?, ?, ?, ?, ?, ?)\n`,\n [\n archiveKey,\n entryPath,\n mode,\n ownerId,\n process.pid,\n Date.now(),\n Date.now(),\n ],\n );\n return true;\n });\n });\n\n if (acquired) {\n return async () => {\n await withStateDatabase(async (releaseState) => {\n await releaseState.run(\n `\nDELETE FROM entry_locks\nWHERE archive_key = ? AND entry_path = ? AND owner_id = ?\n`,\n [archiveKey, entryPath, ownerId],\n );\n });\n };\n }\n\n await delay(LOCK_POLL_INTERVAL_MS);\n }\n}\n\nasync function withEntryLock<T>(\n archiveKey: string,\n entryPath: string,\n mode: EntryLockMode,\n operation: () => Promise<T> | T,\n): Promise<T> {\n const release = await acquireEntryLock(archiveKey, entryPath, mode);\n\n try {\n return await operation();\n } finally {\n await release();\n }\n}\n\nfunction locksConflict(requested: EntryLockMode, existing: EntryLockMode) {\n if (requested === \"state\" || existing === \"state\") {\n return requested === \"state\" && existing === \"state\";\n }\n if (requested === \"read\" && existing === \"read\") {\n return false;\n }\n\n return true;\n}\n\nfunction lockPathsConflict(requested: string, existing: string): boolean {\n return (\n requested === existing ||\n lockPathContains(requested, existing) ||\n lockPathContains(existing, requested)\n );\n}\n\nfunction lockPathContains(parent: string, child: string): boolean {\n return parent.endsWith(\"/\") && child.startsWith(parent);\n}\n\nasync function acquireSqliteLease(input: {\n readonly archiveKey: string;\n readonly entryPath: string;\n readonly mode: SqliteLeaseMode;\n readonly ownerId: string;\n}): Promise<void> {\n await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n await state.run(\n `\nINSERT OR REPLACE INTO entry_sqlite_leases (\n archive_key, entry_path, mode, owner_id, owner_pid, heartbeat_at, created_at\n) VALUES (?, ?, ?, ?, ?, ?, ?)\n`,\n [\n input.archiveKey,\n input.entryPath,\n input.mode,\n input.ownerId,\n process.pid,\n Date.now(),\n Date.now(),\n ],\n );\n });\n}\n\nasync function releaseSqliteLease(input: {\n readonly archiveKey: string;\n readonly entryPath: string;\n readonly ownerId: string;\n}): Promise<void> {\n await withStateDatabase(async (state) => {\n await state.run(\n `\nDELETE FROM entry_sqlite_leases\nWHERE archive_key = ? AND entry_path = ? AND owner_id = ?\n`,\n [input.archiveKey, input.entryPath, input.ownerId],\n );\n });\n}\n\nasync function waitForSqliteLeasesToDrain(\n archiveKey: string,\n entryPath: string,\n): Promise<void> {\n while (true) {\n const count = await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.queryOne(\n `\nSELECT COUNT(*) AS count\nFROM entry_sqlite_leases AS lease\nLEFT JOIN archive_owners AS owner\n ON owner.archive_key = lease.archive_key\n AND owner.owner_id = lease.owner_id\nWHERE lease.archive_key = ?\n AND lease.entry_path = ?\n AND (\n owner.owner_id IS NULL\n OR owner.heartbeat_at > ?\n )\n`,\n [archiveKey, entryPath, Date.now() - LOCK_STALE_TIMEOUT_MS],\n (row) => getNumber(row, \"count\"),\n );\n });\n\n if (count === 0) {\n return;\n }\n\n await delay(LOCK_POLL_INTERVAL_MS);\n }\n}\n\nasync function listVisibleEntryPaths(\n archiveEntries: readonly string[],\n input: { readonly archiveKey: string; readonly prefix: string },\n): Promise<readonly string[]> {\n const matchingArchiveEntries = archiveEntries.filter((entryPath) =>\n entryPath.startsWith(input.prefix),\n );\n const overlays = await withStateDatabase(\n async (state) =>\n await state.queryAll(\n `\nSELECT *\nFROM entry_overlays\nWHERE archive_key = ?\n`,\n [input.archiveKey],\n mapEntryOverlay,\n ),\n );\n const entries = new Set(matchingArchiveEntries);\n\n for (const overlay of overlays) {\n if (!overlay.entryPath.startsWith(input.prefix)) {\n continue;\n }\n if (overlay.kind === \"deleted\") {\n entries.delete(overlay.entryPath);\n } else {\n entries.add(overlay.entryPath);\n }\n }\n\n return [...entries].sort((left, right) => left.localeCompare(right));\n}\n\nasync function resolveEntrySource(input: {\n readonly archiveKey: string;\n readonly entryPath: string;\n}): Promise<\n | { readonly kind: \"archive\" }\n | { readonly kind: \"deleted\" }\n | { readonly kind: \"workspace\"; readonly path: string }\n> {\n const overlay = await readOverlay(input.archiveKey, input.entryPath);\n\n if (overlay?.kind === \"deleted\") {\n return { kind: \"deleted\" };\n }\n if (overlay?.workspacePath !== undefined) {\n return { kind: \"workspace\", path: overlay.workspacePath };\n }\n return { kind: \"archive\" };\n}\n\nasync function readOverlay(\n archiveKey: string,\n entryPath: string,\n): Promise<EntryOverlay | undefined> {\n return await withStateDatabase(\n async (state) =>\n await state.queryOne(\n \"SELECT * FROM entry_overlays WHERE archive_key = ? AND entry_path = ?\",\n [archiveKey, entryPath],\n mapEntryOverlay,\n ),\n );\n}\n\nasync function listOverlays(\n archiveKey: string,\n): Promise<readonly EntryOverlay[]> {\n return await withStateDatabase(\n async (state) =>\n await state.queryAll(\n `\nSELECT *\nFROM entry_overlays\nWHERE archive_key = ?\nORDER BY entry_path ASC\n`,\n [archiveKey],\n mapEntryOverlay,\n ),\n );\n}\n\nasync function upsertOverlay(input: {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly entryPath: string;\n readonly kind: \"deleted\" | \"file\";\n readonly workspacePath?: string;\n}): Promise<void> {\n const mutationToken =\n input.entryPath === SEARCH_INDEX_DATABASE_ENTRY_PATH\n ? await readWikgArchiveMutationToken(input.archivePath)\n : undefined;\n\n await withStateDatabase(async (state) => {\n await state.run(\n `\nINSERT INTO entry_overlays (\n archive_key, archive_path, entry_path, kind, workspace_path,\n archive_signature, mutation_token, updated_at\n) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\nON CONFLICT(archive_key, entry_path)\nDO UPDATE SET kind = excluded.kind,\n archive_path = excluded.archive_path,\n workspace_path = excluded.workspace_path,\n archive_signature = excluded.archive_signature,\n mutation_token = excluded.mutation_token,\n updated_at = excluded.updated_at\n`,\n [\n input.archiveKey,\n input.archivePath,\n input.entryPath,\n input.kind,\n input.workspacePath ?? null,\n await createArchiveSignature(input.archivePath),\n mutationToken ?? null,\n Date.now(),\n ],\n );\n });\n}\n\nasync function tryAdoptSearchIndexCacheOverlay(input: {\n readonly targetArchiveKey: string;\n readonly targetArchivePath: string;\n}): Promise<void> {\n const mutationToken = await readWikgArchiveMutationToken(\n input.targetArchivePath,\n );\n const candidates = await listSearchIndexAdoptionCandidates({\n mutationToken,\n targetArchiveKey: input.targetArchiveKey,\n });\n const adoptable: EntryOverlay[] = [];\n\n for (const candidate of candidates) {\n if (\n candidate.workspacePath === undefined ||\n (await pathExists(candidate.archivePath)) ||\n (await hasActiveWorkspaceUse(candidate.archiveKey))\n ) {\n continue;\n }\n\n adoptable.push(candidate);\n }\n\n if (adoptable.length !== 1) {\n return;\n }\n\n const candidate = adoptable[0]!;\n const releaseWriteLock = await acquireEntryLock(\n candidate.archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n \"write\",\n );\n\n try {\n const releaseStateLock = await acquireEntryLock(\n candidate.archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n \"state\",\n );\n\n try {\n const current = await readOverlay(\n candidate.archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n );\n const target = await readOverlay(\n input.targetArchiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n );\n\n if (\n current?.workspacePath === undefined ||\n current.workspacePath !== candidate.workspacePath ||\n current.mutationToken !== mutationToken ||\n target !== undefined ||\n (await pathExists(candidate.archivePath)) ||\n (await hasActiveArchiveOwnerOrSqliteLease(\n candidate.archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n ))\n ) {\n return;\n }\n\n const targetWorkspacePath = await createWorkspaceFilePath(\n input.targetArchiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n );\n\n await rm(targetWorkspacePath, { force: true }).catch(() => undefined);\n await mkdir(dirname(targetWorkspacePath), { recursive: true });\n await rename(current.workspacePath, targetWorkspacePath);\n await withStateDatabase(async (state) => {\n await state.run(\n `\nUPDATE entry_overlays\nSET archive_key = ?,\n archive_path = ?,\n workspace_path = ?,\n archive_signature = ?,\n updated_at = ?\nWHERE archive_key = ?\n AND entry_path = ?\n`,\n [\n input.targetArchiveKey,\n input.targetArchivePath,\n targetWorkspacePath,\n await createArchiveSignature(input.targetArchivePath),\n Date.now(),\n candidate.archiveKey,\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n ],\n );\n });\n } finally {\n await releaseStateLock();\n }\n } finally {\n await releaseWriteLock();\n }\n}\n\nasync function listSearchIndexAdoptionCandidates(input: {\n readonly mutationToken: string;\n readonly targetArchiveKey: string;\n}): Promise<readonly EntryOverlay[]> {\n return await withStateDatabase(async (state) => {\n await cleanupStaleState(state);\n return await state.queryAll(\n `\nSELECT *\nFROM entry_overlays\nWHERE entry_path = ?\n AND kind = 'file'\n AND workspace_path IS NOT NULL\n AND mutation_token = ?\n AND archive_key <> ?\nORDER BY updated_at ASC\n`,\n [\n SEARCH_INDEX_DATABASE_ENTRY_PATH,\n input.mutationToken,\n input.targetArchiveKey,\n ],\n mapEntryOverlay,\n );\n });\n}\n\nasync function deleteOverlay(\n archiveKey: string,\n entryPath: string,\n): Promise<void> {\n await withStateDatabase(async (state) => {\n await state.run(\n \"DELETE FROM entry_overlays WHERE archive_key = ? AND entry_path = ?\",\n [archiveKey, entryPath],\n );\n });\n}\n\nasync function resolveArchivePathFromKey(\n archiveKey: string,\n): Promise<string | undefined> {\n return await withStateDatabase(\n async (state) =>\n await state.queryOne(\n \"SELECT archive_path FROM entry_overlays WHERE archive_key = ? LIMIT 1\",\n [archiveKey],\n (row) => getString(row, \"archive_path\"),\n ),\n );\n}\n\nasync function cleanupStaleState(state: Database): Promise<void> {\n const now = Date.now();\n const staleOwnerPids = new Set<number>();\n\n for (const tableName of [\n \"archive_owners\",\n \"entry_locks\",\n \"entry_sqlite_leases\",\n \"archive_commit_locks\",\n ]) {\n const rows = await state.queryAll(\n `\nSELECT DISTINCT owner_pid\nFROM ${tableName}\nWHERE owner_pid IS NOT NULL\n AND heartbeat_at <= ?\n`,\n [now - LOCK_STALE_TIMEOUT_MS],\n (row) => getNumber(row, \"owner_pid\"),\n );\n\n for (const ownerPid of rows) {\n if (!isProcessAlive(ownerPid)) {\n staleOwnerPids.add(ownerPid);\n }\n }\n }\n\n if (staleOwnerPids.size === 0) {\n return;\n }\n\n await state.run(\n `\nDELETE FROM archive_owners\nWHERE owner_pid IN (${createPlaceholders(staleOwnerPids.size)})\n`,\n [...staleOwnerPids],\n );\n await state.run(\n `\nDELETE FROM entry_locks\nWHERE owner_pid IN (${createPlaceholders(staleOwnerPids.size)})\n`,\n [...staleOwnerPids],\n );\n await state.run(\n `\nDELETE FROM entry_sqlite_leases\nWHERE owner_pid IN (${createPlaceholders(staleOwnerPids.size)})\n`,\n [...staleOwnerPids],\n );\n await state.run(\n `\nDELETE FROM archive_commit_locks\nWHERE owner_pid IN (${createPlaceholders(staleOwnerPids.size)})\n`,\n [...staleOwnerPids],\n );\n}\n\nfunction createPlaceholders(count: number): string {\n return Array.from({ length: count }, () => \"?\").join(\", \");\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ESRCH\") {\n return false;\n }\n\n return true;\n }\n}\n\nasync function withStateDatabase<T>(\n operation: (state: Database) => Promise<T> | T,\n): Promise<T> {\n return await STATE_DATABASE_SEMAPHORE.use(async () => {\n const state = await openStateDatabase();\n\n try {\n return await operation(state);\n } finally {\n await state.close();\n }\n });\n}\n\nasync function openStateDatabase(): Promise<Database> {\n const database = await openSharedStateDatabase(\n join(getCoordinatorStateDirectoryPath(), \"staging.sqlite\"),\n STATE_SCHEMA_SQL,\n );\n\n await migrateStateDatabase(database);\n return database;\n}\n\nasync function migrateStateDatabase(database: Database): Promise<void> {\n const overlayColumns = await database.queryAll(\n \"PRAGMA table_info(entry_overlays)\",\n undefined,\n (row) => getString(row, \"name\"),\n );\n\n if (!overlayColumns.includes(\"archive_signature\")) {\n await database.run(`\n ALTER TABLE entry_overlays\n ADD COLUMN archive_signature TEXT\n `);\n }\n\n if (!overlayColumns.includes(\"mutation_token\")) {\n await database.run(`\n ALTER TABLE entry_overlays\n ADD COLUMN mutation_token TEXT\n `);\n }\n\n const sqliteLeaseColumns = await database.queryAll(\n \"PRAGMA table_info(entry_sqlite_leases)\",\n undefined,\n (row) => getString(row, \"name\"),\n );\n\n if (!sqliteLeaseColumns.includes(\"mode\")) {\n await database.run(`\n ALTER TABLE entry_sqlite_leases\n ADD COLUMN mode TEXT NOT NULL DEFAULT 'read'\n `);\n }\n}\n\nasync function createWorkspaceFilePath(\n archiveKey: string,\n entryPath: string,\n): Promise<string> {\n const directoryPath = join(\n getCoordinatorWorkspaceRootPath(),\n archiveKey,\n dirname(entryPath),\n );\n\n await mkdir(directoryPath, { recursive: true });\n return join(directoryPath, basename(entryPath));\n}\n\nfunction getCoordinatorWorkspaceRootPath(): string {\n return join(getCoordinatorStateDirectoryPath(), \"work\");\n}\n\nasync function ensureEmptyDirectory(directoryPath: string): Promise<void> {\n await mkdir(directoryPath, { recursive: true });\n\n const entries = await readdir(directoryPath);\n\n if (entries.length > 0) {\n throw new Error(`Read workspace directory is not empty: ${directoryPath}`);\n }\n}\n\nasync function listWorkspaceFiles(directoryPath: string): Promise<string[]> {\n let entries: readonly WorkspaceDirectoryEntry[];\n\n try {\n entries = await readdir(directoryPath, { withFileTypes: true });\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return [];\n }\n\n throw error;\n }\n\n const files: string[] = [];\n\n for (const entry of entries) {\n const path = join(directoryPath, entry.name);\n\n if (entry.isDirectory()) {\n files.push(...(await listWorkspaceFiles(path)));\n } else if (entry.isFile()) {\n files.push(path);\n }\n }\n\n return files;\n}\n\nfunction toArchiveOverlay(overlay: EntryOverlay): WikgArchiveOverlay {\n if (overlay.kind === \"deleted\") {\n return {\n entryPath: overlay.entryPath,\n kind: \"deleted\",\n };\n }\n if (overlay.workspacePath === undefined) {\n throw new Error(`Missing workspace path for ${overlay.entryPath}.`);\n }\n\n return {\n entryPath: overlay.entryPath,\n kind: \"file\",\n workspacePath: overlay.workspacePath,\n };\n}\n\nfunction normalizeEntryPath(path: string): string {\n return path.replaceAll(\"\\\\\", \"/\").replace(/^\\/+/u, \"\");\n}\n\nfunction normalizeEntryDirectoryPrefix(path: string): string {\n const entryPath = normalizeEntryPath(path);\n\n return entryPath === \"\" || entryPath.endsWith(\"/\")\n ? entryPath\n : `${entryPath}/`;\n}\n\nfunction getCoordinatorStateDirectoryPath(): string {\n return resolveWikiGraphStagingDirectoryPath();\n}\n\nfunction createArchiveKey(archivePath: string): string {\n return createHash(\"sha256\").update(resolve(archivePath)).digest(\"hex\");\n}\n\nasync function createArchiveSignature(archivePath: string): Promise<string> {\n const stats = await stat(archivePath);\n\n return `${stats.size}:${stats.mtimeMs}`;\n}\n\nfunction createOwnerId(): string {\n return `${process.pid}-${randomUUID()}`;\n}\n\nasync function delay(ms: number): Promise<void> {\n await new Promise<void>((resolveDelay) => {\n setTimeout(resolveDelay, ms);\n });\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return false;\n }\n\n throw error;\n }\n}\n\ninterface EntryOverlay {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly archiveSignature?: string;\n readonly entryPath: string;\n readonly kind: \"deleted\" | \"file\";\n readonly mutationToken?: string;\n readonly updatedAt: number;\n readonly workspacePath?: string;\n}\n\ninterface EntryLock {\n readonly entryPath: string;\n readonly mode: EntryLockMode;\n readonly ownerId: string;\n}\n\ninterface ArchiveCommitLock {\n readonly ownerId: string;\n}\n\ninterface WorkspaceDirectoryEntry {\n isDirectory(): boolean;\n isFile(): boolean;\n readonly name: string;\n}\n\nfunction mapEntryOverlay(row: Record<string, unknown>): EntryOverlay {\n const workspacePath = getOptionalString(row, \"workspace_path\");\n const archiveSignature = getOptionalString(row, \"archive_signature\");\n const mutationToken = getOptionalString(row, \"mutation_token\");\n\n return {\n archiveKey: getString(row, \"archive_key\"),\n archivePath: getString(row, \"archive_path\"),\n ...(archiveSignature === undefined ? {} : { archiveSignature }),\n entryPath: getString(row, \"entry_path\"),\n kind: getOverlayKind(row),\n ...(mutationToken === undefined ? {} : { mutationToken }),\n updatedAt: getNumber(row, \"updated_at\"),\n ...(workspacePath === undefined ? {} : { workspacePath }),\n };\n}\n\nfunction mapEntryLock(row: Record<string, unknown>): EntryLock {\n return {\n entryPath: getString(row, \"entry_path\"),\n mode: getEntryLockMode(row),\n ownerId: getString(row, \"owner_id\"),\n };\n}\n\nfunction mapArchiveCommitLock(row: Record<string, unknown>): ArchiveCommitLock {\n return {\n ownerId: getString(row, \"owner_id\"),\n };\n}\n\nfunction getEntryLockMode(row: Record<string, unknown>): EntryLockMode {\n const mode = getString(row, \"mode\");\n\n if (mode === \"read\" || mode === \"state\" || mode === \"write\") {\n return mode;\n }\n\n throw new Error(`Unsupported entry lock mode: ${mode}.`);\n}\n\nfunction getOverlayKind(row: Record<string, unknown>): \"deleted\" | \"file\" {\n const kind = getString(row, \"kind\");\n\n if (kind === \"deleted\" || kind === \"file\") {\n return kind;\n }\n\n throw new Error(`Unsupported entry overlay kind: ${kind}.`);\n}\n\nfunction getString(row: Record<string, unknown>, key: string): string {\n const value = row[key];\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n\nfunction getNumber(row: Record<string, unknown>, key: string): number {\n const value = row[key];\n\n if (typeof value !== \"number\") {\n throw new TypeError(`Expected ${key} to be a number`);\n }\n\n return value;\n}\n\nfunction getOptionalString(\n row: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = row[key];\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n","import { resolve } from \"path\";\n\nimport { DirectoryDocument } from \"../document/index.js\";\n\nimport { WikgCoordinator } from \"./wikg-coordinator.js\";\nimport { deleteArchiveSearchSessions } from \"../archive/query/index.js\";\nimport { WikiGraphArchive } from \"../facade/wiki-graph-archive.js\";\n\nexport class WikiGraphArchiveFile {\n readonly #path: string;\n readonly #coordinator = new WikgCoordinator();\n\n public constructor(path: string) {\n this.#path = resolve(path);\n }\n\n public async read<T>(\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n options: {\n readonly documentDirPath?: string;\n } = {},\n ): Promise<T> {\n return await this.readDocument(\n async (document, directoryPath) =>\n await operation(new WikiGraphArchive(document, directoryPath)),\n options,\n );\n }\n\n public async readDocument<T>(\n operation: (\n document: DirectoryDocument,\n directoryPath: string,\n ) => Promise<T> | T,\n options: {\n readonly documentDirPath?: string;\n readonly searchIndexWritebackPolicy?: \"archive\" | \"cache\";\n } = {},\n ): Promise<T> {\n return await this.#coordinator.withArchiveSession(\n this.#path,\n async (session) => {\n if (options.documentDirPath !== undefined) {\n return await session.materializeReadWorkspace(\n options.documentDirPath,\n async (directoryPath) => {\n const document = await DirectoryDocument.open(directoryPath);\n\n try {\n return await operation(document, directoryPath);\n } finally {\n await document.release();\n }\n },\n );\n }\n\n const document = await DirectoryDocument.open(this.#path, {\n fileStore: session.createFileStore({\n readonlyDatabase: true,\n ...(options.searchIndexWritebackPolicy === undefined\n ? {}\n : {\n searchIndexWritebackPolicy:\n options.searchIndexWritebackPolicy,\n }),\n }),\n });\n\n try {\n return await operation(document, this.#path);\n } finally {\n await document.release();\n }\n },\n );\n }\n\n public async write<T>(\n operation: (document: DirectoryDocument) => Promise<T> | T,\n options: { readonly searchIndexWritebackPolicy?: \"archive\" | \"cache\" } = {},\n ): Promise<T> {\n return await this.#coordinator.withArchiveSession(\n this.#path,\n async (session) => {\n const document = await DirectoryDocument.open(this.#path, {\n fileStore: session.createFileStore({\n ...(options.searchIndexWritebackPolicy === undefined\n ? {}\n : {\n searchIndexWritebackPolicy:\n options.searchIndexWritebackPolicy,\n }),\n }),\n });\n\n try {\n return await operation(document);\n } finally {\n try {\n await document.release();\n } finally {\n await deleteArchiveSearchSessions(this.#path);\n }\n }\n },\n );\n }\n}\n","export {\n extractWikgArchive,\n listWikgArchiveEntries,\n readWikgArchiveEntry,\n readWikgArchiveFormatVersion,\n WikgArchiveReader,\n WIKG_FORMAT_VERSION,\n writeWikgArchive,\n writeWikgArchiveWithOverlays,\n} from \"./archive.js\";\nexport type { WikgArchiveOverlay } from \"./archive.js\";\nexport {\n formatLocatedChapterResourceUri,\n formatLocatedChapterSourceCollectionUri,\n formatLocatedChapterUri,\n formatLocatedWikiGraphUri,\n formatWikiGraphCommandUri,\n formatWikiGraphObjectUri,\n formatWikiGraphUriExpectedError,\n isWikiGraphJobUri,\n isWikiGraphUri,\n parseLocatedWikiGraphUri,\n requireArchiveUri,\n requireLocatedObjectOrArchiveUri,\n requireLocatedObjectUri,\n WIKI_GRAPH_ARCHIVE_EXTENSION,\n WIKI_GRAPH_JOB_URI_PREFIX,\n WIKI_GRAPH_URI_PREFIX,\n} from \"./archive-uri.js\";\nexport type { LocatedWikiGraphUri } from \"./archive-uri.js\";\nexport { WikiGraphArchiveFile } from \"./wiki-graph-archive-file.js\";\nexport { runWikgCoordinatorGc, WikgCoordinator } from \"./wikg-coordinator.js\";\n","import type { ReadonlyDocument } from \"../document/index.js\";\nimport { writeEpub, writePlainText } from \"../output/index.js\";\nimport type {\n BookMeta,\n SourceAsset,\n TocFile,\n TocItem,\n} from \"../source/index.js\";\n\nimport {\n readWikgArchiveFormatVersion,\n writeWikgArchive,\n} from \"../wikg/index.js\";\nimport type { ChapterStage } from \"./chapter.js\";\nimport type { WikiGraphSerialEntry } from \"./types.js\";\n\nexport class WikiGraphArchive {\n readonly #document: ReadonlyDocument;\n readonly #documentDirectoryPath: string;\n\n public constructor(\n document: ReadonlyDocument,\n documentDirectoryPath: string,\n ) {\n this.#document = document;\n this.#documentDirectoryPath = documentDirectoryPath;\n }\n\n public async exportEpub(path: string): Promise<void> {\n await writeEpub({\n document: this.#document,\n path,\n });\n }\n\n public async exportText(path: string): Promise<void> {\n await writePlainText({\n document: this.#document,\n path,\n });\n }\n\n public async readCover(): Promise<SourceAsset | undefined> {\n return await this.#document.readCover();\n }\n\n public async readMeta(): Promise<BookMeta | undefined> {\n return await this.#document.readBookMeta();\n }\n\n public async readToc(): Promise<TocFile | undefined> {\n return await this.#document.readToc();\n }\n\n public async readArchiveFormatVersion(): Promise<number> {\n return await readWikgArchiveFormatVersion(this.#documentDirectoryPath);\n }\n\n public async readChapterStage(serialId: number): Promise<ChapterStage> {\n return await this.#document.openSession(async (document) => {\n const toc = await document.readToc();\n\n if (toc === undefined) {\n throw new Error(\"Document TOC is missing\");\n }\n if (!toc.items.some((item) => hasSerialId(item, serialId))) {\n throw new Error(\n `Chapter ${serialId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n\n const summary = await document.readSummary(serialId);\n\n if (summary !== undefined) {\n return \"summarized\";\n }\n\n const serial = await document.serials.getById(serialId);\n\n if (serial?.topologyReady === true) {\n return \"graphed\";\n }\n if (\n (await document.getSerialFragments(serialId).listFragmentIds()).length >\n 0\n ) {\n return \"sourced\";\n }\n\n return \"planned\";\n });\n }\n\n public async listSerials(): Promise<readonly WikiGraphSerialEntry[]> {\n return await this.#document.openSession(async (document) => {\n const toc = await document.readToc();\n\n if (toc === undefined) {\n throw new Error(\"Document TOC is missing\");\n }\n\n return await collectSerialEntries(document, toc.items);\n });\n }\n\n public async readSerialSummary(serialId: number): Promise<string> {\n return await this.#document.openSession(async (document) => {\n const record = await document.serials.getById(serialId);\n\n if (record === undefined) {\n throw new Error(\n `No completed summary exists for id ${serialId}. Use \\`wg wikg://<archive.wikg>/chapter list\\` to discover chapter ids, then \\`wg wikg://<archive.wikg>/chapter/${serialId}/summary get\\` after summary is ready.`,\n );\n }\n\n const summary = await document.readSummary(serialId);\n\n if (summary === undefined) {\n throw new Error(\n `Chapter ${serialId} summary is missing. Run \\`wg wikg://local/job add --input <chapter-uri> --task reading-summary --accept-cost\\` before export, or inspect the archive with \\`wg <archive-uri>/chapter/tree get\\`.`,\n );\n }\n\n return summary;\n });\n }\n\n public async saveAs(path: string): Promise<void> {\n await flushDocument(this.#document);\n await writeWikgArchive(this.#documentDirectoryPath, path);\n }\n}\n\nfunction hasSerialId(item: TocItem, serialId: number): boolean {\n return (\n item.serialId === serialId ||\n item.children.some((child) => hasSerialId(child, serialId))\n );\n}\n\nasync function flushDocument(document: ReadonlyDocument): Promise<void> {\n if (!isFlushableDocument(document)) {\n return;\n }\n\n await document.flush();\n}\n\nfunction isFlushableDocument(\n document: ReadonlyDocument,\n): document is ReadonlyDocument & { flush(): Promise<void> } {\n return \"flush\" in document && typeof document.flush === \"function\";\n}\n\nasync function collectSerialEntries(\n document: ReadonlyDocument,\n items: readonly TocItem[],\n ancestorTitles: readonly string[] = [],\n): Promise<readonly WikiGraphSerialEntry[]> {\n const entries: WikiGraphSerialEntry[] = [];\n\n for (const item of items) {\n const title = item.title?.trim() || `Chapter ${item.serialId ?? \"group\"}`;\n const tocPath = [...ancestorTitles, title];\n\n if (item.serialId !== undefined) {\n const summary = await document.readSummary(item.serialId);\n\n if (summary !== undefined) {\n entries.push({\n fragmentCount: (\n await document.getSerialFragments(item.serialId).listFragmentIds()\n ).length,\n serialId: item.serialId,\n title,\n tocPath,\n });\n }\n }\n\n entries.push(\n ...(await collectSerialEntries(document, item.children, tocPath)),\n );\n }\n\n return entries;\n}\n","import { rm } from \"fs/promises\";\nimport { resolve } from \"path\";\n\nimport { createWikiGraphTempDirectory } from \"../common/wiki-graph-temp.js\";\nimport { BOOK_META_VERSION, TOC_FILE_VERSION } from \"../source/index.js\";\nimport {\n EPUB_SOURCE_ADAPTER,\n MARKDOWN_SOURCE_ADAPTER,\n TXT_SOURCE_ADAPTER,\n type SourceFormat,\n type SourceAdapter,\n} from \"../source/index.js\";\nimport { DirectoryDocument } from \"../document/index.js\";\nimport type { Language } from \"../common/language.js\";\nimport type { WikiGraphScope } from \"../common/llm-scope.js\";\nimport {\n createDigestProgressTracker,\n type WikiGraphProgressCallback,\n} from \"../progress/index.js\";\nimport type { ReaderSegmenter, ReaderTextStream } from \"../reader/index.js\";\nimport type { LLM } from \"../llm/index.js\";\nimport { SerialGeneration, writeSerialSource } from \"../serial.js\";\n\nimport { importSource } from \"./import.js\";\nimport { WikiGraphArchive } from \"./wiki-graph-archive.js\";\nimport type { ChapterStage } from \"./chapter.js\";\n\ninterface DigestSessionOptions {\n readonly documentDirPath?: string;\n readonly extractionPrompt: string;\n readonly llm?: LLM<WikiGraphScope>;\n readonly logDirPath?: string;\n readonly onProgress?: WikiGraphProgressCallback;\n readonly segmenter?: ReaderSegmenter;\n readonly targetStage?: ChapterStage;\n readonly userLanguage?: Language;\n}\n\nexport interface DigestDocumentSessionOptions {\n readonly documentDirPath?: string;\n}\n\nexport interface DigestSourceSessionOptions extends DigestSessionOptions {\n readonly path: string;\n}\n\nexport interface DigestTextStreamSessionOptions extends DigestSessionOptions {\n readonly bookLanguage?: string | null;\n readonly sourceFormat?: Extract<SourceFormat, \"markdown\" | \"txt\">;\n readonly stream: ReaderTextStream;\n readonly title?: string | null;\n}\n\nexport async function digestEpubSession<T>(\n options: DigestSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n): Promise<T> {\n return await digestSourceSession(\n \"digest-epub\",\n EPUB_SOURCE_ADAPTER,\n options,\n operation,\n );\n}\n\nexport async function digestMarkdownSession<T>(\n options: DigestSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n): Promise<T> {\n return await digestSourceSession(\n \"digest-markdown\",\n MARKDOWN_SOURCE_ADAPTER,\n options,\n operation,\n );\n}\n\nexport async function digestTextStreamSession<T>(\n options: DigestTextStreamSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n): Promise<T> {\n const progressTracker = createDigestProgressTracker({\n operation: \"digest-text-stream\",\n ...(options.onProgress === undefined\n ? {}\n : { onProgress: options.onProgress }),\n });\n\n return await withTemporaryDocumentSession(async (document, directoryPath) => {\n await document.openSession(async (openedDocument) => {\n const serialId = await openedDocument.peekNextSerialId();\n const normalizedTitle = normalizeTitle(options.title);\n const targetStage = options.targetStage ?? \"summarized\";\n await progressTracker.markDiscoveryUnavailable();\n\n if (targetStage === \"planned\") {\n await openedDocument.serials.createWithId(serialId);\n } else if (targetStage === \"sourced\") {\n await openedDocument.serials.createWithId(serialId);\n await writeSerialSource(openedDocument, serialId, options.stream, {\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n } else {\n const serialProgressTracker = progressTracker.createSerialTracker({\n id: serialId,\n });\n const generation = new SerialGeneration({\n document: openedDocument,\n llm: requireDigestLLM(options.llm, targetStage),\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n\n if (targetStage === \"graphed\") {\n await openedDocument.serials.createWithId(serialId);\n await writeSerialSource(openedDocument, serialId, options.stream, {\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n });\n await generation.buildTopologyInto(\n serialId,\n {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n },\n serialProgressTracker,\n );\n } else {\n await generation.generateInto(\n serialId,\n options.stream,\n {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n },\n serialProgressTracker,\n );\n }\n }\n\n await openedDocument.writeBookMeta({\n version: BOOK_META_VERSION,\n sourceFormat: options.sourceFormat ?? \"txt\",\n title: normalizeTitle(options.title) ?? null,\n authors: [],\n description: null,\n identifier: null,\n language: options.bookLanguage ?? null,\n publishedAt: null,\n publisher: null,\n });\n await openedDocument.writeToc({\n version: TOC_FILE_VERSION,\n items: [\n {\n serialId,\n children: [],\n ...(normalizedTitle === undefined\n ? {}\n : { title: normalizedTitle }),\n },\n ],\n });\n });\n\n return await operation(new WikiGraphArchive(document, directoryPath));\n }, options.documentDirPath);\n}\n\nexport async function digestTxtSession<T>(\n options: DigestSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n): Promise<T> {\n return await digestSourceSession(\n \"digest-txt\",\n TXT_SOURCE_ADAPTER,\n options,\n operation,\n );\n}\n\nasync function digestSourceSession<T>(\n operationName: \"digest-epub\" | \"digest-markdown\" | \"digest-txt\",\n adapter: SourceAdapter,\n options: DigestSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n): Promise<T> {\n const progressTracker = createDigestProgressTracker({\n operation: operationName,\n ...(options.onProgress === undefined\n ? {}\n : { onProgress: options.onProgress }),\n });\n\n return await withTemporaryDocumentSession(async (document, directoryPath) => {\n await importSource({\n adapter,\n document,\n digestProgressTracker: progressTracker,\n extractionPrompt: options.extractionPrompt,\n ...(options.llm === undefined ? {} : { llm: options.llm }),\n path: options.path,\n ...(options.targetStage === undefined\n ? {}\n : { targetStage: options.targetStage }),\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.segmenter === undefined\n ? {}\n : { segmenter: options.segmenter }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n\n return await operation(new WikiGraphArchive(document, directoryPath));\n }, options.documentDirPath);\n}\n\nasync function withTemporaryDocumentSession<T>(\n operation: (\n document: DirectoryDocument,\n directoryPath: string,\n ) => Promise<T> | T,\n documentDirPath?: string,\n): Promise<T> {\n const directoryPath =\n documentDirPath === undefined\n ? await createWikiGraphTempDirectory(\"archive-open\")\n : resolve(documentDirPath);\n const document = await DirectoryDocument.open(directoryPath);\n\n try {\n return await operation(document, directoryPath);\n } finally {\n await document.release();\n if (documentDirPath === undefined) {\n await rm(directoryPath, { force: true, recursive: true });\n }\n }\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | undefined {\n const normalized = title?.trim();\n\n return normalized === undefined || normalized === \"\" ? undefined : normalized;\n}\n\nfunction requireDigestLLM(\n llm: LLM<WikiGraphScope> | undefined,\n targetStage: ChapterStage,\n): LLM<WikiGraphScope> {\n if (llm === undefined) {\n throw new Error(`LLM is required to digest source to ${targetStage}.`);\n }\n\n return llm;\n}\n","import { WikiGraphScope } from \"../common/llm-scope.js\";\nimport type {\n SamplingProfile,\n SamplingScopeConfig,\n TemperatureSetting,\n} from \"../llm/index.js\";\n\nexport type WikiGraphSamplingConfig = SamplingScopeConfig<WikiGraphScope>;\n\nconst DEFAULT_WIKI_GRAPH_SAMPLING = Object.freeze({\n [WikiGraphScope.EditorCompress]: Object.freeze({\n temperature: 0.7,\n topP: 0.9,\n }),\n [WikiGraphScope.EditorReview]: Object.freeze({\n temperature: [0.3, 0.95] as const,\n topP: [0.4, 0.8] as const,\n }),\n [WikiGraphScope.EditorReviewGuide]: Object.freeze({\n temperature: 0.4,\n topP: 0.6,\n }),\n [WikiGraphScope.ReaderChoice]: Object.freeze({\n temperature: [0.3, 0.95] as const,\n topP: [0.4, 0.8] as const,\n }),\n [WikiGraphScope.ReaderExtraction]: Object.freeze({\n temperature: [0.3, 0.95] as const,\n topP: [0.4, 0.8] as const,\n }),\n} satisfies WikiGraphSamplingConfig);\n\nexport function createDefaultWikiGraphSampling(\n input: {\n readonly temperature?: TemperatureSetting;\n readonly topP?: TemperatureSetting;\n } = {},\n): WikiGraphSamplingConfig {\n return Object.freeze({\n [WikiGraphScope.EditorCompress]: applySamplingOverrides(\n DEFAULT_WIKI_GRAPH_SAMPLING[WikiGraphScope.EditorCompress],\n input,\n ),\n [WikiGraphScope.EditorReview]: applySamplingOverrides(\n DEFAULT_WIKI_GRAPH_SAMPLING[WikiGraphScope.EditorReview],\n input,\n ),\n [WikiGraphScope.EditorReviewGuide]: applySamplingOverrides(\n DEFAULT_WIKI_GRAPH_SAMPLING[WikiGraphScope.EditorReviewGuide],\n input,\n ),\n [WikiGraphScope.ReaderChoice]: applySamplingOverrides(\n DEFAULT_WIKI_GRAPH_SAMPLING[WikiGraphScope.ReaderChoice],\n input,\n ),\n [WikiGraphScope.ReaderExtraction]: applySamplingOverrides(\n DEFAULT_WIKI_GRAPH_SAMPLING[WikiGraphScope.ReaderExtraction],\n input,\n ),\n } satisfies WikiGraphSamplingConfig);\n}\n\nfunction applySamplingOverrides(\n profile: SamplingProfile,\n input: {\n readonly temperature?: TemperatureSetting;\n readonly topP?: TemperatureSetting;\n },\n): SamplingProfile {\n return Object.freeze({\n ...profile,\n ...(input.temperature === undefined\n ? {}\n : { temperature: input.temperature }),\n ...(input.topP === undefined ? {} : { topP: input.topP }),\n });\n}\n","import type { LanguageModel } from \"ai\";\n\nimport { resolveDataDirPath } from \"../common/data-dir.js\";\nimport type { Language } from \"../common/language.js\";\nimport type { WikiGraphScope } from \"../common/llm-scope.js\";\nimport { withLoggingContext } from \"../common/logging.js\";\nimport { LLM } from \"../llm/index.js\";\nimport type {\n DigestProgressEvent,\n SerialDiscoveryItem,\n SerialsDiscoveredEvent,\n SerialProgressEvent,\n WikiGraphProgressCallback,\n WikiGraphProgressEvent,\n WikiGraphProgressEventType,\n WikiGraphOperation,\n} from \"../progress/index.js\";\n\nimport {\n digestEpubSession,\n digestMarkdownSession,\n digestTextStreamSession,\n digestTxtSession,\n type DigestDocumentSessionOptions,\n type DigestSourceSessionOptions,\n type DigestTextStreamSessionOptions,\n} from \"./digest.js\";\nimport { createDefaultWikiGraphSampling } from \"./llm-sampling.js\";\nimport { WikiGraphArchiveFile } from \"../wikg/index.js\";\nimport type { WikiGraphArchive } from \"./wiki-graph-archive.js\";\nimport type { ChapterStage } from \"./chapter.js\";\n\nconst DATA_DIR_PATH = resolveDataDirPath();\n\nconst DEFAULT_EXTRACTION_PROMPT =\n \"Focus on the main storyline and key character developments. Preserve important dialogues and critical plot points. Background descriptions and minor details can be compressed significantly.\";\n\nexport interface WikiGraphLLMOptions {\n readonly cacheDirPath?: string;\n readonly concurrent?: number;\n readonly logDirPath?: string;\n readonly model: LanguageModel;\n readonly retryIntervalSeconds?: number;\n readonly retryTimes?: number;\n readonly stream?: boolean;\n readonly temperature?: number | readonly number[];\n readonly timeout?: number;\n readonly topP?: number | readonly number[];\n}\n\nexport interface WikiGraphOptions {\n readonly debugLogDirPath?: string;\n readonly llm?: LanguageModel | WikiGraphLLMOptions;\n readonly verbose?: boolean;\n}\n\nexport type WikiGraphOpenSessionOptions = DigestDocumentSessionOptions;\n\nexport interface WikiGraphSourceSessionOptions extends DigestDocumentSessionOptions {\n readonly extractionPrompt?: string;\n readonly onProgress?: WikiGraphProgressCallback;\n readonly path: string;\n readonly targetStage?: ChapterStage;\n readonly userLanguage?: Language;\n}\n\nexport interface WikiGraphTextStreamSessionOptions extends DigestDocumentSessionOptions {\n readonly bookLanguage?: string | null;\n readonly extractionPrompt?: string;\n readonly onProgress?: WikiGraphProgressCallback;\n readonly sourceFormat?: \"markdown\" | \"txt\";\n readonly stream: AsyncIterable<string> | Iterable<string>;\n readonly targetStage?: ChapterStage;\n readonly title?: string | null;\n readonly userLanguage?: Language;\n}\n\nexport class WikiGraph {\n readonly #debugLogDirPath: string | undefined;\n readonly #llm: LLM<WikiGraphScope> | undefined;\n readonly #verbose: boolean;\n\n public constructor(options: WikiGraphOptions) {\n this.#debugLogDirPath = options.debugLogDirPath;\n this.#verbose = options.verbose ?? false;\n if (options.llm === undefined) {\n this.#llm = undefined;\n return;\n }\n const llmOptions = normalizeLLMOptions(options.llm);\n\n this.#llm = new LLM<WikiGraphScope>({\n dataDirPath: DATA_DIR_PATH,\n sampling: createDefaultWikiGraphSampling({\n ...(llmOptions.temperature === undefined\n ? {}\n : { temperature: llmOptions.temperature }),\n ...(llmOptions.topP === undefined ? {} : { topP: llmOptions.topP }),\n }),\n ...llmOptions,\n });\n }\n\n public async digestEpubSession<T>(\n options: WikiGraphSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n ): Promise<T> {\n return await this.#withLogging(\n \"digest-epub\",\n async () =>\n await digestEpubSession(this.#createSourceOptions(options), operation),\n );\n }\n\n public async digestMarkdownSession<T>(\n options: WikiGraphSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n ): Promise<T> {\n return await this.#withLogging(\n \"digest-markdown\",\n async () =>\n await digestMarkdownSession(\n this.#createSourceOptions(options),\n operation,\n ),\n );\n }\n\n public async digestTextStreamSession<T>(\n options: WikiGraphTextStreamSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n ): Promise<T> {\n return await this.#withLogging(\n \"digest-text-stream\",\n async () =>\n await digestTextStreamSession(\n {\n extractionPrompt: resolveExtractionPrompt(options.extractionPrompt),\n ...optionalLLM(this.#resolveStageLLM(options.targetStage)),\n ...(options.onProgress === undefined\n ? {}\n : { onProgress: options.onProgress }),\n stream: options.stream,\n ...(this.#debugLogDirPath === undefined\n ? {}\n : { logDirPath: this.#debugLogDirPath }),\n ...(options.bookLanguage === undefined\n ? {}\n : { bookLanguage: options.bookLanguage }),\n ...(options.documentDirPath === undefined\n ? {}\n : { documentDirPath: options.documentDirPath }),\n ...(options.sourceFormat === undefined\n ? {}\n : { sourceFormat: options.sourceFormat }),\n ...(options.targetStage === undefined\n ? {}\n : { targetStage: options.targetStage }),\n ...(options.title === undefined ? {} : { title: options.title }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n } satisfies DigestTextStreamSessionOptions,\n operation,\n ),\n );\n }\n\n public async digestTxtSession<T>(\n options: WikiGraphSourceSessionOptions,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n ): Promise<T> {\n return await this.#withLogging(\n \"digest-txt\",\n async () =>\n await digestTxtSession(this.#createSourceOptions(options), operation),\n );\n }\n\n public async openSession<T>(\n path: string,\n operation: (digest: WikiGraphArchive) => Promise<T> | T,\n options: WikiGraphOpenSessionOptions = {},\n ): Promise<T> {\n return await this.#withLogging(\n \"open-wikg\",\n async () =>\n await new WikiGraphArchiveFile(path).read(operation, {\n ...(options.documentDirPath === undefined\n ? {}\n : { documentDirPath: options.documentDirPath }),\n }),\n );\n }\n\n #createSourceOptions(\n options: WikiGraphSourceSessionOptions,\n ): DigestSourceSessionOptions {\n return {\n extractionPrompt: resolveExtractionPrompt(options.extractionPrompt),\n ...optionalLLM(this.#resolveStageLLM(options.targetStage)),\n ...(options.onProgress === undefined\n ? {}\n : { onProgress: options.onProgress }),\n path: options.path,\n ...(this.#debugLogDirPath === undefined\n ? {}\n : { logDirPath: this.#debugLogDirPath }),\n ...(options.documentDirPath === undefined\n ? {}\n : { documentDirPath: options.documentDirPath }),\n ...(options.targetStage === undefined\n ? {}\n : { targetStage: options.targetStage }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n };\n }\n\n #requireLLM(): LLM<WikiGraphScope> {\n if (this.#llm === undefined) {\n throw new Error(\n \"LLM is required for digest operations. Configure `llm` when constructing WikiGraph.\",\n );\n }\n\n return this.#llm;\n }\n\n #resolveStageLLM(\n targetStage: ChapterStage | undefined,\n ): LLM<WikiGraphScope> | undefined {\n if (targetStage === \"planned\" || targetStage === \"sourced\") {\n return undefined;\n }\n\n return this.#requireLLM();\n }\n\n async #withLogging<T>(operation: string, task: () => Promise<T>): Promise<T> {\n return await withLoggingContext(\n {\n operation,\n ...(this.#debugLogDirPath === undefined\n ? {}\n : { logDirPath: this.#debugLogDirPath }),\n ...(this.#verbose ? { verbose: true } : {}),\n },\n task,\n );\n }\n}\n\nexport type {\n DigestProgressEvent,\n SerialDiscoveryItem,\n SerialsDiscoveredEvent,\n SerialProgressEvent,\n WikiGraphProgressCallback,\n WikiGraphProgressEvent,\n WikiGraphProgressEventType,\n WikiGraphOperation,\n};\n\nfunction optionalLLM(\n llm: LLM<WikiGraphScope> | undefined,\n): { readonly llm: LLM<WikiGraphScope> } | Record<string, never> {\n return llm === undefined ? {} : { llm };\n}\n\nfunction normalizeLLMOptions(\n llm: NonNullable<WikiGraphOptions[\"llm\"]>,\n): WikiGraphLLMOptions {\n if (isWikiGraphLLMOptions(llm)) {\n return llm;\n }\n\n return { model: llm };\n}\n\nfunction isWikiGraphLLMOptions(\n llm: NonNullable<WikiGraphOptions[\"llm\"]>,\n): llm is WikiGraphLLMOptions {\n return typeof llm === \"object\" && llm !== null && \"model\" in llm;\n}\n\nfunction resolveExtractionPrompt(prompt: string | undefined): string {\n const normalizedPrompt = prompt?.trim();\n\n return normalizedPrompt === undefined || normalizedPrompt === \"\"\n ? DEFAULT_EXTRACTION_PROMPT\n : normalizedPrompt;\n}\n","import { createHash, randomUUID } from \"crypto\";\nimport { appendFile, mkdir, readFile, rm } from \"fs/promises\";\nimport { join, resolve } from \"path\";\n\nimport { resolveWikiGraphJobsDirectoryPath } from \"../common/wiki-graph-dir.js\";\nimport { openSharedStateDatabase } from \"../document/index.js\";\nimport type { Database } from \"../document/index.js\";\nimport { readPathSize, removeDisposableChildDirectories } from \"../gc/files.js\";\nimport type { GcContext, GcJobResult } from \"../gc/index.js\";\n\nexport const BUILD_JOB_STATES = [\n \"queued\",\n \"running\",\n \"canceling\",\n \"paused\",\n \"succeeded\",\n \"failed\",\n \"canceled\",\n] as const;\n\nexport type BuildJobState = (typeof BUILD_JOB_STATES)[number];\nexport type BuildJobTarget =\n | \"knowledge-graph\"\n | \"reading-graph\"\n | \"reading-summary\";\nexport type BuildJobProgressPhase =\n | \"committing\"\n | \"enrichment\"\n | \"grounding\"\n | \"matching\"\n | \"narrowing\"\n | \"relation-discovery\"\n | \"screening\";\nexport type BuildJobProgressUnit =\n | \"candidate\"\n | \"char\"\n | \"item\"\n | \"page\"\n | \"qid\"\n | \"sentence\"\n | \"window\"\n | \"record\";\n\nexport interface BuildJobProgressCounter {\n readonly done: number;\n readonly name: string;\n readonly total: number;\n readonly unit: BuildJobProgressUnit | \"word\";\n}\n\nexport interface BuildJobTokenUsage {\n readonly cacheReadTokens?: number;\n readonly inputTokens?: number;\n readonly outputTokens?: number;\n}\n\nexport interface BuildJob {\n readonly archiveKey: string;\n readonly archivePath: string;\n readonly cachePath: string;\n readonly chapterId: number;\n readonly createdAt: number;\n readonly currentStep?: BuildJobTarget;\n readonly errorJSON?: string;\n readonly eventsPath: string;\n readonly finishedAt?: number;\n readonly jobId: string;\n readonly inputRevision?: number;\n readonly logPath: string;\n readonly llmJSON?: string;\n readonly ownerId?: string;\n readonly ownerPid?: number;\n readonly prompt?: string;\n readonly queueRank: number;\n readonly state: BuildJobState;\n readonly readingSummaryStartedAt?: number;\n readonly target: BuildJobTarget;\n readonly updatedAt: number;\n readonly workspacePath: string;\n}\n\nexport type BuildJobEvent =\n | {\n readonly at: number;\n readonly jobId: string;\n readonly seq: number;\n readonly state: BuildJobState;\n readonly type: \"created\" | \"paused\" | \"resumed\" | \"boosted\" | \"canceling\";\n }\n | {\n readonly at: number;\n readonly jobId: string;\n readonly seq: number;\n readonly from: BuildJobTarget;\n readonly to: BuildJobTarget;\n readonly type: \"target_changed\";\n }\n | {\n readonly at: number;\n readonly jobId: string;\n readonly seq: number;\n readonly state: \"running\";\n readonly type: \"started\";\n }\n | {\n readonly at: number;\n readonly jobId: string;\n readonly seq: number;\n readonly state: \"queued\";\n readonly type: \"requeued\";\n }\n | {\n readonly at: number;\n readonly jobId: string;\n readonly seq: number;\n readonly step: BuildJobTarget;\n readonly type: \"step_started\" | \"step_completed\";\n }\n | {\n readonly at: number;\n readonly counters: readonly BuildJobProgressCounter[];\n readonly jobId: string;\n readonly phase?: BuildJobProgressPhase;\n readonly seq: number;\n readonly step?: BuildJobTarget;\n readonly tokens?: BuildJobTokenUsage;\n readonly type: \"status_snapshot\";\n }\n | {\n readonly at: number;\n readonly error?: unknown;\n readonly jobId: string;\n readonly seq: number;\n readonly state: \"failed\" | \"canceled\" | \"succeeded\";\n readonly type: \"failed\" | \"canceled\" | \"succeeded\";\n };\n\nexport interface AddBuildJobOptions {\n readonly archivePath: string;\n readonly boost?: boolean;\n readonly chapterId: number;\n readonly jobId?: string;\n readonly llmJSON?: string;\n readonly prompt?: string;\n readonly target: BuildJobTarget;\n}\n\nexport interface BuildJobListOptions {\n readonly activeOnly?: boolean;\n readonly all?: boolean;\n readonly archivePath?: string;\n}\n\nexport interface BuildJobWorkerOptions {\n readonly concurrency: number;\n readonly executeJob: (\n job: BuildJob,\n reporter: BuildJobProgressReporter,\n context: BuildJobExecutionContext,\n ) => Promise<void>;\n readonly idleTimeoutMs?: number;\n}\n\nexport interface BuildJobExecutionContext {\n readonly signal: AbortSignal;\n}\n\nexport interface BuildJobProgressReporter {\n addOutputCharacters(characters: number): Promise<void>;\n addTokenUsage(usage: BuildJobTokenUsage): Promise<void>;\n setTotals(input: {\n readonly totalGraphWords?: number;\n readonly totalReadingSummaryWords?: number;\n }): Promise<void>;\n stepCompleted(step: BuildJobTarget): Promise<void>;\n stepStarted(step: BuildJobTarget): Promise<void>;\n updateWords(input: {\n readonly graphWords?: number;\n readonly readingSummaryWords?: number;\n }): Promise<void>;\n updatePhase(input: {\n readonly done: number;\n readonly force?: boolean;\n readonly phase: BuildJobProgressPhase;\n readonly phaseDetail?: string;\n readonly total: number;\n readonly unit: BuildJobProgressUnit;\n }): Promise<void>;\n throwIfStopped(): Promise<void>;\n}\n\nexport type BuildJobConflictScope =\n | {\n readonly kind: \"archive\";\n }\n | {\n readonly chapterIds: readonly number[];\n readonly kind: \"chapter\";\n };\n\nconst BUILD_QUEUE_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS build_jobs (\n job_id TEXT PRIMARY KEY,\n archive_key TEXT NOT NULL,\n archive_path TEXT NOT NULL,\n chapter_id INTEGER NOT NULL,\n target TEXT NOT NULL,\n current_step TEXT,\n state TEXT NOT NULL,\n queue_rank INTEGER NOT NULL,\n workspace_path TEXT NOT NULL,\n cache_path TEXT NOT NULL,\n log_path TEXT NOT NULL,\n events_path TEXT NOT NULL,\n input_revision INTEGER,\n llm_json TEXT,\n prompt TEXT,\n owner_id TEXT,\n owner_pid INTEGER,\n reading_summary_started_at INTEGER,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n finished_at INTEGER,\n error_json TEXT\n);\n\nDROP INDEX IF EXISTS idx_build_jobs_active_chapter;\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_build_jobs_active_knowledge_chapter\nON build_jobs(archive_key, chapter_id)\nWHERE target = 'knowledge-graph'\n AND state IN ('queued', 'running', 'canceling', 'paused');\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_build_jobs_active_reading_chapter\nON build_jobs(archive_key, chapter_id)\nWHERE target IN ('reading-graph', 'reading-summary')\n AND state IN ('queued', 'running', 'canceling', 'paused');\n\nCREATE INDEX IF NOT EXISTS idx_build_jobs_queue\nON build_jobs(state, queue_rank, updated_at);\n\nCREATE TABLE IF NOT EXISTS build_worker_lease (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n owner_id TEXT,\n owner_pid INTEGER,\n heartbeat_at INTEGER\n);\n\nINSERT OR IGNORE INTO build_worker_lease (id)\nVALUES (1);\n`;\n\nconst ACTIVE_JOB_STATES = new Set<BuildJobState>([\n \"queued\",\n \"running\",\n \"canceling\",\n \"paused\",\n]);\nconst WORKER_HEARTBEAT_INTERVAL_MS = 5_000;\n\nexport async function addBuildJob(\n options: AddBuildJobOptions,\n): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await state.transaction(async () => {\n const archivePath = resolve(options.archivePath);\n const archiveKey = createArchiveKey(archivePath);\n const now = Date.now();\n const existing = await findActiveBuildJobInLane(state, {\n archiveKey,\n chapterId: options.chapterId,\n target: options.target,\n });\n\n if (existing !== undefined) {\n return await mergeActiveBuildJob(state, existing, options, now);\n }\n\n const jobId = options.jobId ?? randomUUID();\n const workspacePath = await createJobWorkspacePath(jobId);\n const cachePath = await createJobCachePath(jobId);\n const logPath = await createJobLogPath(jobId);\n const eventsPath = await createJobEventsPath(jobId);\n const queueRank =\n options.boost === true\n ? (await readMinQueueRank(state)) - 1\n : (await readMaxQueueRank(state)) + 1;\n\n await state.run(\n `\nINSERT INTO build_jobs (\n job_id, archive_key, archive_path, chapter_id, target, state, queue_rank,\n workspace_path, cache_path, log_path, events_path, llm_json, prompt,\n created_at, updated_at\n) VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`,\n [\n jobId,\n archiveKey,\n archivePath,\n options.chapterId,\n options.target,\n queueRank,\n workspacePath,\n cachePath,\n logPath,\n eventsPath,\n options.llmJSON ?? null,\n options.prompt ?? null,\n now,\n now,\n ],\n );\n\n const job = await requireBuildJobById(state, jobId);\n\n await appendBuildJobEvent(job, {\n at: now,\n jobId,\n seq: 0,\n state: \"queued\",\n type: \"created\",\n });\n return job;\n });\n } finally {\n await state.close();\n }\n}\n\nexport async function recordBuildJobInputRevision(input: {\n readonly currentRevision: number;\n readonly jobId: string;\n readonly ownerId: string;\n}): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await state.transaction(async () => {\n await state.run(\n `\nUPDATE build_jobs\nSET input_revision = ?,\n updated_at = ?\nWHERE job_id = ?\n AND owner_id = ?\n AND state = 'running'\n`,\n [input.currentRevision, Date.now(), input.jobId, input.ownerId],\n );\n });\n\n return await requireBuildJobById(state, input.jobId);\n } finally {\n await state.close();\n }\n}\n\nexport async function assertBuildJobInputRevision(input: {\n readonly currentRevision: number;\n readonly jobId: string;\n readonly ownerId: string;\n}): Promise<void> {\n const job = await getBuildJob(input.jobId);\n\n if (job.state !== \"running\" || job.ownerId !== input.ownerId) {\n throw new BuildJobStoppedError(\n `Job ${input.jobId} is ${job.state}. Stop current worker execution.`,\n );\n }\n if (job.inputRevision === undefined) {\n throw new Error(\n `Job ${input.jobId} has no recorded chapter input revision. Requeue the job before committing build output.`,\n );\n }\n if (job.inputRevision === input.currentRevision) {\n return;\n }\n\n throw new Error(\n `Chapter ${job.chapterId} changed while job ${job.jobId} was running. Requeue the job before committing build output.`,\n );\n}\n\nexport async function listBuildJobs(\n options: BuildJobListOptions = {},\n): Promise<BuildJob[]> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n const params: Array<number | string> = [];\n const filters: string[] = [];\n\n if (options.archivePath !== undefined) {\n params.push(createArchiveKey(resolve(options.archivePath)));\n filters.push(\"archive_key = ?\");\n }\n if (options.activeOnly === true || options.all !== true) {\n filters.push(\"state IN ('queued', 'running', 'canceling', 'paused')\");\n }\n\n return await state.queryAll(\n `\nSELECT *\nFROM build_jobs\n${filters.length === 0 ? \"\" : `WHERE ${filters.join(\" AND \")}`}\nORDER BY\n CASE state\n WHEN 'running' THEN 0\n WHEN 'canceling' THEN 1\n WHEN 'queued' THEN 2\n WHEN 'paused' THEN 3\n ELSE 4\n END,\n queue_rank ASC,\n updated_at DESC\n`,\n params,\n mapBuildJob,\n );\n } finally {\n await state.close();\n }\n}\n\nasync function findActiveBuildJobInLane(\n state: Database,\n input: {\n readonly archiveKey: string;\n readonly chapterId: number;\n readonly target: BuildJobTarget;\n },\n): Promise<BuildJob | undefined> {\n const laneFilter =\n input.target === \"knowledge-graph\"\n ? \"target = 'knowledge-graph'\"\n : \"target IN ('reading-graph', 'reading-summary')\";\n\n return await state.queryOne(\n `\nSELECT *\nFROM build_jobs\nWHERE archive_key = ?\n AND chapter_id = ?\n AND state IN ('queued', 'running', 'canceling', 'paused')\n AND ${laneFilter}\nORDER BY updated_at DESC\nLIMIT 1\n`,\n [input.archiveKey, input.chapterId],\n mapBuildJob,\n );\n}\n\nasync function mergeActiveBuildJob(\n state: Database,\n job: BuildJob,\n options: AddBuildJobOptions,\n now: number,\n): Promise<BuildJob> {\n const nextTarget =\n job.target === \"reading-graph\" && options.target === \"reading-summary\"\n ? \"reading-summary\"\n : job.target;\n const nextQueueRank =\n options.boost === true && job.state === \"queued\"\n ? (await readMinQueueRank(state)) - 1\n : job.queueRank;\n\n if (nextTarget === job.target && nextQueueRank === job.queueRank) {\n return job;\n }\n\n await state.run(\n `\nUPDATE build_jobs\nSET target = ?, queue_rank = ?, updated_at = ?\nWHERE job_id = ?\n`,\n [nextTarget, nextQueueRank, now, job.jobId],\n );\n\n const updated = await requireBuildJobById(state, job.jobId);\n\n if (nextTarget !== job.target) {\n await appendBuildJobEvent(updated, {\n at: now,\n from: job.target,\n jobId: job.jobId,\n seq: 0,\n to: nextTarget,\n type: \"target_changed\",\n });\n }\n if (nextQueueRank !== job.queueRank) {\n await appendBuildJobEvent(updated, {\n at: now,\n jobId: job.jobId,\n seq: 0,\n state: updated.state,\n type: \"boosted\",\n });\n }\n\n return updated;\n}\n\nexport async function getBuildJob(jobId: string): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await requireBuildJobById(state, jobId);\n } finally {\n await state.close();\n }\n}\n\nasync function readBuildJobForStopCheck(jobId: string): Promise<BuildJob> {\n const state = await openReadonlyBuildQueueDatabase();\n\n try {\n return await requireBuildJobById(state, jobId);\n } finally {\n await state.close();\n }\n}\n\nexport async function resolveBuildJobId(jobIdPrefix: string): Promise<string> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await resolveBuildJobIdInState(state, jobIdPrefix);\n } finally {\n await state.close();\n }\n}\n\nexport async function pauseBuildJob(jobId: string): Promise<BuildJob> {\n return await updateBuildJobState(jobId, \"paused\", \"paused\", {\n allowedStates: [\"queued\", \"running\"],\n });\n}\n\nexport async function resumeBuildJob(jobId: string): Promise<BuildJob> {\n return await updateBuildJobState(jobId, \"queued\", \"resumed\", {\n allowedStates: [\"paused\"],\n clearOwner: true,\n });\n}\n\nexport async function cancelBuildJob(jobId: string): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (job.state === \"running\") {\n return await markBuildJobCanceling(state, job);\n }\n if (job.state === \"queued\" || job.state === \"paused\") {\n return await markBuildJobCanceled(state, job);\n }\n\n throw new Error(`Cannot cancel ${job.state} job ${jobId}.`);\n });\n } finally {\n await state.close();\n }\n}\n\nexport async function boostBuildJob(jobId: string): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (job.state !== \"queued\") {\n throw new Error(\n `Only queued jobs can be boosted. Job ${jobId} is ${job.state}.`,\n );\n }\n\n const now = Date.now();\n await state.run(\n `\nUPDATE build_jobs\nSET queue_rank = ?, updated_at = ?\nWHERE job_id = ?\n`,\n [(await readMinQueueRank(state)) - 1, now, jobId],\n );\n\n const updated = await requireBuildJobById(state, jobId);\n await appendBuildJobEvent(updated, {\n at: now,\n jobId,\n seq: 0,\n state: \"queued\",\n type: \"boosted\",\n });\n return updated;\n });\n } finally {\n await state.close();\n }\n}\n\nexport async function updateBuildJobTarget(\n jobId: string,\n target: BuildJobTarget,\n): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (!ACTIVE_JOB_STATES.has(job.state)) {\n throw new Error(`Cannot change target for ${job.state} job ${jobId}.`);\n }\n if (job.target === target) {\n return job;\n }\n const existing = await findActiveBuildJobInLane(state, {\n archiveKey: job.archiveKey,\n chapterId: job.chapterId,\n target,\n });\n\n if (existing !== undefined && existing.jobId !== job.jobId) {\n throw new Error(\n `Chapter ${job.chapterId} already has active ${formatBuildJobLane(target)} job ${existing.jobId}.`,\n );\n }\n if (job.target === \"reading-summary\" && target === \"reading-graph\") {\n if (\n job.readingSummaryStartedAt !== undefined ||\n job.currentStep === \"reading-summary\"\n ) {\n throw new Error(\n `Cannot downgrade job ${jobId} after summary has started. Cancel it explicitly instead.`,\n );\n }\n }\n\n const now = Date.now();\n await state.run(\n `\nUPDATE build_jobs\nSET target = ?, updated_at = ?\nWHERE job_id = ?\n`,\n [target, now, jobId],\n );\n\n const updated = await requireBuildJobById(state, jobId);\n await appendBuildJobEvent(updated, {\n at: now,\n from: job.target,\n jobId,\n seq: 0,\n to: target,\n type: \"target_changed\",\n });\n return updated;\n });\n } finally {\n await state.close();\n }\n}\n\nexport async function assertNoActiveBuildJobs(input: {\n readonly archivePath: string;\n readonly chapterIds: readonly number[];\n readonly operation: string;\n readonly requiresTarget?: BuildJobTarget;\n}): Promise<void> {\n await assertNoActiveBuildJobConflicts({\n archivePath: input.archivePath,\n operation: input.operation,\n ...(input.requiresTarget === undefined\n ? {}\n : { requiresTarget: input.requiresTarget }),\n scope: { chapterIds: input.chapterIds, kind: \"chapter\" },\n });\n}\n\nexport async function assertNoActiveBuildJobConflicts(input: {\n readonly archivePath: string;\n readonly operation: string;\n readonly requiresTarget?: BuildJobTarget;\n readonly scope: BuildJobConflictScope;\n}): Promise<void> {\n if (input.scope.kind === \"chapter\" && input.scope.chapterIds.length === 0) {\n return;\n }\n\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n const archiveKey = createArchiveKey(resolve(input.archivePath));\n const targetFilter =\n input.requiresTarget === undefined ? \"\" : \"AND target = ?\";\n const params: Array<number | string> = [archiveKey];\n let scopeFilter = \"\";\n\n if (input.scope.kind === \"chapter\") {\n const placeholders = input.scope.chapterIds.map(() => \"?\").join(\", \");\n\n scopeFilter = `AND chapter_id IN (${placeholders})`;\n params.push(...input.scope.chapterIds);\n }\n if (input.requiresTarget !== undefined) {\n params.push(input.requiresTarget);\n }\n\n const job = await state.queryOne(\n `\nSELECT *\nFROM build_jobs\nWHERE archive_key = ?\n ${scopeFilter}\n AND state IN ('queued', 'running', 'canceling', 'paused')\n ${targetFilter}\nORDER BY updated_at DESC\nLIMIT 1\n`,\n params,\n mapBuildJob,\n );\n\n if (job === undefined) {\n return;\n }\n\n throw new Error(\n `Chapter ${job.chapterId} has active ${job.target} job ${job.jobId}. ${input.operation} is blocked until the job is paused/canceled or completed.`,\n );\n } finally {\n await state.close();\n }\n}\n\nexport async function runBuildJobWorker(\n options: BuildJobWorkerOptions,\n): Promise<void> {\n const state = await openBuildQueueDatabase();\n const ownerId = `${process.pid}-${randomUUID()}`;\n const concurrency = Math.max(1, options.concurrency);\n const idleTimeoutMs = options.idleTimeoutMs ?? 10_000;\n let stopping = false;\n let busySlotCount = 0;\n let idleSince = Date.now();\n\n const stop = (_signal: NodeJS.Signals): void => {\n stopping = true;\n };\n\n process.once(\"SIGINT\", stop);\n process.once(\"SIGTERM\", stop);\n\n const heartbeat = setInterval(() => {\n void heartbeatBuildWorker(ownerId).catch(() => undefined);\n }, WORKER_HEARTBEAT_INTERVAL_MS);\n\n try {\n const acquired = await acquireBuildWorkerLease(state, ownerId);\n\n if (!acquired) {\n return;\n }\n\n const runSlot = async (): Promise<void> => {\n while (!stopping) {\n await heartbeatBuildWorker(ownerId, state);\n await recoverStaleBuildJobs(state);\n\n busySlotCount += 1;\n let job: BuildJob | undefined;\n\n try {\n job = await claimQueuedBuildJob(state, ownerId);\n } finally {\n if (job === undefined) {\n busySlotCount -= 1;\n }\n }\n\n if (job === undefined) {\n if (busySlotCount === 0 && Date.now() - idleSince >= idleTimeoutMs) {\n break;\n }\n await delay(500);\n continue;\n }\n\n idleSince = Date.now();\n\n try {\n await executeClaimedBuildJob(job, ownerId, options);\n } finally {\n busySlotCount -= 1;\n if (busySlotCount === 0) {\n idleSince = Date.now();\n }\n }\n }\n };\n\n await Promise.all(\n Array.from({ length: concurrency }, async () => await runSlot()),\n );\n } finally {\n clearInterval(heartbeat);\n process.removeListener(\"SIGINT\", stop);\n process.removeListener(\"SIGTERM\", stop);\n await releaseBuildWorkerLease(state, ownerId);\n await state.close();\n }\n}\n\nasync function executeClaimedBuildJob(\n job: BuildJob,\n ownerId: string,\n options: BuildJobWorkerOptions,\n): Promise<void> {\n const reporter = new BuildJobProgressAccumulator(job, ownerId);\n const abortController = new AbortController();\n const stopWatcher = setInterval(() => {\n void abortJobWhenStopped(job, ownerId, abortController).catch(\n () => undefined,\n );\n }, 500);\n\n try {\n await appendBuildJobEvent(job, {\n at: Date.now(),\n jobId: job.jobId,\n seq: 0,\n state: \"running\",\n type: \"started\",\n });\n await options.executeJob(job, reporter, {\n signal: abortController.signal,\n });\n await reporter.throwIfStopped();\n await markBuildJobSucceeded(job.jobId, ownerId);\n } catch (error) {\n if (\n error instanceof BuildJobStoppedError ||\n abortController.signal.aborted\n ) {\n await markBuildJobStopped(job.jobId, ownerId);\n return;\n }\n\n await markBuildJobFailed(job.jobId, ownerId, error);\n } finally {\n clearInterval(stopWatcher);\n }\n}\n\nasync function abortJobWhenStopped(\n job: BuildJob,\n ownerId: string,\n abortController: AbortController,\n): Promise<void> {\n if (abortController.signal.aborted) {\n return;\n }\n\n const latest = await readBuildJobForStopCheck(job.jobId);\n\n if (latest.state === \"running\" && latest.ownerId === ownerId) {\n return;\n }\n\n abortController.abort(\n new BuildJobStoppedError(\n `Job ${job.jobId} is ${latest.state}. Stop current worker execution.`,\n ),\n );\n}\n\nexport async function readBuildJobEvents(\n job: Pick<BuildJob, \"eventsPath\">,\n): Promise<BuildJobEvent[]> {\n let content: string;\n\n try {\n content = await readFile(job.eventsPath, \"utf8\");\n } catch {\n return [];\n }\n\n return content\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map((line) => JSON.parse(line) as BuildJobEvent);\n}\n\nexport async function cleanBuildJobs(\n options: {\n readonly olderThanMs?: number;\n readonly states?: readonly BuildJobState[];\n } = {},\n): Promise<number> {\n const state = await openBuildQueueDatabase();\n const cutoff = Date.now() - (options.olderThanMs ?? 7 * 24 * 60 * 60 * 1000);\n const states = options.states ?? [\"succeeded\", \"failed\", \"canceled\"];\n\n try {\n const placeholders = states.map(() => \"?\").join(\", \");\n const jobs = await state.queryAll(\n `\nSELECT *\nFROM build_jobs\nWHERE state IN (${placeholders})\n AND updated_at <= ?\n`,\n [...states, cutoff],\n mapBuildJob,\n );\n\n for (const job of jobs) {\n await rm(job.workspacePath, { force: true, recursive: true });\n await rm(job.cachePath, { force: true, recursive: true });\n await rm(job.logPath, { force: true, recursive: true });\n await rm(job.eventsPath, { force: true });\n await state.run(\"DELETE FROM build_jobs WHERE job_id = ?\", [job.jobId]);\n }\n\n return jobs.length;\n } finally {\n await state.close();\n }\n}\n\nexport async function runBuildQueueGc(\n context: GcContext,\n): Promise<GcJobResult> {\n const state = await openBuildQueueDatabase();\n const cutoff = context.force\n ? context.now\n : context.now - 7 * 24 * 60 * 60 * 1000;\n\n try {\n const scanned = await state.queryOne(\n \"SELECT COUNT(*) AS count FROM build_jobs\",\n undefined,\n (row) => getNumber(row, \"count\"),\n );\n const jobs = await state.queryAll(\n `\nSELECT *\nFROM build_jobs\nWHERE state IN ('succeeded', 'failed', 'canceled')\n AND updated_at <= ?\n`,\n [cutoff],\n mapBuildJob,\n );\n const childDirectories = context.dryRun\n ? { freedBytes: 0, removed: 0, scanned: 0 }\n : await removeDisposableChildDirectories(getBuildJobWorkspaceRootPath());\n let freedBytes = 0;\n\n for (const job of jobs) {\n freedBytes += await readPathSize(job.workspacePath);\n freedBytes += await readPathSize(job.cachePath);\n freedBytes += await readPathSize(job.logPath);\n freedBytes += await readPathSize(job.eventsPath);\n if (!context.dryRun) {\n await rm(job.workspacePath, { force: true, recursive: true });\n await rm(job.cachePath, { force: true, recursive: true });\n await rm(job.logPath, { force: true, recursive: true });\n await rm(job.eventsPath, { force: true });\n await state.run(\"DELETE FROM build_jobs WHERE job_id = ?\", [job.jobId]);\n }\n }\n\n return {\n freedBytes: freedBytes + childDirectories.freedBytes,\n removed: jobs.length + childDirectories.removed,\n scanned: (scanned ?? 0) + childDirectories.scanned,\n };\n } finally {\n await state.close();\n }\n}\n\nasync function updateBuildJobState(\n jobId: string,\n stateName: BuildJobState,\n eventType: Extract<BuildJobEvent[\"type\"], \"paused\" | \"resumed\">,\n options: {\n readonly allowedStates: readonly BuildJobState[];\n readonly clearOwner?: boolean;\n readonly finished?: boolean;\n },\n): Promise<BuildJob> {\n const state = await openBuildQueueDatabase();\n\n try {\n await recoverStaleBuildJobs(state);\n return await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (!options.allowedStates.includes(job.state)) {\n throw new Error(`Cannot ${eventType} ${job.state} job ${jobId}.`);\n }\n\n const now = Date.now();\n await state.run(\n `\nUPDATE build_jobs\nSET state = ?, updated_at = ?, finished_at = ?,\n owner_id = CASE WHEN ? = 1 THEN NULL ELSE owner_id END,\n owner_pid = CASE WHEN ? = 1 THEN NULL ELSE owner_pid END\nWHERE job_id = ?\n`,\n [\n stateName,\n now,\n options.finished === true ? now : null,\n options.clearOwner === true || options.finished === true ? 1 : 0,\n options.clearOwner === true || options.finished === true ? 1 : 0,\n jobId,\n ],\n );\n\n const updated = await requireBuildJobById(state, jobId);\n await appendBuildJobEvent(updated, {\n at: now,\n jobId,\n seq: 0,\n state: stateName as \"queued\" | \"paused\",\n type: eventType,\n } as BuildJobEvent);\n return updated;\n });\n } finally {\n await state.close();\n }\n}\n\nasync function markBuildJobCanceling(\n state: Database,\n job: BuildJob,\n): Promise<BuildJob> {\n const now = Date.now();\n\n await state.run(\n `\nUPDATE build_jobs\nSET state = 'canceling', updated_at = ?\nWHERE job_id = ? AND state = 'running'\n`,\n [now, job.jobId],\n );\n\n const updated = await requireBuildJobById(state, job.jobId);\n await appendBuildJobEvent(updated, {\n at: now,\n jobId: job.jobId,\n seq: 0,\n state: \"canceling\",\n type: \"canceling\",\n });\n return updated;\n}\n\nasync function markBuildJobCanceled(\n state: Database,\n job: BuildJob,\n): Promise<BuildJob> {\n const now = Date.now();\n\n await state.run(\n `\nUPDATE build_jobs\nSET state = 'canceled', owner_id = NULL, owner_pid = NULL,\n current_step = NULL, finished_at = ?, updated_at = ?\nWHERE job_id = ?\n`,\n [now, now, job.jobId],\n );\n\n const updated = await requireBuildJobById(state, job.jobId);\n await appendBuildJobEvent(updated, {\n at: now,\n jobId: job.jobId,\n seq: 0,\n state: \"canceled\",\n type: \"canceled\",\n });\n return updated;\n}\n\nasync function markBuildJobFailedInState(\n state: Database,\n job: BuildJob,\n error: unknown,\n): Promise<void> {\n const now = Date.now();\n const formattedError = formatErrorEvent(error);\n const errorJSON = JSON.stringify(formattedError);\n\n await state.run(\n `\nUPDATE build_jobs\nSET state = 'failed', owner_id = NULL, owner_pid = NULL,\n current_step = NULL, finished_at = ?, updated_at = ?, error_json = ?\nWHERE job_id = ?\n`,\n [now, now, errorJSON, job.jobId],\n );\n await appendBuildJobEvent(job, {\n at: now,\n error: formattedError,\n jobId: job.jobId,\n seq: 0,\n state: \"failed\",\n type: \"failed\",\n });\n}\n\nasync function markBuildJobSucceeded(\n jobId: string,\n ownerId: string,\n): Promise<void> {\n const state = await openBuildQueueDatabase();\n\n try {\n await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (job.ownerId !== ownerId || job.state !== \"running\") {\n return;\n }\n\n const now = Date.now();\n await state.run(\n `\nUPDATE build_jobs\nSET state = 'succeeded', owner_id = NULL, owner_pid = NULL,\n current_step = NULL, finished_at = ?, updated_at = ?\nWHERE job_id = ?\n`,\n [now, now, jobId],\n );\n await appendBuildJobEvent(job, {\n at: now,\n jobId,\n seq: 0,\n state: \"succeeded\",\n type: \"succeeded\",\n });\n await rm(job.workspacePath, { force: true, recursive: true });\n });\n } finally {\n await state.close();\n }\n}\n\nasync function markBuildJobFailed(\n jobId: string,\n ownerId: string,\n error: unknown,\n): Promise<void> {\n const state = await openBuildQueueDatabase();\n\n try {\n await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (job.ownerId !== ownerId || job.state !== \"running\") {\n return;\n }\n\n await markBuildJobFailedInState(state, job, error);\n });\n } finally {\n await state.close();\n }\n}\n\nasync function markBuildJobStopped(\n jobId: string,\n ownerId: string,\n): Promise<void> {\n const state = await openBuildQueueDatabase();\n\n try {\n await state.transaction(async () => {\n const job = await requireBuildJobById(state, jobId);\n\n if (job.ownerId !== ownerId || job.state !== \"canceling\") {\n return;\n }\n\n await markBuildJobCanceled(state, job);\n });\n } finally {\n await state.close();\n }\n}\n\nasync function claimQueuedBuildJob(\n state: Database,\n ownerId: string,\n): Promise<BuildJob | undefined> {\n return await state.transaction(async () => {\n const job = await state.queryOne(\n `\nSELECT *\nFROM build_jobs\nWHERE state = 'queued'\nORDER BY queue_rank ASC, created_at ASC\nLIMIT 1\n`,\n undefined,\n mapBuildJob,\n );\n\n if (job === undefined) {\n return undefined;\n }\n\n const now = Date.now();\n await state.run(\n `\nUPDATE build_jobs\nSET state = 'running', owner_id = ?, owner_pid = ?, updated_at = ?\nWHERE job_id = ? AND state = 'queued'\n`,\n [ownerId, process.pid, now, job.jobId],\n );\n\n return await requireBuildJobById(state, job.jobId);\n });\n}\n\nasync function recoverStaleBuildJobs(state: Database): Promise<void> {\n const workspacePathsToDelete: string[] = [];\n\n await state.transaction(async () => {\n const jobs = await state.queryAll(\n `\nSELECT *\nFROM build_jobs\nWHERE state IN ('running', 'canceling')\n AND owner_pid IS NOT NULL\n`,\n undefined,\n mapBuildJob,\n );\n\n for (const job of jobs) {\n if (job.ownerPid !== undefined && isProcessAlive(job.ownerPid)) {\n continue;\n }\n\n if (job.state === \"canceling\") {\n await markBuildJobCanceled(state, job);\n continue;\n }\n\n await markBuildJobFailedInState(state, job, {\n message: \"Build worker process disappeared before finishing the job.\",\n name: \"BuildJobWorkerLost\",\n });\n workspacePathsToDelete.push(job.workspacePath);\n }\n });\n\n for (const workspacePath of workspacePathsToDelete) {\n await rm(workspacePath, { force: true, recursive: true });\n }\n}\n\nasync function acquireBuildWorkerLease(\n state: Database,\n ownerId: string,\n): Promise<boolean> {\n await recoverStaleBuildWorkerLease(state);\n\n return await state.transaction(async () => {\n const lease = await state.queryOne(\n `\nSELECT owner_pid\nFROM build_worker_lease\nWHERE id = 1\n`,\n undefined,\n (row) => ({\n ownerPid:\n row.owner_pid === null ? undefined : getNumber(row, \"owner_pid\"),\n }),\n );\n\n if (lease?.ownerPid !== undefined && isProcessAlive(lease.ownerPid)) {\n return false;\n }\n\n await state.run(\n `\nUPDATE build_worker_lease\nSET owner_id = ?, owner_pid = ?, heartbeat_at = ?\nWHERE id = 1\n`,\n [ownerId, process.pid, Date.now()],\n );\n return true;\n });\n}\n\nasync function heartbeatBuildWorker(\n ownerId: string,\n existingState?: Database,\n): Promise<void> {\n const state = existingState ?? (await openBuildQueueDatabase());\n\n try {\n await state.run(\n `\nUPDATE build_worker_lease\nSET heartbeat_at = ?\nWHERE id = 1 AND owner_id = ?\n`,\n [Date.now(), ownerId],\n );\n } finally {\n if (existingState === undefined) {\n await state.close();\n }\n }\n}\n\nasync function releaseBuildWorkerLease(\n state: Database,\n ownerId: string,\n): Promise<void> {\n await state.run(\n `\nUPDATE build_worker_lease\nSET owner_id = NULL, owner_pid = NULL, heartbeat_at = NULL\nWHERE id = 1 AND owner_id = ?\n`,\n [ownerId],\n );\n}\n\nasync function recoverStaleBuildWorkerLease(state: Database): Promise<void> {\n const lease = await state.queryOne(\n `\nSELECT owner_id, owner_pid\nFROM build_worker_lease\nWHERE id = 1\n`,\n undefined,\n (row) => ({\n ownerId: getOptionalString(row, \"owner_id\"),\n ownerPid:\n row.owner_pid === null ? undefined : getNumber(row, \"owner_pid\"),\n }),\n );\n\n if (lease?.ownerPid === undefined || isProcessAlive(lease.ownerPid)) {\n return;\n }\n\n await state.run(\n `\nUPDATE build_worker_lease\nSET owner_id = NULL, owner_pid = NULL, heartbeat_at = NULL\nWHERE id = 1\n`,\n );\n}\n\nasync function requireBuildJobById(\n state: Database,\n jobId: string,\n): Promise<BuildJob> {\n const job = await state.queryOne(\n \"SELECT * FROM build_jobs WHERE job_id = ?\",\n [jobId],\n mapBuildJob,\n );\n\n if (job === undefined) {\n throw new Error(`Build job not found: ${jobId}`);\n }\n\n return job;\n}\n\nasync function resolveBuildJobIdInState(\n state: Database,\n jobIdPrefix: string,\n): Promise<string> {\n const normalizedPrefix = jobIdPrefix.trim();\n\n if (normalizedPrefix === \"\") {\n throw new Error(\"Build job id is empty.\");\n }\n\n const jobs = await state.queryAll(\n `\nSELECT job_id\nFROM build_jobs\nWHERE substr(job_id, 1, ?) = ?\nORDER BY created_at DESC\n`,\n [normalizedPrefix.length, normalizedPrefix],\n (row) => getString(row, \"job_id\"),\n );\n\n if (jobs.length === 0) {\n throw new Error(`Build job not found: ${jobIdPrefix}`);\n }\n if (jobs.length > 1) {\n throw new Error(\n `Build job id prefix is ambiguous: ${jobIdPrefix}. Matches: ${jobs.join(\n \", \",\n )}`,\n );\n }\n\n return jobs[0]!;\n}\n\nasync function appendBuildJobEvent(\n job: Pick<BuildJob, \"eventsPath\" | \"jobId\">,\n event: BuildJobEvent,\n): Promise<void> {\n const seq = (await readLastBuildJobEventSeq(job)) + 1;\n const nextEvent = {\n ...event,\n jobId: job.jobId,\n seq,\n };\n\n await appendFile(job.eventsPath, `${JSON.stringify(nextEvent)}\\n`, \"utf8\");\n}\n\nasync function readLastBuildJobEventSeq(\n job: Pick<BuildJob, \"eventsPath\">,\n): Promise<number> {\n const events = await readBuildJobEvents(job);\n\n return events.at(-1)?.seq ?? 0;\n}\n\nasync function readMaxQueueRank(state: Database): Promise<number> {\n return (\n (await state.queryOne(\n \"SELECT COALESCE(MAX(queue_rank), 0) AS rank FROM build_jobs\",\n undefined,\n (row) => getNumber(row, \"rank\"),\n )) ?? 0\n );\n}\n\nasync function readMinQueueRank(state: Database): Promise<number> {\n return (\n (await state.queryOne(\n \"SELECT COALESCE(MIN(queue_rank), 0) AS rank FROM build_jobs\",\n undefined,\n (row) => getNumber(row, \"rank\"),\n )) ?? 0\n );\n}\n\nasync function openBuildQueueDatabase(): Promise<Database> {\n return await openSharedStateDatabase(\n getBuildQueueDatabasePath(),\n BUILD_QUEUE_SCHEMA_SQL,\n );\n}\n\nasync function openReadonlyBuildQueueDatabase(): Promise<Database> {\n return await openBuildQueueDatabase();\n}\n\nfunction getBuildQueueDatabasePath(): string {\n return join(getBuildQueueStateDirectoryPath(), \"job.sqlite\");\n}\n\nasync function createJobWorkspacePath(jobId: string): Promise<string> {\n const workspacePath = join(getBuildJobWorkspaceRootPath(), jobId);\n\n await mkdir(workspacePath, { recursive: true });\n return workspacePath;\n}\n\nfunction getBuildJobWorkspaceRootPath(): string {\n return join(getBuildQueueStateDirectoryPath(), \"work\");\n}\n\nasync function createJobCachePath(jobId: string): Promise<string> {\n const cachePath = join(getBuildJobCacheRootPath(), jobId);\n\n await mkdir(cachePath, { recursive: true });\n return cachePath;\n}\n\nfunction getBuildJobCacheRootPath(): string {\n return join(getBuildQueueStateDirectoryPath(), \"cache\");\n}\n\nasync function createJobLogPath(jobId: string): Promise<string> {\n const logPath = join(getBuildJobLogRootPath(), jobId);\n\n await mkdir(logPath, { recursive: true });\n return logPath;\n}\n\nfunction getBuildJobLogRootPath(): string {\n return join(getBuildQueueStateDirectoryPath(), \"logs\");\n}\n\nasync function createJobEventsPath(jobId: string): Promise<string> {\n const rootPath = join(getBuildQueueStateDirectoryPath(), \"events\");\n\n await mkdir(rootPath, { recursive: true });\n return join(rootPath, `${jobId}.ndjson`);\n}\n\nfunction getBuildQueueStateDirectoryPath(): string {\n return resolveWikiGraphJobsDirectoryPath();\n}\n\nfunction mapBuildJob(row: Record<string, unknown>): BuildJob {\n const currentStep = parseOptionalBuildJobTarget(\n getOptionalString(row, \"current_step\"),\n \"current_step\",\n );\n const ownerId = getOptionalString(row, \"owner_id\");\n const ownerPid =\n row.owner_pid === null ? undefined : getNumber(row, \"owner_pid\");\n const readingSummaryStartedAt =\n row.reading_summary_started_at === null\n ? undefined\n : getNumber(row, \"reading_summary_started_at\");\n const finishedAt =\n row.finished_at === null ? undefined : getNumber(row, \"finished_at\");\n const errorJSON = getOptionalString(row, \"error_json\");\n const inputRevision =\n row.input_revision === null ? undefined : getNumber(row, \"input_revision\");\n const llmJSON = getOptionalString(row, \"llm_json\");\n const prompt = getOptionalString(row, \"prompt\");\n\n return {\n archiveKey: getString(row, \"archive_key\"),\n archivePath: getString(row, \"archive_path\"),\n cachePath: getString(row, \"cache_path\"),\n chapterId: getNumber(row, \"chapter_id\"),\n createdAt: getNumber(row, \"created_at\"),\n ...(currentStep === undefined ? {} : { currentStep }),\n ...(errorJSON === undefined ? {} : { errorJSON }),\n eventsPath: getString(row, \"events_path\"),\n ...(finishedAt === undefined ? {} : { finishedAt }),\n jobId: getString(row, \"job_id\"),\n ...(inputRevision === undefined ? {} : { inputRevision }),\n logPath: getString(row, \"log_path\"),\n ...(llmJSON === undefined ? {} : { llmJSON }),\n ...(ownerId === undefined ? {} : { ownerId }),\n ...(ownerPid === undefined ? {} : { ownerPid }),\n ...(prompt === undefined ? {} : { prompt }),\n queueRank: getNumber(row, \"queue_rank\"),\n state: parseBuildJobState(getString(row, \"state\")),\n ...(readingSummaryStartedAt === undefined\n ? {}\n : { readingSummaryStartedAt }),\n target: parseBuildJobTarget(getString(row, \"target\"), \"target\"),\n updatedAt: getNumber(row, \"updated_at\"),\n workspacePath: getString(row, \"workspace_path\"),\n };\n}\n\nfunction parseBuildJobState(value: string): BuildJobState {\n if (BUILD_JOB_STATES.includes(value as BuildJobState)) {\n return value as BuildJobState;\n }\n\n throw new Error(`Invalid build job state: ${value}`);\n}\n\nfunction parseBuildJobTarget(value: string, field: string): BuildJobTarget {\n if (\n value === \"reading-graph\" ||\n value === \"knowledge-graph\" ||\n value === \"reading-summary\"\n ) {\n return value;\n }\n\n throw new Error(`Invalid ${field}: ${value}`);\n}\n\nfunction parseOptionalBuildJobTarget(\n value: string | undefined,\n field: string,\n): BuildJobTarget | undefined {\n return value === undefined ? undefined : parseBuildJobTarget(value, field);\n}\n\nfunction formatBuildJobLane(target: BuildJobTarget): string {\n return target === \"knowledge-graph\" ? \"knowledge-graph\" : \"reading\";\n}\n\nfunction getString(row: Record<string, unknown>, key: string): string {\n const value = row[key];\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n\nfunction getNumber(row: Record<string, unknown>, key: string): number {\n const value = row[key];\n\n if (typeof value !== \"number\") {\n throw new TypeError(`Expected ${key} to be a number`);\n }\n\n return value;\n}\n\nfunction getOptionalString(\n row: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = row[key];\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value !== \"string\") {\n throw new TypeError(`Expected ${key} to be a string`);\n }\n\n return value;\n}\n\nfunction createArchiveKey(archivePath: string): string {\n return createHash(\"sha256\").update(resolve(archivePath)).digest(\"hex\");\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction formatErrorEvent(error: unknown): unknown {\n if (error instanceof Error) {\n return {\n message: error.message,\n name: error.name,\n stack: error.stack,\n };\n }\n\n return error;\n}\n\nasync function delay(ms: number): Promise<void> {\n await new Promise<void>((resolveDelay) => {\n setTimeout(resolveDelay, ms);\n });\n}\n\nclass BuildJobProgressAccumulator implements BuildJobProgressReporter {\n readonly #job: BuildJob;\n readonly #ownerId: string;\n readonly #outputCharactersPerToken = 4;\n readonly #refreshIntervalMs = 5_000;\n #graphWords = 0;\n #lastSnapshotAt = 0;\n #outputCharacters = 0;\n #phase: BuildJobProgressPhase | undefined;\n readonly #phaseCounters = new Map<string, BuildJobProgressCounter>();\n #step: BuildJobTarget | undefined;\n #tokenUsage: BuildJobTokenUsage = {};\n #readingSummaryWords = 0;\n #stopCheckQueue: Promise<void> = Promise.resolve();\n #totalGraphWords = 0;\n #totalReadingSummaryWords = 0;\n #writeQueue: Promise<void> = Promise.resolve();\n\n public constructor(job: BuildJob, ownerId: string) {\n this.#job = job;\n this.#ownerId = ownerId;\n }\n\n public async addOutputCharacters(characters: number): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#outputCharacters += characters;\n await this.#snapshot();\n });\n }\n\n public async addTokenUsage(usage: BuildJobTokenUsage): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#tokenUsage = {\n ...formatOptionalTokenUsage(\n \"cacheReadTokens\",\n addOptionalNumbers(\n this.#tokenUsage.cacheReadTokens,\n usage.cacheReadTokens,\n ),\n ),\n ...formatOptionalTokenUsage(\n \"inputTokens\",\n addOptionalNumbers(this.#tokenUsage.inputTokens, usage.inputTokens),\n ),\n ...formatOptionalTokenUsage(\n \"outputTokens\",\n addOptionalNumbers(this.#tokenUsage.outputTokens, usage.outputTokens),\n ),\n };\n await this.#snapshot(true);\n });\n }\n\n public async setTotals(input: {\n readonly totalGraphWords?: number;\n readonly totalReadingSummaryWords?: number;\n }): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#totalGraphWords = input.totalGraphWords ?? this.#totalGraphWords;\n this.#totalReadingSummaryWords =\n input.totalReadingSummaryWords ?? this.#totalReadingSummaryWords;\n await this.#snapshot(true);\n });\n }\n\n public async stepStarted(step: BuildJobTarget): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#step = step;\n this.#phase = undefined;\n this.#phaseCounters.clear();\n await markBuildJobStep(this.#job.jobId, step);\n await appendBuildJobEvent(this.#job, {\n at: Date.now(),\n jobId: this.#job.jobId,\n seq: 0,\n step,\n type: \"step_started\",\n });\n await this.#snapshot(true);\n });\n }\n\n public async stepCompleted(step: BuildJobTarget): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#step = step;\n this.#phase = undefined;\n this.#phaseCounters.clear();\n await appendBuildJobEvent(this.#job, {\n at: Date.now(),\n jobId: this.#job.jobId,\n seq: 0,\n step,\n type: \"step_completed\",\n });\n await this.#snapshot(true);\n });\n }\n\n public async updateWords(input: {\n readonly graphWords?: number;\n readonly readingSummaryWords?: number;\n }): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n this.#graphWords =\n input.graphWords === undefined\n ? this.#graphWords\n : clampProgressWords(input.graphWords, this.#totalGraphWords);\n this.#readingSummaryWords =\n input.readingSummaryWords === undefined\n ? this.#readingSummaryWords\n : clampProgressWords(\n input.readingSummaryWords,\n this.#totalReadingSummaryWords,\n );\n await this.#snapshot();\n });\n }\n\n public async updatePhase(input: {\n readonly done: number;\n readonly force?: boolean;\n readonly phase: BuildJobProgressPhase;\n readonly phaseDetail?: string;\n readonly total: number;\n readonly unit: BuildJobProgressUnit;\n }): Promise<void> {\n await this.#enqueue(async () => {\n await this.throwIfStopped();\n if (this.#phase !== input.phase) {\n this.#phaseCounters.clear();\n }\n this.#phase = input.phase;\n this.#phaseCounters.set(formatProgressCounterKey(input), {\n done: clampProgressWords(input.done, input.total),\n name: formatProgressCounterName(input),\n total: Math.max(0, input.total),\n unit: input.unit,\n });\n await this.#snapshot(input.force ?? true);\n });\n }\n\n public async throwIfStopped(): Promise<void> {\n const queued = this.#stopCheckQueue.then(async () => {\n const job = await readBuildJobForStopCheck(this.#job.jobId);\n\n if (job.state === \"running\" && job.ownerId === this.#ownerId) {\n return;\n }\n\n throw new BuildJobStoppedError(\n `Job ${this.#job.jobId} is ${job.state}. Stop current worker execution.`,\n );\n });\n\n this.#stopCheckQueue = queued.catch(() => undefined);\n await queued;\n }\n\n async #enqueue(operation: () => Promise<void>): Promise<void> {\n const queued = this.#writeQueue.then(operation, operation);\n\n this.#writeQueue = queued.catch(() => undefined);\n await queued;\n }\n\n async #snapshot(force = false): Promise<void> {\n const now = Date.now();\n\n if (!force && now - this.#lastSnapshotAt < this.#refreshIntervalMs) {\n return;\n }\n\n this.#lastSnapshotAt = now;\n const tokens = this.#formatTokenUsage();\n\n await appendBuildJobEvent(this.#job, {\n at: now,\n counters: this.#createCounters(),\n jobId: this.#job.jobId,\n ...(this.#phase === undefined ? {} : { phase: this.#phase }),\n seq: 0,\n ...(this.#step === undefined ? {} : { step: this.#step }),\n ...(tokens === undefined ? {} : { tokens }),\n type: \"status_snapshot\",\n });\n }\n\n #createCounters(): readonly BuildJobProgressCounter[] {\n const wordCounter = this.#createWordCounter();\n return [\n ...(wordCounter === undefined ? [] : [wordCounter]),\n ...this.#phaseCounters.values(),\n ];\n }\n\n #createWordCounter(): BuildJobProgressCounter | undefined {\n switch (this.#step) {\n case \"reading-graph\":\n return this.#totalGraphWords <= 0\n ? undefined\n : {\n done: this.#graphWords,\n name: \"words\",\n total: this.#totalGraphWords,\n unit: \"word\",\n };\n case \"reading-summary\":\n return this.#totalReadingSummaryWords <= 0\n ? undefined\n : {\n done: this.#readingSummaryWords,\n name: \"words\",\n total: this.#totalReadingSummaryWords,\n unit: \"word\",\n };\n case undefined:\n return undefined;\n }\n }\n\n #formatTokenUsage(): BuildJobTokenUsage | undefined {\n const outputTokens =\n this.#tokenUsage.outputTokens ??\n (this.#outputCharacters === 0\n ? undefined\n : Math.floor(this.#outputCharacters / this.#outputCharactersPerToken));\n const usage = {\n ...(this.#tokenUsage.cacheReadTokens === undefined\n ? {}\n : { cacheReadTokens: this.#tokenUsage.cacheReadTokens }),\n ...(this.#tokenUsage.inputTokens === undefined\n ? {}\n : { inputTokens: this.#tokenUsage.inputTokens }),\n ...(outputTokens === undefined ? {} : { outputTokens }),\n };\n\n return Object.keys(usage).length === 0 ? undefined : usage;\n }\n}\n\nfunction formatOptionalTokenUsage(\n key: keyof BuildJobTokenUsage,\n value: number | undefined,\n): BuildJobTokenUsage {\n return value === undefined ? {} : { [key]: value };\n}\n\nfunction addOptionalNumbers(\n left: number | undefined,\n right: number | undefined,\n): number | undefined {\n if (left === undefined && right === undefined) {\n return undefined;\n }\n\n return (left ?? 0) + (right ?? 0);\n}\n\nfunction formatProgressCounterKey(input: {\n readonly phaseDetail?: string;\n readonly unit: BuildJobProgressUnit;\n}): string {\n return input.phaseDetail ?? input.unit;\n}\n\nfunction formatProgressCounterName(input: {\n readonly phaseDetail?: string;\n readonly unit: BuildJobProgressUnit;\n}): string {\n if (input.phaseDetail !== undefined) {\n return input.phaseDetail;\n }\n\n switch (input.unit) {\n case \"candidate\":\n return \"candidates\";\n case \"char\":\n return \"chars\";\n case \"item\":\n return \"items\";\n case \"page\":\n return \"page\";\n case \"qid\":\n return \"qids\";\n case \"record\":\n return \"records\";\n case \"sentence\":\n return \"sentences\";\n case \"window\":\n return \"windows\";\n }\n}\n\nclass BuildJobStoppedError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"BuildJobStoppedError\";\n }\n}\n\nfunction clampProgressWords(words: number, totalWords: number): number {\n if (totalWords <= 0) {\n return Math.max(0, words);\n }\n\n return Math.min(totalWords, Math.max(0, words));\n}\n\nasync function markBuildJobStep(\n jobId: string,\n step: BuildJobTarget,\n): Promise<void> {\n const state = await openBuildQueueDatabase();\n\n try {\n await state.run(\n `\nUPDATE build_jobs\nSET current_step = ?,\n reading_summary_started_at = CASE\n WHEN ? = 'reading-summary' AND reading_summary_started_at IS NULL THEN ?\n ELSE reading_summary_started_at\n END,\n updated_at = ?\nWHERE job_id = ?\n`,\n [step, step, Date.now(), Date.now(), jobId],\n );\n } finally {\n await state.close();\n }\n}\n","import { mkdir, readFile, rm, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { z } from \"zod\";\n\nimport type {\n ChunkRecord,\n Document,\n FragmentRecord,\n ReadingEdgeRecord,\n MentionLinkRecord,\n MentionRecord,\n ReadonlyChunkStore,\n ReadonlyDocument,\n ReadonlyFragmentGroupStore,\n ReadonlyGraphBuildParameterStore,\n ReadonlyReadingEdgeStore,\n ReadonlyMentionLinkStore,\n ReadonlyMentionStore,\n ReadonlyObjectMetadataStore,\n ReadonlySerialFragments,\n ReadonlySerialStore,\n ReadonlySnakeChunkStore,\n ReadonlySnakeEdgeStore,\n ReadonlySnakeStore,\n SentenceId,\n SentenceGroupRecord,\n SerialRecord,\n SnakeChunkRecord,\n SnakeEdgeRecord,\n SnakeRecord,\n} from \"../document/index.js\";\nimport { DirectoryDocument, Fragments } from \"../document/index.js\";\nimport { WIKI_GRAPH_EDITOR_SCOPES } from \"../common/llm-scope.js\";\nimport { normalizeLanguageCode } from \"../common/language.js\";\nimport { compressText } from \"../editor/index.js\";\nimport { segmentTextStream, type ReaderTextStream } from \"../reader/index.js\";\nimport {\n SerialGeneration,\n type BuildSerialTopologyOptions,\n} from \"../serial.js\";\nimport { ChunkImportance, ChunkRetention } from \"../document/types.js\";\n\nimport {\n getChapterDetails,\n type ChapterDetails,\n type GenerateChapterGraphOptions,\n type GenerateChapterSummaryOptions,\n} from \"./chapter.js\";\n\nexport interface ChapterGraphBuildArtifact {\n readonly documentPath: string;\n readonly chapterId: number;\n readonly parameter: GraphBuildParameterInput;\n}\n\nexport interface GraphBuildParameterInput {\n readonly language?: string;\n readonly prompt: string;\n}\n\nexport interface ChapterSummaryInputSnapshot {\n readonly filePath: string;\n}\n\nexport interface BuildChapterGraphArtifactOptions extends GenerateChapterGraphOptions {\n readonly sourceText: readonly string[];\n readonly workspacePath: string;\n}\n\nexport interface BuildChapterSummaryArtifactOptions extends GenerateChapterSummaryOptions {\n readonly snapshotPath?: string;\n readonly sourceDocumentPath?: string;\n readonly workspacePath: string;\n}\n\nconst sentenceIdSchema = z.tuple([\n z.number(),\n z.number(),\n]) satisfies z.ZodType<SentenceId>;\nconst GRAPH_ARTIFACT_FRAGMENT_WORDS_COUNT = 320;\nconst sentenceRecordSchema = z.object({\n text: z.string(),\n wordsCount: z.number(),\n});\nconst fragmentRecordSchema = z.object({\n serialId: z.number(),\n fragmentId: z.number(),\n summary: z.string(),\n sentences: z.array(sentenceRecordSchema),\n}) satisfies z.ZodType<FragmentRecord>;\nconst serialRecordSchema = z\n .object({\n documentOrder: z.number().optional(),\n id: z.number(),\n knowledgeGraphReady: z.boolean(),\n knowledgeGraphParameterHash: z.string().optional(),\n revision: z.number().optional(),\n topologyParameterHash: z.string().optional(),\n topologyReady: z.boolean(),\n })\n .transform((record) => ({\n documentOrder: record.documentOrder ?? record.id,\n id: record.id,\n knowledgeGraphReady: record.knowledgeGraphReady,\n ...(record.knowledgeGraphParameterHash === undefined\n ? {}\n : { knowledgeGraphParameterHash: record.knowledgeGraphParameterHash }),\n revision: record.revision ?? 0,\n topologyReady: record.topologyReady,\n ...(record.topologyParameterHash === undefined\n ? {}\n : { topologyParameterHash: record.topologyParameterHash }),\n })) satisfies z.ZodType<SerialRecord>;\nconst chunkRecordSchema = z.object({\n id: z.number(),\n generation: z.number(),\n sentenceId: sentenceIdSchema,\n label: z.string(),\n content: z.string(),\n sentenceIds: z.array(sentenceIdSchema),\n retention: z.enum(ChunkRetention).optional(),\n importance: z.enum(ChunkImportance).optional(),\n wordsCount: z.number(),\n weight: z.number(),\n});\nconst knowledgeEdgeRecordSchema = z.object({\n fromId: z.number(),\n toId: z.number(),\n strength: z.string().optional(),\n weight: z.number(),\n});\nconst snakeRecordSchema = z.object({\n id: z.number(),\n serialId: z.number(),\n groupId: z.number(),\n localSnakeId: z.number(),\n size: z.number(),\n firstLabel: z.string(),\n lastLabel: z.string(),\n wordsCount: z.number(),\n weight: z.number(),\n}) satisfies z.ZodType<SnakeRecord>;\nconst snakeChunkRecordSchema = z.object({\n snakeId: z.number(),\n chunkId: z.number(),\n position: z.number(),\n}) satisfies z.ZodType<SnakeChunkRecord>;\nconst snakeEdgeRecordSchema = z.object({\n fromSnakeId: z.number(),\n toSnakeId: z.number(),\n weight: z.number(),\n}) satisfies z.ZodType<SnakeEdgeRecord>;\nconst fragmentGroupRecordSchema = z.object({\n serialId: z.number(),\n groupId: z.number(),\n startSentenceIndex: z.number(),\n endSentenceIndex: z.number(),\n}) satisfies z.ZodType<SentenceGroupRecord>;\n\nconst summaryInputSnapshotSchema = z.object({\n chunks: z.array(chunkRecordSchema),\n fragmentGroups: z.array(fragmentGroupRecordSchema),\n fragments: z.array(fragmentRecordSchema),\n readingEdges: z.array(knowledgeEdgeRecordSchema),\n serial: serialRecordSchema,\n snakeChunks: z.array(snakeChunkRecordSchema),\n snakeEdges: z.array(snakeEdgeRecordSchema),\n snakes: z.array(snakeRecordSchema),\n});\n\ninterface SummaryInputSnapshotData {\n readonly chunks: readonly ChunkRecord[];\n readonly fragmentGroups: readonly SentenceGroupRecord[];\n readonly fragments: readonly FragmentRecord[];\n readonly readingEdges: readonly ReadingEdgeRecord[];\n readonly serial: SerialRecord;\n readonly snakeChunks: readonly SnakeChunkRecord[];\n readonly snakeEdges: readonly SnakeEdgeRecord[];\n readonly snakes: readonly SnakeRecord[];\n}\n\nexport async function readChapterBuildInput(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<{\n readonly details: ChapterDetails;\n readonly revision: number;\n readonly sourceText: readonly string[];\n}> {\n const details = await getChapterDetails(document, chapterId);\n\n return {\n details,\n revision: await document.serials.getRevision(chapterId),\n sourceText: await collectReaderText(readChapterSource(document, chapterId)),\n };\n}\n\nexport async function buildChapterGraphArtifact(\n chapterId: number,\n options: BuildChapterGraphArtifactOptions,\n): Promise<ChapterGraphBuildArtifact> {\n const documentPath = join(options.workspacePath, \"graph-document\");\n\n await rm(documentPath, { force: true, recursive: true });\n await mkdir(options.workspacePath, { recursive: true });\n\n const document = await DirectoryDocument.open(documentPath);\n\n try {\n await document.openSession(async (openedDocument) => {\n await openedDocument.serials.createWithId(chapterId);\n await writeGraphArtifactSourceFragments(\n documentPath,\n chapterId,\n options.sourceText,\n );\n const artifactDocument = createFragmentBackedDocument(\n openedDocument,\n documentPath,\n );\n\n await new SerialGeneration({\n document: artifactDocument,\n llm: options.llm,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n }).buildTopologyInto(\n chapterId,\n createTopologyOptions(options),\n options.progressTracker,\n );\n });\n } finally {\n await document.release();\n }\n\n return {\n chapterId,\n documentPath,\n parameter: createGraphBuildParameterInput(options),\n };\n}\n\nexport async function commitChapterGraphArtifact(\n document: Document,\n artifact: ChapterGraphBuildArtifact,\n): Promise<ChapterDetails> {\n const sourceDocument = await DirectoryDocument.open(artifact.documentPath);\n\n try {\n await document.openSession(async (openedDocument) => {\n await requireStage(openedDocument, artifact.chapterId, \"sourced\");\n await openedDocument.clearSerialGraph(artifact.chapterId);\n await openedDocument.serials.ensure(artifact.chapterId);\n\n const chunkIdMap = await copyChunks(\n sourceDocument,\n openedDocument,\n artifact.chapterId,\n );\n\n for (const edge of await sourceDocument.readingEdges.listBySerial(\n artifact.chapterId,\n )) {\n const fromId = chunkIdMap.get(edge.fromId);\n const toId = chunkIdMap.get(edge.toId);\n\n if (fromId === undefined || toId === undefined) {\n continue;\n }\n\n await openedDocument.readingEdges.save({\n ...edge,\n fromId,\n toId,\n });\n }\n\n await openedDocument.fragmentGroups.saveMany(\n await sourceDocument.fragmentGroups.listBySerial(artifact.chapterId),\n );\n await copySnakes(\n sourceDocument,\n openedDocument,\n artifact.chapterId,\n chunkIdMap,\n );\n const parameter = await openedDocument.graphBuildParameters.save(\n artifact.parameter,\n );\n await openedDocument.serials.setTopologyReady(\n artifact.chapterId,\n true,\n parameter.hash,\n );\n });\n\n return await getChapterDetails(document, artifact.chapterId);\n } finally {\n await sourceDocument.release();\n }\n}\n\nexport async function buildChapterSummaryArtifact(\n document: ReadonlyDocument,\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions,\n): Promise<string> {\n const snapshotPath = options.snapshotPath;\n\n if (snapshotPath !== undefined) {\n return await buildChapterSummaryArtifactFromSnapshot(chapterId, {\n ...options,\n snapshotPath,\n });\n }\n\n const sourceDocumentPath = options.sourceDocumentPath;\n\n if (sourceDocumentPath !== undefined) {\n return await buildChapterSummaryArtifactFromDocumentSnapshot(chapterId, {\n ...options,\n sourceDocumentPath,\n });\n }\n\n return await buildChapterSummaryArtifactFromDocument(\n document,\n chapterId,\n options,\n );\n}\n\nexport async function buildChapterSummaryArtifactFromSnapshot(\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions & {\n readonly snapshotPath: string;\n },\n): Promise<string> {\n const snapshot = await readSummaryInputSnapshot(options.snapshotPath);\n return await buildSummaryFromSnapshot(snapshot, chapterId, options);\n}\n\nasync function buildChapterSummaryArtifactFromDocumentSnapshot(\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions & {\n readonly sourceDocumentPath: string;\n },\n): Promise<string> {\n const document = await DirectoryDocument.open(options.sourceDocumentPath);\n\n try {\n return await buildChapterSummaryArtifactFromDocument(\n createFragmentBackedDocument(document, options.sourceDocumentPath),\n chapterId,\n options,\n );\n } finally {\n await document.release();\n }\n}\n\nasync function buildChapterSummaryArtifactFromDocument(\n document: ReadonlyDocument,\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions,\n): Promise<string> {\n const details = await getChapterDetails(document, chapterId);\n\n if (details.stage !== \"graphed\") {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Generate a summary only for graphed chapters.`,\n );\n }\n\n const summary = await document.readSummary(chapterId);\n\n if (summary !== undefined) {\n return summary;\n }\n\n return await buildSummaryFromDocument(document, chapterId, options);\n}\n\nexport async function commitChapterSummaryArtifact(\n document: Document,\n chapterId: number,\n summary: string,\n): Promise<ChapterDetails> {\n await document.openSession(async (openedDocument) => {\n await requireStage(openedDocument, chapterId, \"graphed\");\n await openedDocument.writeSummary(chapterId, summary);\n });\n\n return await getChapterDetails(document, chapterId);\n}\n\nexport async function snapshotChapterSummaryInput(\n document: ReadonlyDocument,\n chapterId: number,\n workspacePath: string,\n): Promise<ChapterSummaryInputSnapshot> {\n const filePath = join(workspacePath, \"summary-input.json\");\n\n await mkdir(workspacePath, { recursive: true });\n await requireStage(document, chapterId, \"graphed\");\n\n const fragments = await readSerialFragments(document, chapterId);\n const snakes = await document.snakes.listBySerial(chapterId);\n const snakeChunks = (\n await Promise.all(\n snakes.map(\n async (snake) => await document.snakeChunks.listBySnake(snake.id),\n ),\n )\n ).flat();\n\n await writeSummaryInputSnapshot(filePath, {\n chunks: await document.chunks.listBySerial(chapterId),\n fragmentGroups: await document.fragmentGroups.listBySerial(chapterId),\n fragments,\n readingEdges: await document.readingEdges.listBySerial(chapterId),\n serial: {\n documentOrder: chapterId,\n id: chapterId,\n knowledgeGraphReady: false,\n revision: 0,\n topologyReady: true,\n },\n snakeChunks,\n snakeEdges: await document.snakeEdges.listBySerial(chapterId),\n snakes,\n });\n\n return { filePath };\n}\n\nasync function buildSummaryFromSnapshot(\n snapshot: SummaryInputSnapshotData,\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions,\n): Promise<string> {\n if (snapshot.serial.id !== chapterId) {\n throw new Error(\n `Summary snapshot belongs to chapter ${snapshot.serial.id}, not chapter ${chapterId}.`,\n );\n }\n if (!snapshot.serial.topologyReady) {\n throw new Error(`Chapter ${chapterId} is not ready for summary.`);\n }\n\n const document = new SummaryInputSnapshotDocument(snapshot);\n const fragments = document.getSerialFragments(chapterId);\n const sentenceStartIndexes = await fragments.listFragmentIds();\n\n if (sentenceStartIndexes.length <= 1) {\n return await readPassthroughSummary(fragments, sentenceStartIndexes);\n }\n\n const summaryParts: string[] = [];\n\n for (const groupId of await document.fragmentGroups.listGroupIdsForSerial(\n chapterId,\n )) {\n const groupSummary = await compressText({\n compressionRatio: 0.2,\n document,\n groupId,\n llm: options.llm,\n maxClues: 10,\n maxIterations: 5,\n scopes: WIKI_GRAPH_EDITOR_SCOPES,\n serialId: chapterId,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n\n if (groupSummary.trim() === \"\") {\n continue;\n }\n summaryParts.push(groupSummary);\n }\n\n return summaryParts.join(\"\\n\\n\");\n}\n\nasync function buildSummaryFromDocument(\n document: ReadonlyDocument,\n chapterId: number,\n options: BuildChapterSummaryArtifactOptions,\n): Promise<string> {\n const serial = await document.serials.getById(chapterId);\n\n if (serial === undefined) {\n throw new Error(\n `Chapter ${chapterId} does not exist. Use \\`wg <archive-uri>/chapter list\\` to discover chapter ids.`,\n );\n }\n if (!serial.topologyReady) {\n throw new Error(`Chapter ${chapterId} is not ready for summary.`);\n }\n\n const fragments = document.getSerialFragments(chapterId);\n const sentenceStartIndexes = await fragments.listFragmentIds();\n\n if (sentenceStartIndexes.length <= 1) {\n return await readPassthroughSummary(fragments, sentenceStartIndexes);\n }\n\n const summaryParts: string[] = [];\n\n for (const groupId of await document.fragmentGroups.listGroupIdsForSerial(\n chapterId,\n )) {\n const groupSummary = await compressText({\n compressionRatio: 0.2,\n document,\n groupId,\n llm: options.llm,\n maxClues: 10,\n maxIterations: 5,\n scopes: WIKI_GRAPH_EDITOR_SCOPES,\n serialId: chapterId,\n ...(options.logDirPath === undefined\n ? {}\n : { logDirPath: options.logDirPath }),\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n });\n\n if (groupSummary.trim() === \"\") {\n continue;\n }\n summaryParts.push(groupSummary);\n }\n\n return summaryParts.join(\"\\n\\n\");\n}\n\nasync function readPassthroughSummary(\n fragments: ReadonlySerialFragments,\n sentenceStartIndexes: readonly number[],\n): Promise<string> {\n if (sentenceStartIndexes.length === 0) {\n return \"\";\n }\n\n const records = await Promise.all(\n sentenceStartIndexes.map(\n async (startSentenceIndex) =>\n await fragments.getFragment(startSentenceIndex),\n ),\n );\n\n return records\n .flatMap((fragment) => fragment.sentences.map((sentence) => sentence.text))\n .join(\" \")\n .trim();\n}\n\nasync function writeGraphArtifactSourceFragments(\n documentPath: string,\n chapterId: number,\n sourceText: ReaderTextStream,\n): Promise<void> {\n const fragments = new Fragments(documentPath);\n const serial = fragments.getSerial(chapterId);\n let draft = await serial.createDraft();\n let draftWordsCount = 0;\n let hasSentences = false;\n\n await fragments.ensureCreated();\n\n for await (const sentence of segmentTextStream(sourceText)) {\n const text = sentence.text.trim();\n\n if (text === \"\") {\n continue;\n }\n if (\n draftWordsCount > 0 &&\n draftWordsCount + sentence.wordsCount >\n GRAPH_ARTIFACT_FRAGMENT_WORDS_COUNT\n ) {\n await draft.commit();\n draft = await serial.createDraft();\n draftWordsCount = 0;\n }\n\n draft.addSentence(text, sentence.wordsCount);\n draftWordsCount += sentence.wordsCount;\n hasSentences = true;\n }\n\n if (hasSentences) {\n await draft.commit();\n } else {\n draft.discard();\n }\n}\n\nfunction createFragmentBackedDocument<TDocument extends ReadonlyDocument>(\n document: TDocument,\n documentPath: string,\n): TDocument {\n const fragments = new Fragments(documentPath);\n\n return new Proxy(document, {\n get(target, property, receiver): unknown {\n if (property === \"getSerialFragments\") {\n return (serialId: number) => fragments.getSerial(serialId);\n }\n if (property === \"getSummaryFragments\") {\n return (serialId: number) => fragments.getSummarySerial(serialId);\n }\n if (property === \"getSentence\") {\n return async (sentenceId: SentenceId) =>\n await fragments.getSentence(sentenceId);\n }\n\n const value = Reflect.get(target, property, receiver) as unknown;\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n return value.bind(target) as unknown;\n },\n });\n}\n\nasync function readSerialFragments(\n document: ReadonlyDocument,\n serialId: number,\n): Promise<readonly FragmentRecord[]> {\n const fragments = document.getSerialFragments(serialId);\n\n return await Promise.all(\n (await fragments.listFragmentIds()).map(\n async (fragmentId) => await fragments.getFragment(fragmentId),\n ),\n );\n}\n\nasync function readSummaryInputSnapshot(\n filePath: string,\n): Promise<SummaryInputSnapshotData> {\n const snapshot = summaryInputSnapshotSchema.parse(\n JSON.parse(await readFile(filePath, \"utf8\")),\n );\n\n return {\n ...snapshot,\n chunks: snapshot.chunks.map(toChunkRecord),\n readingEdges: snapshot.readingEdges.map(toReadingEdgeRecord),\n };\n}\n\nfunction toChunkRecord(record: z.infer<typeof chunkRecordSchema>): ChunkRecord {\n return {\n id: record.id,\n generation: record.generation,\n sentenceId: record.sentenceId,\n label: record.label,\n content: record.content,\n sentenceIds: record.sentenceIds,\n ...(record.retention === undefined ? {} : { retention: record.retention }),\n ...(record.importance === undefined\n ? {}\n : { importance: record.importance }),\n wordsCount: record.wordsCount,\n weight: record.weight,\n };\n}\n\nfunction toReadingEdgeRecord(\n record: z.infer<typeof knowledgeEdgeRecordSchema>,\n): ReadingEdgeRecord {\n return {\n fromId: record.fromId,\n toId: record.toId,\n ...(record.strength === undefined ? {} : { strength: record.strength }),\n weight: record.weight,\n };\n}\n\nasync function writeSummaryInputSnapshot(\n filePath: string,\n snapshot: SummaryInputSnapshotData,\n): Promise<void> {\n await writeFile(filePath, `${JSON.stringify(snapshot)}\\n`, \"utf8\");\n}\n\nclass SummaryInputSnapshotDocument implements ReadonlyDocument {\n public readonly chunks: ReadonlyChunkStore;\n public readonly fragmentGroups: ReadonlyFragmentGroupStore;\n public readonly graphBuildParameters: ReadonlyGraphBuildParameterStore;\n public readonly readingEdges: ReadonlyReadingEdgeStore;\n public readonly mentionLinks: ReadonlyMentionLinkStore;\n public readonly mentions: ReadonlyMentionStore;\n public readonly metadata: ReadonlyObjectMetadataStore;\n public readonly serials: ReadonlySerialStore;\n public readonly snakeChunks: ReadonlySnakeChunkStore;\n public readonly snakeEdges: ReadonlySnakeEdgeStore;\n public readonly snakes: ReadonlySnakeStore;\n readonly #fragments: ReadonlySerialFragments;\n\n public constructor(snapshot: SummaryInputSnapshotData) {\n const serialId = snapshot.serial.id;\n\n this.chunks = new SnapshotChunkStore(snapshot.chunks, snapshot.fragments);\n this.fragmentGroups = new SnapshotFragmentGroupStore(\n snapshot.fragmentGroups,\n );\n this.graphBuildParameters = new EmptySnapshotGraphBuildParameterStore();\n this.readingEdges = new SnapshotReadingEdgeStore(\n snapshot.readingEdges,\n snapshot.chunks,\n );\n this.mentionLinks = new EmptySnapshotMentionLinkStore();\n this.mentions = new EmptySnapshotMentionStore();\n this.metadata = new EmptySnapshotObjectMetadataStore();\n this.serials = new SnapshotSerialStore(snapshot.serial);\n this.snakeChunks = new SnapshotSnakeChunkStore(snapshot.snakeChunks);\n this.snakeEdges = new SnapshotSnakeEdgeStore(\n snapshot.snakeEdges,\n snapshot.snakes,\n );\n this.snakes = new SnapshotSnakeStore(snapshot.snakes);\n this.#fragments = new SnapshotSerialFragments(serialId, snapshot.fragments);\n }\n\n public async getSentence(sentenceId: SentenceId): Promise<string> {\n const [serialId, sentenceIndex] = sentenceId;\n const sentence = (await this.getSerialFragments(serialId).listSentences!())[\n sentenceIndex\n ];\n\n if (sentence === undefined) {\n throw new RangeError(`Sentence ${sentenceIndex} does not exist`);\n }\n\n return sentence.text;\n }\n\n public getSerialFragments(serialId: number): ReadonlySerialFragments {\n if (serialId !== this.#fragments.serialId) {\n return new SnapshotSerialFragments(serialId, []);\n }\n return this.#fragments;\n }\n\n public getSummaryFragments(serialId: number): ReadonlySerialFragments {\n return new SnapshotSerialFragments(serialId, []);\n }\n\n public async openSession<T>(\n operation: (document: ReadonlyDocument) => Promise<T> | T,\n ): Promise<T> {\n return await operation(this);\n }\n\n public readDatabase<T>(): Promise<T> {\n return Promise.reject(\n new Error(\"Summary input snapshots do not expose a SQLite database.\"),\n );\n }\n\n public readSearchIndexDatabase<T>(): Promise<T> {\n return Promise.reject(\n new Error(\n \"Summary input snapshots do not expose a search index database.\",\n ),\n );\n }\n\n public readBookMeta(): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public readCover(): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public readSummary(_serialId: number): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public readToc(): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public release(): Promise<void> {\n return Promise.resolve();\n }\n}\n\nclass EmptySnapshotMentionStore implements ReadonlyMentionStore {\n public getById(_mentionId: string): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public listAll(): Promise<MentionRecord[]> {\n return Promise.resolve([]);\n }\n\n public listByQid(_qid: string): Promise<MentionRecord[]> {\n return Promise.resolve([]);\n }\n\n public listBySurfaceTerms(\n _terms: readonly string[],\n ): Promise<MentionRecord[]> {\n return Promise.resolve([]);\n }\n\n public listBySurfaces(\n _surfaces: readonly string[],\n ): Promise<MentionRecord[]> {\n return Promise.resolve([]);\n }\n\n public listByChapter(_chapterId: number): Promise<MentionRecord[]> {\n return Promise.resolve([]);\n }\n}\n\nclass EmptySnapshotMentionLinkStore implements ReadonlyMentionLinkStore {\n public getById(_linkId: string): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n\n public listByTriple(_input: {\n readonly objectQid: string;\n readonly predicate: string;\n readonly subjectQid: string;\n }): Promise<MentionLinkRecord[]> {\n return Promise.resolve([]);\n }\n\n public listByChapter(_chapterId: number): Promise<MentionLinkRecord[]> {\n return Promise.resolve([]);\n }\n}\n\nclass EmptySnapshotObjectMetadataStore implements ReadonlyObjectMetadataStore {\n public getMap(\n _objectPath: string,\n ): Promise<Readonly<Record<string, unknown>>> {\n return Promise.resolve({});\n }\n}\n\nclass EmptySnapshotGraphBuildParameterStore implements ReadonlyGraphBuildParameterStore {\n public getByHash(_hash: string): Promise<undefined> {\n return Promise.resolve(undefined);\n }\n}\n\nclass SnapshotSerialStore implements ReadonlySerialStore {\n readonly #serial: SerialRecord;\n\n public constructor(serial: SerialRecord) {\n this.#serial = serial;\n }\n\n public getById(serialId: number): Promise<SerialRecord | undefined> {\n return Promise.resolve(\n serialId === this.#serial.id ? this.#serial : undefined,\n );\n }\n\n public getRevision(serialId: number): Promise<number> {\n return Promise.resolve(\n serialId === this.#serial.id ? this.#serial.revision : 0,\n );\n }\n\n public getRevisions(\n serialIds: readonly number[],\n ): Promise<ReadonlyMap<number, number>> {\n return Promise.resolve(\n new Map(\n serialIds\n .filter((serialId) => serialId === this.#serial.id)\n .map((serialId) => [serialId, this.#serial.revision] as const),\n ),\n );\n }\n\n public getChaptersRevision(): Promise<number> {\n return Promise.resolve(this.#serial.revision);\n }\n\n public getMaxId(): Promise<number> {\n return Promise.resolve(this.#serial.id);\n }\n\n public listIds(): Promise<number[]> {\n return Promise.resolve([this.#serial.id]);\n }\n\n public listDocumentOrders(): Promise<ReadonlyMap<number, number>> {\n return Promise.resolve(\n new Map([[this.#serial.id, this.#serial.documentOrder]]),\n );\n }\n}\n\nclass SnapshotSerialFragments implements ReadonlySerialFragments {\n public readonly path = \"\";\n public readonly serialId: number;\n readonly #fragmentsById: Map<number, FragmentRecord>;\n\n public constructor(serialId: number, fragments: readonly FragmentRecord[]) {\n this.serialId = serialId;\n this.#fragmentsById = new Map(\n fragments\n .filter((fragment) => fragment.serialId === serialId)\n .map((fragment) => [fragment.fragmentId, fragment]),\n );\n }\n\n public getFragment(fragmentId: number): Promise<FragmentRecord> {\n const fragment = this.#fragmentsById.get(fragmentId);\n\n if (fragment === undefined) {\n throw new Error(`Fragment ${fragmentId} does not exist`);\n }\n\n return Promise.resolve(fragment);\n }\n\n public listFragmentIds(): Promise<readonly number[]> {\n return Promise.resolve([...this.#fragmentsById.keys()].sort(compareNumber));\n }\n\n public async getSentence(sentenceIndex: number) {\n const sentence = (await this.listSentences())[sentenceIndex];\n\n if (sentence === undefined) {\n throw new RangeError(`Sentence ${sentenceIndex} does not exist`);\n }\n\n return sentence;\n }\n\n public async listSentencesInRange(\n startSentenceIndex: number,\n endSentenceIndex: number,\n ) {\n return (await this.listSentences()).slice(\n startSentenceIndex,\n endSentenceIndex + 1,\n );\n }\n\n public async listSentences() {\n const fragments = await Promise.all(\n (await this.listFragmentIds()).map(\n async (fragmentId) => await this.getFragment(fragmentId),\n ),\n );\n\n return fragments.flatMap((fragment) => fragment.sentences);\n }\n\n public async readText(): Promise<string | undefined> {\n const sentences = await this.listSentences();\n\n if (sentences.length === 0) {\n return undefined;\n }\n\n return sentences.map((sentence) => sentence.text).join(\"\");\n }\n}\n\nclass SnapshotChunkStore implements ReadonlyChunkStore {\n readonly #chunks: readonly ChunkRecord[];\n readonly #chunksById: Map<number, ChunkRecord>;\n readonly #fragmentStartIndexesBySerialId: ReadonlyMap<\n number,\n readonly number[]\n >;\n\n public constructor(\n chunks: readonly ChunkRecord[],\n fragments: readonly FragmentRecord[],\n ) {\n this.#chunks = [...chunks].sort(compareChunkById);\n this.#chunksById = new Map(chunks.map((chunk) => [chunk.id, chunk]));\n this.#fragmentStartIndexesBySerialId =\n createFragmentStartIndexesBySerialId(fragments);\n }\n\n public getById(chunkId: number): Promise<ChunkRecord | undefined> {\n return Promise.resolve(this.#chunksById.get(chunkId));\n }\n\n public countAll(): Promise<number> {\n return Promise.resolve(this.#chunks.length);\n }\n\n public listAll(): Promise<ChunkRecord[]> {\n return Promise.resolve([...this.#chunks]);\n }\n\n public listBySentenceStartIndexes(\n serialId: number,\n sentenceStartIndexes: readonly number[],\n ): Promise<ChunkRecord[]> {\n const segmentRanges = createSegmentRanges(\n this.#fragmentStartIndexesBySerialId.get(serialId) ?? [],\n sentenceStartIndexes,\n );\n\n return Promise.resolve(\n this.#chunks.filter(\n (chunk) =>\n chunk.sentenceId[0] === serialId &&\n segmentRanges.some(\n (range) =>\n chunk.sentenceId[1] >= range.startSentenceIndex &&\n chunk.sentenceId[1] <= range.endSentenceIndex,\n ),\n ),\n );\n }\n\n public listBySentenceRange(\n serialId: number,\n startSentenceIndex: number,\n endSentenceIndex: number,\n ): Promise<ChunkRecord[]> {\n return Promise.resolve(\n this.#chunks.filter(\n (chunk) =>\n chunk.sentenceId[0] === serialId &&\n chunk.sentenceId[1] >= startSentenceIndex &&\n chunk.sentenceId[1] <= endSentenceIndex,\n ),\n );\n }\n\n public listBySerial(serialId: number): Promise<ChunkRecord[]> {\n return Promise.resolve(\n this.#chunks.filter((chunk) => chunk.sentenceId[0] === serialId),\n );\n }\n\n public getMaxId(): Promise<number> {\n return Promise.resolve(\n this.#chunks.reduce((maxId, chunk) => Math.max(maxId, chunk.id), 0),\n );\n }\n\n public listFragmentPairs(): Promise<\n ReadonlyArray<readonly [number, number]>\n > {\n const pairs = new Set<string>();\n\n for (const chunk of this.#chunks) {\n pairs.add(`${chunk.sentenceId[0]}:${chunk.sentenceId[1]}`);\n }\n\n return Promise.resolve(\n [...pairs]\n .map((pair) => pair.split(\":\").map(Number) as [number, number])\n .sort(comparePair)\n .map(([serialId, fragmentId]) => [serialId, fragmentId] as const),\n );\n }\n}\n\nclass SnapshotReadingEdgeStore implements ReadonlyReadingEdgeStore {\n readonly #edges: readonly ReadingEdgeRecord[];\n readonly #serialIdByChunkId: Map<number, number>;\n\n public constructor(\n edges: readonly ReadingEdgeRecord[],\n chunks: readonly ChunkRecord[],\n ) {\n this.#edges = [...edges].sort(compareReadingEdge);\n this.#serialIdByChunkId = new Map(\n chunks.map((chunk) => [chunk.id, chunk.sentenceId[0]]),\n );\n }\n\n public listAll(): Promise<ReadingEdgeRecord[]> {\n return Promise.resolve([...this.#edges]);\n }\n\n public countAll(): Promise<number> {\n return Promise.resolve(this.#edges.length);\n }\n\n public listBySerial(serialId: number): Promise<ReadingEdgeRecord[]> {\n return Promise.resolve(\n this.#edges.filter(\n (edge) =>\n this.#serialIdByChunkId.get(edge.fromId) === serialId &&\n this.#serialIdByChunkId.get(edge.toId) === serialId,\n ),\n );\n }\n\n public listIncoming(chunkId: number): Promise<ReadingEdgeRecord[]> {\n return Promise.resolve(this.#edges.filter((edge) => edge.toId === chunkId));\n }\n\n public listOutgoing(chunkId: number): Promise<ReadingEdgeRecord[]> {\n return Promise.resolve(\n this.#edges.filter((edge) => edge.fromId === chunkId),\n );\n }\n}\n\nclass SnapshotSnakeStore implements ReadonlySnakeStore {\n readonly #snakes: readonly SnakeRecord[];\n readonly #snakesById: Map<number, SnakeRecord>;\n\n public constructor(snakes: readonly SnakeRecord[]) {\n this.#snakes = [...snakes].sort(compareSnake);\n this.#snakesById = new Map(snakes.map((snake) => [snake.id, snake]));\n }\n\n public getById(snakeId: number): Promise<SnakeRecord | undefined> {\n return Promise.resolve(this.#snakesById.get(snakeId));\n }\n\n public listIdsByGroup(serialId: number, groupId: number): Promise<number[]> {\n return Promise.resolve(\n this.#snakes\n .filter(\n (snake) => snake.serialId === serialId && snake.groupId === groupId,\n )\n .map((snake) => snake.id)\n .sort(compareNumber),\n );\n }\n\n public listBySerial(serialId: number): Promise<SnakeRecord[]> {\n return Promise.resolve(\n this.#snakes.filter((snake) => snake.serialId === serialId),\n );\n }\n}\n\nclass SnapshotSnakeChunkStore implements ReadonlySnakeChunkStore {\n readonly #snakeChunks: readonly SnakeChunkRecord[];\n\n public constructor(snakeChunks: readonly SnakeChunkRecord[]) {\n this.#snakeChunks = [...snakeChunks].sort(compareSnakeChunk);\n }\n\n public listChunkIds(snakeId: number): Promise<number[]> {\n return Promise.resolve(\n this.#snakeChunks\n .filter((snakeChunk) => snakeChunk.snakeId === snakeId)\n .map((snakeChunk) => snakeChunk.chunkId),\n );\n }\n\n public listBySnake(snakeId: number): Promise<SnakeChunkRecord[]> {\n return Promise.resolve(\n this.#snakeChunks.filter((snakeChunk) => snakeChunk.snakeId === snakeId),\n );\n }\n}\n\nclass SnapshotSnakeEdgeStore implements ReadonlySnakeEdgeStore {\n readonly #edges: readonly SnakeEdgeRecord[];\n readonly #serialIdBySnakeId: Map<number, number>;\n\n public constructor(\n edges: readonly SnakeEdgeRecord[],\n snakes: readonly SnakeRecord[],\n ) {\n this.#edges = [...edges].sort(compareSnakeEdge);\n this.#serialIdBySnakeId = new Map(\n snakes.map((snake) => [snake.id, snake.serialId]),\n );\n }\n\n public listIncoming(snakeId: number): Promise<SnakeEdgeRecord[]> {\n return Promise.resolve(\n this.#edges.filter((edge) => edge.toSnakeId === snakeId),\n );\n }\n\n public listOutgoing(snakeId: number): Promise<SnakeEdgeRecord[]> {\n return Promise.resolve(\n this.#edges.filter((edge) => edge.fromSnakeId === snakeId),\n );\n }\n\n public listWithin(snakeIds: readonly number[]): Promise<SnakeEdgeRecord[]> {\n const snakeIdSet = new Set(snakeIds);\n\n return Promise.resolve(\n this.#edges.filter(\n (edge) =>\n snakeIdSet.has(edge.fromSnakeId) && snakeIdSet.has(edge.toSnakeId),\n ),\n );\n }\n\n public listBySerial(serialId: number): Promise<SnakeEdgeRecord[]> {\n return Promise.resolve(\n this.#edges.filter(\n (edge) =>\n this.#serialIdBySnakeId.get(edge.fromSnakeId) === serialId &&\n this.#serialIdBySnakeId.get(edge.toSnakeId) === serialId,\n ),\n );\n }\n}\n\nclass SnapshotFragmentGroupStore implements ReadonlyFragmentGroupStore {\n readonly #groups: readonly SentenceGroupRecord[];\n\n public constructor(groups: readonly SentenceGroupRecord[]) {\n this.#groups = [...groups].sort(compareFragmentGroup);\n }\n\n public listBySerial(serialId: number): Promise<SentenceGroupRecord[]> {\n return Promise.resolve(\n this.#groups.filter((group) => group.serialId === serialId),\n );\n }\n\n public listSerialIds(): Promise<number[]> {\n return Promise.resolve(\n [...new Set(this.#groups.map((group) => group.serialId))].sort(\n compareNumber,\n ),\n );\n }\n\n public listGroupIdsForSerial(serialId: number): Promise<number[]> {\n return Promise.resolve(\n [\n ...new Set(\n this.#groups\n .filter((group) => group.serialId === serialId)\n .map((group) => group.groupId),\n ),\n ].sort(compareNumber),\n );\n }\n}\n\nasync function copySnakes(\n sourceDocument: ReadonlyDocument,\n targetDocument: Document,\n serialId: number,\n chunkIdMap: ReadonlyMap<number, number>,\n): Promise<void> {\n const sourceSnakes = await sourceDocument.snakes.listBySerial(serialId);\n const snakeIdMap = new Map<number, number>();\n\n for (const sourceSnake of sourceSnakes) {\n const targetSnakeId = await targetDocument.snakes.create({\n firstLabel: sourceSnake.firstLabel,\n groupId: sourceSnake.groupId,\n lastLabel: sourceSnake.lastLabel,\n localSnakeId: sourceSnake.localSnakeId,\n serialId,\n size: sourceSnake.size,\n weight: sourceSnake.weight,\n wordsCount: sourceSnake.wordsCount,\n });\n\n snakeIdMap.set(sourceSnake.id, targetSnakeId);\n\n for (const snakeChunk of await sourceDocument.snakeChunks.listBySnake(\n sourceSnake.id,\n )) {\n const chunkId = chunkIdMap.get(snakeChunk.chunkId);\n\n if (chunkId === undefined) {\n continue;\n }\n\n await targetDocument.snakeChunks.save({\n chunkId,\n position: snakeChunk.position,\n snakeId: targetSnakeId,\n });\n }\n }\n\n for (const edge of await sourceDocument.snakeEdges.listBySerial(serialId)) {\n const fromSnakeId = snakeIdMap.get(edge.fromSnakeId);\n const toSnakeId = snakeIdMap.get(edge.toSnakeId);\n\n if (fromSnakeId === undefined || toSnakeId === undefined) {\n continue;\n }\n\n await targetDocument.snakeEdges.save({\n fromSnakeId,\n toSnakeId,\n weight: edge.weight,\n });\n }\n}\n\nasync function copyChunks(\n sourceDocument: ReadonlyDocument,\n targetDocument: Document,\n serialId: number,\n): Promise<ReadonlyMap<number, number>> {\n const chunkIdMap = new Map<number, number>();\n\n for (const chunk of await sourceDocument.chunks.listBySerial(serialId)) {\n const createdChunk = await targetDocument.chunks.create({\n content: chunk.content,\n generation: chunk.generation,\n label: chunk.label,\n sentenceId: chunk.sentenceId,\n sentenceIds: chunk.sentenceIds,\n weight: chunk.weight,\n wordsCount: chunk.wordsCount,\n ...(chunk.importance === undefined\n ? {}\n : { importance: chunk.importance }),\n ...(chunk.retention === undefined ? {} : { retention: chunk.retention }),\n });\n\n chunkIdMap.set(chunk.id, createdChunk.id);\n }\n\n return chunkIdMap;\n}\n\nasync function requireStage(\n document: ReadonlyDocument,\n chapterId: number,\n stage: ChapterDetails[\"stage\"],\n): Promise<void> {\n const details = await getChapterDetails(document, chapterId);\n\n if (details.stage !== stage) {\n throw new Error(\n `Chapter ${chapterId} is ${details.stage}. Expected ${stage} before committing build output.`,\n );\n }\n}\n\nfunction createTopologyOptions(\n options: Pick<\n BuildSerialTopologyOptions,\n \"extractionPrompt\" | \"userLanguage\"\n >,\n): BuildSerialTopologyOptions {\n return {\n extractionPrompt: options.extractionPrompt,\n ...(options.userLanguage === undefined\n ? {}\n : { userLanguage: options.userLanguage }),\n };\n}\n\nfunction createGraphBuildParameterInput(\n options: Pick<\n BuildSerialTopologyOptions,\n \"extractionPrompt\" | \"userLanguage\"\n >,\n): GraphBuildParameterInput {\n const language = normalizeLanguageCode(options.userLanguage);\n\n return {\n ...(language === undefined ? {} : { language }),\n prompt: options.extractionPrompt,\n };\n}\n\nasync function* readChapterSource(\n document: ReadonlyDocument,\n chapterId: number,\n): ReaderTextStream {\n const fragments = document.getSerialFragments(chapterId);\n\n for (const fragmentId of await fragments.listFragmentIds()) {\n const fragment = await fragments.getFragment(fragmentId);\n\n for (const sentence of fragment.sentences) {\n yield sentence.text;\n }\n }\n}\n\nasync function collectReaderText(\n stream: ReaderTextStream,\n): Promise<readonly string[]> {\n const text: string[] = [];\n\n for await (const chunk of stream) {\n text.push(chunk);\n }\n\n return text;\n}\n\nfunction compareNumber(left: number, right: number): number {\n return left - right;\n}\n\nfunction createFragmentStartIndexesBySerialId(\n fragments: readonly FragmentRecord[],\n): ReadonlyMap<number, readonly number[]> {\n const indexesBySerialId = new Map<number, number[]>();\n\n for (const fragment of fragments) {\n const indexes = indexesBySerialId.get(fragment.serialId) ?? [];\n\n indexes.push(fragment.fragmentId);\n indexesBySerialId.set(fragment.serialId, indexes);\n }\n\n return new Map(\n [...indexesBySerialId.entries()].map(\n ([serialId, indexes]) => [serialId, indexes.sort(compareNumber)] as const,\n ),\n );\n}\n\nfunction createSegmentRanges(\n allStartIndexes: readonly number[],\n selectedStartIndexes: readonly number[],\n): Array<{\n readonly endSentenceIndex: number;\n readonly startSentenceIndex: number;\n}> {\n const selected = new Set(selectedStartIndexes);\n\n return allStartIndexes.flatMap((startSentenceIndex, index) => {\n if (!selected.has(startSentenceIndex)) {\n return [];\n }\n\n const nextStartSentenceIndex = allStartIndexes[index + 1];\n\n return [\n {\n endSentenceIndex:\n nextStartSentenceIndex === undefined\n ? Infinity\n : nextStartSentenceIndex - 1,\n startSentenceIndex,\n },\n ];\n });\n}\n\nfunction compareChunkById(left: ChunkRecord, right: ChunkRecord): number {\n return left.id - right.id;\n}\n\nfunction compareFragmentGroup(\n left: SentenceGroupRecord,\n right: SentenceGroupRecord,\n): number {\n return (\n left.serialId - right.serialId ||\n left.groupId - right.groupId ||\n left.startSentenceIndex - right.startSentenceIndex ||\n left.endSentenceIndex - right.endSentenceIndex\n );\n}\n\nfunction compareReadingEdge(\n left: ReadingEdgeRecord,\n right: ReadingEdgeRecord,\n): number {\n return left.fromId - right.fromId || left.toId - right.toId;\n}\n\nfunction comparePair(\n left: readonly [number, number],\n right: readonly [number, number],\n): number {\n return left[0] - right[0] || left[1] - right[1];\n}\n\nfunction compareSnake(left: SnakeRecord, right: SnakeRecord): number {\n return left.groupId - right.groupId || left.id - right.id;\n}\n\nfunction compareSnakeChunk(\n left: SnakeChunkRecord,\n right: SnakeChunkRecord,\n): number {\n return left.snakeId - right.snakeId || left.position - right.position;\n}\n\nfunction compareSnakeEdge(\n left: SnakeEdgeRecord,\n right: SnakeEdgeRecord,\n): number {\n return (\n left.fromSnakeId - right.fromSnakeId || left.toSnakeId - right.toSnakeId\n );\n}\n","import type { DisambiguationProfile } from \"../wikipage/index.js\";\n\nimport type { WikimatchCandidate, WikimatchQidOption } from \"./types.js\";\n\nexport function countWikimatchCandidateOptions(\n candidate: WikimatchCandidate,\n): number {\n return candidate.qidOptions.reduce(\n (total, option) => total + countWikimatchQidOption(option),\n 0,\n );\n}\n\nexport function countWikimatchQidOption(option: WikimatchQidOption): number {\n if (option.disambiguation === undefined) {\n return 1;\n }\n\n return listSelectableQids(option).length;\n}\n\nexport function listCandidateSelectableQids(\n candidate: WikimatchCandidate,\n): readonly string[] {\n return [\n ...new Set(\n candidate.qidOptions.flatMap((option) => listSelectableQids(option)),\n ),\n ];\n}\n\nexport function filterCandidateQidOptions(\n candidate: WikimatchCandidate,\n allowedQids: ReadonlySet<string>,\n): WikimatchCandidate {\n return {\n ...candidate,\n qidOptions: candidate.qidOptions.flatMap((option) => {\n const filtered = filterQidOption(option, allowedQids);\n\n return filtered === undefined ? [] : [filtered];\n }),\n };\n}\n\nexport function splitCandidateByOptionBudget(\n candidate: WikimatchCandidate,\n optionBudget: number,\n): readonly WikimatchCandidate[] {\n if (!Number.isFinite(optionBudget) || optionBudget <= 0) {\n throw new Error(\"Wikimatch option budget must be positive.\");\n }\n\n const chunks: WikimatchCandidate[] = [];\n let pendingOptions: WikimatchQidOption[] = [];\n let pendingCost = 0;\n\n for (const option of candidate.qidOptions) {\n const optionChunks = splitQidOptionByBudget(option, optionBudget);\n\n for (const optionChunk of optionChunks) {\n const cost = countWikimatchQidOption(optionChunk);\n\n if (pendingOptions.length > 0 && pendingCost + cost > optionBudget) {\n chunks.push({\n ...candidate,\n qidOptions: pendingOptions,\n });\n pendingOptions = [];\n pendingCost = 0;\n }\n\n pendingOptions.push(optionChunk);\n pendingCost += cost;\n }\n }\n\n if (pendingOptions.length > 0) {\n chunks.push({\n ...candidate,\n qidOptions: pendingOptions,\n });\n }\n\n return chunks;\n}\n\nexport function sliceCandidateByOptionBudget(\n candidate: WikimatchCandidate,\n optionBudget: number,\n offset: number,\n): {\n readonly candidate: WikimatchCandidate;\n readonly nextOffset?: number;\n} {\n if (!Number.isFinite(optionBudget) || optionBudget <= 0) {\n throw new Error(\"Wikimatch option budget must be positive.\");\n }\n if (!Number.isInteger(offset) || offset < 0) {\n throw new Error(\"Wikimatch option offset must be a non-negative integer.\");\n }\n\n const selectedQids = listCandidateSelectableQids(candidate).slice(\n offset,\n offset + optionBudget,\n );\n const filtered = filterCandidateQidOptions(candidate, new Set(selectedQids));\n const nextOffset =\n offset + selectedQids.length < listCandidateSelectableQids(candidate).length\n ? offset + selectedQids.length\n : undefined;\n\n return {\n candidate: {\n ...filtered,\n ...(nextOffset === undefined ? {} : { hasMoreOptions: true }),\n },\n ...(nextOffset === undefined ? {} : { nextOffset }),\n };\n}\n\nfunction splitQidOptionByBudget(\n option: WikimatchQidOption,\n optionBudget: number,\n): readonly WikimatchQidOption[] {\n if (option.disambiguation === undefined) {\n return [option];\n }\n\n const selectableQids = listSelectableQids(option);\n\n if (selectableQids.length <= optionBudget) {\n return [option];\n }\n\n const chunks: WikimatchQidOption[] = [];\n\n for (let index = 0; index < selectableQids.length; index += optionBudget) {\n const qids = new Set(selectableQids.slice(index, index + optionBudget));\n const filtered = filterQidOption(option, qids);\n\n if (filtered !== undefined) {\n chunks.push(filtered);\n }\n }\n\n return chunks;\n}\n\nfunction filterQidOption(\n option: WikimatchQidOption,\n allowedQids: ReadonlySet<string>,\n): WikimatchQidOption | undefined {\n if (option.disambiguation === undefined) {\n return allowedQids.has(option.qid) ? option : undefined;\n }\n\n const linkedQids = option.disambiguation.linkedQids.filter((item) =>\n allowedQids.has(item.qid),\n );\n const profile = filterDisambiguationProfile(\n option.disambiguation.profile,\n allowedQids,\n );\n const hasProfileMeanings = (profile?.meanings.length ?? 0) > 0;\n\n if (linkedQids.length === 0 && !hasProfileMeanings) {\n return undefined;\n }\n\n return {\n ...option,\n disambiguation: {\n ...option.disambiguation,\n linkedQids,\n ...(profile === undefined ? {} : { profile }),\n },\n };\n}\n\nfunction filterDisambiguationProfile(\n profile: DisambiguationProfile | undefined,\n allowedQids: ReadonlySet<string>,\n): DisambiguationProfile | undefined {\n if (profile === undefined) {\n return undefined;\n }\n\n return {\n ...profile,\n meanings: profile.meanings.filter((meaning) =>\n allowedQids.has(meaning.qid),\n ),\n };\n}\n\nfunction listSelectableQids(option: WikimatchQidOption): readonly string[] {\n if (option.disambiguation === undefined) {\n return [option.qid];\n }\n\n return [\n ...new Set([\n ...(option.disambiguation.profile?.meanings.map(\n (meaning) => meaning.qid,\n ) ?? []),\n ...option.disambiguation.linkedQids.map((item) => item.qid),\n ]),\n ];\n}\n","import { z } from \"zod\";\n\nimport {\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n type GuaranteedRequest,\n} from \"../guaranteed/index.js\";\nimport type { LLMessage } from \"../llm/index.js\";\n\nimport { listCandidateSelectableQids } from \"./options.js\";\nimport type {\n WikimatchAcceptedMention,\n WikimatchCandidate,\n WikimatchConflictGroup,\n WikimatchPolicyContinuation,\n WikimatchQidOption,\n WikimatchPolicyDecisionOutput,\n WikimatchPolicyJudgeInput,\n WikimatchPolicyJudgeResult,\n WikimatchPolicyResponse,\n WikimatchPolicyUpdate,\n} from \"./types.js\";\n\nconst policyDecisionSchema = z\n .object({\n candidateId: z.string().min(1),\n decision: z.enum([\"continue\", \"recall\", \"skip_this_time\", \"never_recall\"]),\n qid: z.string().nullable().optional(),\n })\n .strict();\n\nconst policyGroupSchema = z\n .object({\n decisions: z.array(policyDecisionSchema),\n groupId: z.string().min(1),\n })\n .strict();\n\nconst policyResponseSchema = z\n .object({\n groups: z.array(policyGroupSchema),\n })\n .strict();\n\nexport interface JudgeWikimatchPolicyOptions extends WikimatchPolicyJudgeInput {\n readonly maxRetries?: number;\n readonly request: GuaranteedRequest;\n}\n\nexport async function judgeWikimatchPolicy(\n options: JudgeWikimatchPolicyOptions,\n): Promise<WikimatchPolicyJudgeResult> {\n try {\n return await requestGuaranteedJson({\n messages: buildPolicyMessages(options),\n parse: (response) =>\n parsePolicyResponse(\n options.candidates,\n normalizePolicyResponse(response),\n options.window.groups,\n ),\n request: options.request,\n responseIntentClassifierPrompt:\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n schema: policyResponseSchema,\n ...(options.maxRetries === undefined\n ? {}\n : { maxRetries: options.maxRetries }),\n });\n } catch (error) {\n return {\n continuations: [],\n fallback: {\n issues: [formatFallbackIssue(error)],\n reason: \"guaranteed_json_failed\",\n },\n mentions: [],\n policyUpdates: [],\n };\n }\n}\n\nfunction normalizePolicyResponse(\n response: z.infer<typeof policyResponseSchema>,\n): WikimatchPolicyResponse {\n return {\n groups: response.groups.map((group) => ({\n decisions: group.decisions.map((decision) => ({\n candidateId: decision.candidateId,\n decision: decision.decision,\n ...normalizeDecisionQid(decision.qid),\n })),\n groupId: group.groupId,\n })),\n };\n}\n\nfunction normalizeDecisionQid(qid: string | null | undefined): {\n readonly qid?: string;\n} {\n const normalized = qid?.trim();\n\n return normalized === undefined || normalized === \"\"\n ? {}\n : { qid: normalized };\n}\n\nexport function parsePolicyResponse(\n candidates: readonly WikimatchCandidate[],\n response: WikimatchPolicyResponse,\n groups: readonly WikimatchConflictGroup[] = inferGroups(candidates),\n): WikimatchPolicyJudgeResult {\n const issues = validatePolicyResponse(candidates, response, groups);\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n const candidatesById = createCandidateMap(candidates);\n const continuations: WikimatchPolicyContinuation[] = [];\n const mentions: WikimatchAcceptedMention[] = [];\n const policyUpdates: WikimatchPolicyUpdate[] = [];\n\n for (const group of response.groups) {\n const continuedCandidateIds: string[] = [];\n\n for (const decision of group.decisions) {\n const candidate = candidatesById.get(decision.candidateId)!;\n\n if (decision.decision === \"continue\") {\n if (candidate.hasMoreOptions !== true) {\n policyUpdates.push({\n candidateId: candidate.id,\n decision: \"skip_this_time\",\n surface: candidate.surface,\n });\n continue;\n }\n continuedCandidateIds.push(candidate.id);\n continue;\n }\n\n if (decision.decision === \"recall\") {\n mentions.push({\n candidateId: candidate.id,\n qid: decision.qid!,\n range: candidate.range,\n surface: candidate.surface,\n });\n continue;\n }\n\n policyUpdates.push({\n candidateId: candidate.id,\n decision: decision.decision,\n ...(decision.qid === undefined ? {} : { qid: decision.qid }),\n surface: candidate.surface,\n });\n }\n\n if (continuedCandidateIds.length > 0) {\n continuations.push({\n candidateIds: continuedCandidateIds,\n groupId: group.groupId,\n });\n }\n }\n\n return {\n continuations,\n mentions,\n policyUpdates,\n };\n}\n\nexport function validatePolicyResponse(\n candidates: readonly WikimatchCandidate[],\n response: WikimatchPolicyResponse,\n groups: readonly WikimatchConflictGroup[] = inferGroups(candidates),\n): readonly string[] {\n const issues: string[] = [];\n const candidatesById = createCandidateMap(candidates);\n const groupsById = createGroupMap(groups);\n const seenGroupIds = new Set<string>();\n const seenCandidateIds = new Set<string>();\n const recalled: Array<{\n readonly candidate: WikimatchCandidate;\n readonly decision: WikimatchPolicyDecisionOutput;\n }> = [];\n\n for (const group of response.groups) {\n const expectedCandidateIds = groupsById.get(group.groupId);\n\n if (expectedCandidateIds === undefined) {\n issues.push(\n `Unknown groupId \"${group.groupId}\". Use exactly these group IDs: ${[\n ...groupsById.keys(),\n ].join(\", \")}.`,\n );\n continue;\n }\n if (seenGroupIds.has(group.groupId)) {\n issues.push(`Duplicate group result for ${group.groupId}.`);\n continue;\n }\n seenGroupIds.add(group.groupId);\n\n for (const decision of group.decisions) {\n const candidate = candidatesById.get(decision.candidateId);\n\n if (candidate === undefined) {\n issues.push(\n `Unknown candidateId \"${decision.candidateId}\". Use only one of: ${[\n ...candidatesById.keys(),\n ].join(\", \")}.`,\n );\n continue;\n }\n if (!expectedCandidateIds.has(decision.candidateId)) {\n issues.push(\n `Candidate ${decision.candidateId} does not belong to group ${group.groupId}. Use only candidates from that group: ${[\n ...expectedCandidateIds,\n ].join(\", \")}.`,\n );\n continue;\n }\n if (seenCandidateIds.has(decision.candidateId)) {\n issues.push(\n `Duplicate decision for candidate ${decision.candidateId}. Return exactly one decision per candidate at most.`,\n );\n continue;\n }\n\n seenCandidateIds.add(decision.candidateId);\n\n if (decision.decision === \"recall\") {\n validateRecallDecision(issues, candidate, decision);\n recalled.push({ candidate, decision });\n continue;\n }\n\n if (decision.decision === \"continue\") {\n if (decision.qid !== undefined) {\n issues.push(\n `Candidate ${candidate.id} uses decision \"continue\" but includes qid. Continue means this candidate page has no final choice yet.`,\n );\n }\n continue;\n }\n\n if (decision.qid !== undefined) {\n issues.push(\n `Candidate ${candidate.id} uses decision \"${decision.decision}\" but includes qid. Include qid only when decision is \"recall\".`,\n );\n }\n }\n }\n\n for (const groupId of groupsById.keys()) {\n if (!seenGroupIds.has(groupId)) {\n issues.push(\n `Missing group result for ${groupId}. Return exactly one result for every input group; use decisions: [] when nothing should be recalled.`,\n );\n }\n }\n\n for (let leftIndex = 0; leftIndex < recalled.length; leftIndex += 1) {\n for (\n let rightIndex = leftIndex + 1;\n rightIndex < recalled.length;\n rightIndex += 1\n ) {\n const left = recalled[leftIndex]!;\n const right = recalled[rightIndex]!;\n\n if (rangesOverlap(left.candidate.range, right.candidate.range)) {\n issues.push(\n `Decision conflict: candidates ${left.candidate.id} and ${right.candidate.id} both use decision \"recall\", but their ranges overlap.\\n` +\n `- ${left.candidate.id}: \"${left.candidate.surface}\" [${left.candidate.range.start}, ${left.candidate.range.end})\\n` +\n `- ${right.candidate.id}: \"${right.candidate.surface}\" [${right.candidate.range.start}, ${right.candidate.range.end})\\n` +\n \"Only one recalled mention may occupy an overlapping text range. Choose the most specific valid mention, or mark the others as skip_this_time / never_recall.\",\n );\n }\n }\n }\n\n return issues;\n}\n\nfunction validateRecallDecision(\n issues: string[],\n candidate: WikimatchCandidate,\n decision: WikimatchPolicyDecisionOutput,\n): void {\n if (decision.qid === undefined) {\n issues.push(\n `Candidate ${candidate.id} uses decision \"recall\" but does not provide qid. A recalled mention must choose one concrete QID from the candidate options.`,\n );\n return;\n }\n if (isDisambiguationQid(candidate, decision.qid)) {\n issues.push(\n `Candidate ${candidate.id} selected source disambiguation QID ${decision.qid}. Source disambiguation QIDs are hidden behind DIS references and cannot be final mention groundings. Choose a QID from entityOptions or disambiguationOptions.meanings instead.`,\n );\n return;\n }\n if (!isAllowedQid(candidate, decision.qid)) {\n issues.push(formatIllegalQidIssue(candidate, decision.qid));\n }\n}\n\nfunction isAllowedQid(candidate: WikimatchCandidate, qid: string): boolean {\n return listAllowedQids(candidate).includes(qid);\n}\n\nfunction isDisambiguationQid(\n candidate: WikimatchCandidate,\n qid: string,\n): boolean {\n return candidate.qidOptions.some(\n (option) => option.disambiguation !== undefined && option.qid === qid,\n );\n}\n\nfunction listAllowedQids(candidate: WikimatchCandidate): readonly string[] {\n return listCandidateSelectableQids(candidate);\n}\n\nfunction formatIllegalQidIssue(\n candidate: WikimatchCandidate,\n qid: string,\n): string {\n return (\n `Candidate ${candidate.id} selected qid ${qid}, but that QID is not available for this candidate. ` +\n `Allowed QIDs for \"${candidate.surface}\" are: ${listAllowedQids(candidate).join(\", \")}.`\n );\n}\n\nfunction rangesOverlap(\n left: { readonly end: number; readonly start: number },\n right: { readonly end: number; readonly start: number },\n): boolean {\n return left.start < right.end && right.start < left.end;\n}\n\nfunction createCandidateMap(\n candidates: readonly WikimatchCandidate[],\n): ReadonlyMap<string, WikimatchCandidate> {\n return new Map(candidates.map((candidate) => [candidate.id, candidate]));\n}\n\nfunction createGroupMap(\n groups: readonly WikimatchConflictGroup[],\n): ReadonlyMap<string, ReadonlySet<string>> {\n return new Map(\n groups.map((group) => [group.id, new Set(group.candidateIds)]),\n );\n}\n\nfunction inferGroups(\n candidates: readonly WikimatchCandidate[],\n): readonly WikimatchConflictGroup[] {\n if (candidates.length === 0) {\n return [];\n }\n\n return [\n {\n candidateIds: candidates.map((candidate) => candidate.id),\n id: \"g1\",\n range: {\n end: Math.max(...candidates.map((candidate) => candidate.range.end)),\n start: Math.min(\n ...candidates.map((candidate) => candidate.range.start),\n ),\n },\n },\n ];\n}\n\nfunction buildPolicyMessages(input: WikimatchPolicyJudgeInput): LLMessage[] {\n return [\n {\n role: \"system\",\n content: formatPolicySystemPrompt(input),\n },\n {\n role: \"user\",\n content: formatPolicyPrompt(input),\n },\n ];\n}\n\nfunction formatPolicySystemPrompt(input: WikimatchPolicyJudgeInput): string {\n return [\n \"You judge which precomputed Wikidata mention candidates should be recalled.\",\n \"\",\n \"Recall policy:\",\n input.policyPrompt,\n \"\",\n \"Rules:\",\n \"- Return JSON only.\",\n \"- Return exactly one group result for every input group.\",\n \"- Use decisions: [] only when this group has no selected mention and no policy update.\",\n \"- First judge whether each candidate mention is worth recalling in this exact tagged context, before choosing among QID candidates.\",\n '- If the mention is not worth recalling in context, use \"never_recall\" immediately; do not inspect later pages just because more QIDs might exist.',\n '- Use \"never_recall\" when this exact candidate span is not a standalone knowledge graph object in context.',\n \"- Candidate lists may be incomplete when a candidate has more pages.\",\n '- Use decision \"continue\" only after the mention has passed the context-worthiness test and the current candidate page has no good QID but more pages are available.',\n '- \"continue\" is not a rejection. It asks to inspect the next candidate page for that candidate.',\n '- If a surface should be recalled but the current incomplete page lacks a suitable QID, use \"continue\" instead of skip_this_time.',\n '- Do not use \"continue\" for single-character surnames, abbreviations, side labels, generic words, fragments, roles, quantities, attributes, or discourse words that are not useful standalone knowledge graph objects in context.',\n \"- Do not invent candidates, ranges, surfaces, or QIDs.\",\n \"- A recalled mention must choose a QID from entityOptions or disambiguationOptions.meanings.\",\n '- Include qid only when decision is \"recall\"; do not include qid for continue, skip_this_time, or never_recall.',\n \"- Never return DIS identifiers as qid values; DIS identifiers are only disambiguation references.\",\n \"- Overlapping recalled ranges are illegal; choose the most specific valid mention.\",\n ].join(\"\\n\");\n}\n\nfunction formatPolicyPrompt(input: WikimatchPolicyJudgeInput): string {\n return [\n \"Tagged context:\",\n formatTaggedContext(input),\n \"\",\n \"Candidate groups:\",\n formatGroupsForPrompt(input)\n .map((group) => JSON.stringify(group))\n .join(\"\\n\"),\n \"\",\n \"Return this JSON shape:\",\n JSON.stringify({\n groups: [\n {\n decisions: [\n {\n candidateId: \"candidate id from this group\",\n decision: \"recall | continue | skip_this_time | never_recall\",\n qid: \"required only when decision is recall\",\n },\n ],\n groupId: \"group id from the input\",\n },\n ],\n }),\n ].join(\"\\n\");\n}\n\nfunction formatTaggedContext(input: WikimatchPolicyJudgeInput): string {\n const groups = [...input.window.groups].sort(\n (left, right) => left.range.start - right.range.start,\n );\n const parts: string[] = [];\n let cursor = input.window.baseOffset;\n\n for (const group of groups) {\n parts.push(\n input.window.text.slice(\n cursor - input.window.baseOffset,\n group.range.start - input.window.baseOffset,\n ),\n );\n parts.push(\n `<group id=\"${escapeXmlAttribute(group.id)}\">${escapeXmlText(\n input.window.text.slice(\n group.range.start - input.window.baseOffset,\n group.range.end - input.window.baseOffset,\n ),\n )}</group>`,\n );\n cursor = group.range.end;\n }\n\n parts.push(input.window.text.slice(cursor - input.window.baseOffset));\n\n return parts.join(\"\");\n}\n\nfunction formatGroupsForPrompt(input: WikimatchPolicyJudgeInput): object[] {\n const candidatesById = createCandidateMap(input.window.candidates);\n\n return input.window.groups.map((group) => ({\n candidates: group.candidateIds.flatMap((candidateId) => {\n const candidate = candidatesById.get(candidateId);\n\n return candidate === undefined\n ? []\n : [formatCandidateForPrompt(candidate)];\n }),\n groupId: group.id,\n }));\n}\n\nexport function formatCandidateForPrompt(\n candidate: WikimatchCandidate,\n _group?: WikimatchConflictGroup,\n): object {\n const formattedOptions = formatQidOptions(candidate.qidOptions);\n\n return {\n candidateId: candidate.id,\n ...(candidate.hasMoreOptions === true ? { hasMoreOptions: true } : {}),\n ...(formattedOptions.disambiguationOptions.length === 0\n ? {}\n : { disambiguationOptions: formattedOptions.disambiguationOptions }),\n ...(formattedOptions.entityOptions.length === 0\n ? {}\n : { entityOptions: formattedOptions.entityOptions }),\n };\n}\n\nfunction formatQidOptions(options: readonly WikimatchQidOption[]): {\n readonly disambiguationOptions: readonly object[];\n readonly entityOptions: readonly object[];\n} {\n const disambiguationOptions: object[] = [];\n const entityOptions: object[] = [];\n\n for (const option of options) {\n if (option.disambiguation !== undefined) {\n disambiguationOptions.push({\n ...(option.label === undefined ? {} : { label: option.label }),\n meanings:\n option.disambiguation.profile?.meanings.map(formatMeaningForPrompt) ??\n option.disambiguation.linkedQids.map((item) => ({\n name: item.title,\n qid: item.qid,\n })),\n });\n continue;\n }\n\n entityOptions.push({\n ...(option.description === undefined\n ? {}\n : { description: option.description }),\n ...(option.label === undefined ? {} : { label: option.label }),\n qid: option.qid,\n });\n }\n\n return {\n disambiguationOptions,\n entityOptions,\n };\n}\n\nfunction formatMeaningForPrompt(\n meaning: NonNullable<\n NonNullable<WikimatchQidOption[\"disambiguation\"]>[\"profile\"]\n >[\"meanings\"][number],\n): object {\n return {\n ...(meaning.information === \"\" ? {} : { information: meaning.information }),\n name: meaning.name,\n qid: meaning.qid,\n };\n}\n\nfunction escapeXmlAttribute(value: string): string {\n return escapeXmlText(value).replaceAll('\"', \""\");\n}\n\nfunction escapeXmlText(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\");\n}\n\nfunction formatFallbackIssue(error: unknown): string {\n if (error instanceof ParsedJsonError) {\n return error.issues.join(\"\\n\");\n }\n if (error instanceof Error) {\n const issues = (error as { readonly issues?: unknown }).issues;\n\n if (\n Array.isArray(issues) &&\n issues.every((issue) => typeof issue === \"string\")\n ) {\n return issues.join(\"\\n\");\n }\n\n return error.message;\n }\n\n return String(error);\n}\n","import { z } from \"zod\";\n\nimport {\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n type GuaranteedRequest,\n} from \"../guaranteed/index.js\";\nimport type { LLMessage } from \"../llm/index.js\";\n\nimport {\n filterCandidateQidOptions,\n listCandidateSelectableQids,\n splitCandidateByOptionBudget,\n} from \"./options.js\";\nimport { formatCandidateForPrompt } from \"./policy-judge.js\";\nimport type {\n NarrowWikimatchCandidateOptionsInput,\n WikimatchCandidate,\n WikimatchCandidateOptionNarrowingResponse,\n WikimatchCandidateOptionNarrowingResult,\n} from \"./types.js\";\n\nconst narrowingItemSchema = z\n .object({\n decision: z.enum([\"keep\", \"reject\"]),\n qid: z.string().regex(/^Q[1-9][0-9]*$/),\n })\n .strict();\n\nconst narrowingResponseSchema = z\n .object({\n qids: z.array(narrowingItemSchema),\n })\n .strict();\n\nexport interface NarrowWikimatchCandidateOptionsOptions extends NarrowWikimatchCandidateOptionsInput {\n readonly maxRetries?: number;\n readonly optionBudget: number;\n readonly request: GuaranteedRequest;\n}\n\nexport async function narrowWikimatchCandidateOptions(\n options: NarrowWikimatchCandidateOptionsOptions,\n): Promise<WikimatchCandidateOptionNarrowingResult> {\n const keptQids = new Set<string>();\n const issues: string[] = [];\n\n for (const chunk of splitCandidateByOptionBudget(\n options.candidate,\n options.optionBudget,\n )) {\n const result = await requestNarrowedChunk({\n ...options,\n candidate: chunk,\n });\n\n if (result.fallback !== undefined) {\n issues.push(...result.fallback.issues);\n continue;\n }\n\n for (const qid of listCandidateSelectableQids(result.candidate)) {\n keptQids.add(qid);\n }\n }\n\n if (issues.length > 0) {\n return {\n candidate: {\n ...options.candidate,\n qidOptions: [],\n },\n fallback: {\n issues,\n reason: \"guaranteed_json_failed\",\n },\n };\n }\n\n return {\n candidate: filterCandidateQidOptions(options.candidate, keptQids),\n };\n}\n\nasync function requestNarrowedChunk(\n options: NarrowWikimatchCandidateOptionsOptions,\n): Promise<WikimatchCandidateOptionNarrowingResult> {\n try {\n return await requestGuaranteedJson({\n messages: buildNarrowingMessages(options),\n parse: (response) =>\n parseNarrowingResponse(\n options.candidate,\n normalizeNarrowingResponse(response),\n ),\n request: options.request,\n responseIntentClassifierPrompt:\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n schema: narrowingResponseSchema,\n ...(options.maxRetries === undefined\n ? {}\n : { maxRetries: options.maxRetries }),\n });\n } catch (error) {\n return {\n candidate: {\n ...options.candidate,\n qidOptions: [],\n },\n fallback: {\n issues: [formatFallbackIssue(error)],\n reason: \"guaranteed_json_failed\",\n },\n };\n }\n}\n\nfunction normalizeNarrowingResponse(\n response: z.infer<typeof narrowingResponseSchema>,\n): WikimatchCandidateOptionNarrowingResponse {\n return {\n qids: response.qids.map((item) => ({\n decision: item.decision,\n qid: item.qid,\n })),\n };\n}\n\nexport function parseNarrowingResponse(\n candidate: WikimatchCandidate,\n response: WikimatchCandidateOptionNarrowingResponse,\n): WikimatchCandidateOptionNarrowingResult {\n const issues = validateNarrowingResponse(candidate, response);\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n return {\n candidate: filterCandidateQidOptions(\n candidate,\n new Set(\n response.qids\n .filter((item) => item.decision === \"keep\")\n .map((item) => item.qid),\n ),\n ),\n };\n}\n\nexport function validateNarrowingResponse(\n candidate: WikimatchCandidate,\n response: WikimatchCandidateOptionNarrowingResponse,\n): readonly string[] {\n const issues: string[] = [];\n const allowedQids = new Set(listCandidateSelectableQids(candidate));\n const seenQids = new Set<string>();\n\n for (const item of response.qids) {\n if (!allowedQids.has(item.qid)) {\n issues.push(\n `Unknown qid \"${item.qid}\" for candidate ${candidate.id}. Use exactly these QIDs: ${[\n ...allowedQids,\n ].join(\", \")}.`,\n );\n continue;\n }\n if (seenQids.has(item.qid)) {\n issues.push(`Duplicate narrowing result for qid ${item.qid}.`);\n continue;\n }\n\n seenQids.add(item.qid);\n }\n\n for (const qid of allowedQids) {\n if (!seenQids.has(qid)) {\n issues.push(\n `Missing narrowing result for qid ${qid}. Return exactly one keep/reject decision for every input QID.`,\n );\n }\n }\n\n return issues;\n}\n\nfunction buildNarrowingMessages(\n input: NarrowWikimatchCandidateOptionsInput,\n): LLMessage[] {\n return [\n {\n role: \"system\",\n content: formatNarrowingSystemPrompt(input),\n },\n {\n role: \"user\",\n content: formatNarrowingPrompt(input),\n },\n ];\n}\n\nfunction formatNarrowingSystemPrompt(\n input: NarrowWikimatchCandidateOptionsInput,\n): string {\n return [\n \"You narrow an oversized Wikidata candidate option list before final mention grounding.\",\n \"\",\n \"Recall policy:\",\n input.policyPrompt,\n \"\",\n \"Rules:\",\n \"- Return JSON only.\",\n \"- Return exactly one keep/reject decision for every input QID.\",\n \"- Keep any QID that could plausibly ground this surface in the current context.\",\n \"- Reject QIDs that are clearly unrelated to the surface and context.\",\n \"- Be conservative when uncertain; keep plausible meanings so the final grounding request can decide.\",\n \"- Do not invent QIDs.\",\n ].join(\"\\n\");\n}\n\nfunction formatNarrowingPrompt(\n input: NarrowWikimatchCandidateOptionsInput,\n): string {\n return [\n \"Context:\",\n input.text,\n \"\",\n \"Oversized candidate:\",\n JSON.stringify(\n formatCandidateForPrompt(input.candidate, {\n candidateIds: [input.candidate.id],\n id: \"g1\",\n range: input.candidate.range,\n }),\n null,\n 2,\n ),\n \"\",\n \"Return this JSON shape:\",\n JSON.stringify(\n {\n qids: [\n {\n decision: \"keep | reject\",\n qid: \"QID from the input\",\n },\n ],\n },\n null,\n 2,\n ),\n ].join(\"\\n\");\n}\n\nfunction formatFallbackIssue(error: unknown): string {\n if (error instanceof ParsedJsonError) {\n return error.issues.join(\"\\n\");\n }\n if (error instanceof Error) {\n const issues = (error as { readonly issues?: unknown }).issues;\n\n if (\n Array.isArray(issues) &&\n issues.every((issue) => typeof issue === \"string\")\n ) {\n return issues.join(\"\\n\");\n }\n\n return error.message;\n }\n\n return String(error);\n}\n","import { WikipageResolver } from \"../wikipage/index.js\";\n\nimport type {\n QidResolution,\n WikipageResolverOptions,\n WikipageResolveProgressReporter,\n} from \"../wikipage/index.js\";\nimport type { WikimatchCandidate, WikimatchQidOption } from \"./types.js\";\n\nexport async function enrichWikimatchCandidates(\n candidates: readonly WikimatchCandidate[],\n options: {\n readonly progress?: WikipageResolveProgressReporter;\n readonly resolverOptions?: Omit<WikipageResolverOptions, \"progress\">;\n } = {},\n): Promise<readonly WikimatchCandidate[]> {\n if (candidates.length === 0) {\n return [];\n }\n\n const resolver = await WikipageResolver.open({\n ...(options.resolverOptions ?? {}),\n ...(options.progress === undefined ? {} : { progress: options.progress }),\n });\n\n try {\n return applyQidResolutions(\n candidates,\n await resolver.resolveQids(listQids(candidates)),\n );\n } finally {\n await resolver.close();\n }\n}\n\nexport function applyQidResolutions(\n candidates: readonly WikimatchCandidate[],\n resolutions: readonly QidResolution[],\n): readonly WikimatchCandidate[] {\n const resolutionsByQid = new Map(\n resolutions.map((resolution) => [resolution.qid, resolution]),\n );\n\n return candidates.map((candidate) => ({\n ...candidate,\n qidOptions: candidate.qidOptions.map((option) =>\n enrichQidOption(option, resolutionsByQid.get(option.qid)),\n ),\n }));\n}\n\nfunction enrichQidOption(\n option: WikimatchQidOption,\n resolution: QidResolution | undefined,\n): WikimatchQidOption {\n if (resolution === undefined) {\n return option;\n }\n\n return {\n ...option,\n ...(resolution.description === undefined\n ? {}\n : { description: resolution.description }),\n ...(resolution.disambiguation === undefined\n ? {}\n : { disambiguation: resolution.disambiguation }),\n isDisambiguation: resolution.isDisambiguation,\n ...(resolution.label === undefined ? {} : { label: resolution.label }),\n };\n}\n\nfunction listQids(\n candidates: readonly WikimatchCandidate[],\n): readonly string[] {\n return [\n ...new Set(\n candidates.flatMap((candidate) =>\n candidate.qidOptions.map((option) => option.qid),\n ),\n ),\n ];\n}\n","import { z } from \"zod\";\n\nimport {\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n type GuaranteedRequest,\n} from \"../guaranteed/index.js\";\nimport type { LLMessage } from \"../llm/index.js\";\n\nimport type {\n WikimatchProtectedSurface,\n WikimatchSurface,\n WikimatchSurfaceProtectionInput,\n WikimatchSurfaceProtectionResponse,\n WikimatchSurfaceProtectionResult,\n} from \"./types.js\";\n\nconst protectedSurfaceSchema = z\n .object({\n note: z.string().max(24).optional(),\n surfaceId: z.string().min(1),\n })\n .strict();\n\nconst protectionResponseSchema = z\n .object({\n protectedSurfaces: z.array(protectedSurfaceSchema),\n })\n .strict();\n\nexport interface JudgeWikimatchSurfaceProtectionOptions extends WikimatchSurfaceProtectionInput {\n readonly maxRetries?: number;\n readonly request: GuaranteedRequest;\n}\n\nexport async function judgeWikimatchSurfaceProtection(\n options: JudgeWikimatchSurfaceProtectionOptions,\n): Promise<WikimatchSurfaceProtectionResult> {\n if (options.suspiciousSurfaces.length === 0) {\n return { protectedSurfaces: [] };\n }\n\n try {\n return await requestGuaranteedJson({\n messages: buildProtectionMessages(options),\n parse: (response) =>\n parseSurfaceProtectionResponse(\n options.suspiciousSurfaces,\n normalizeProtectionResponse(response),\n ),\n request: options.request,\n responseIntentClassifierPrompt:\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n schema: protectionResponseSchema,\n ...(options.maxRetries === undefined\n ? {}\n : { maxRetries: options.maxRetries }),\n });\n } catch (error) {\n return {\n fallback: {\n issues: [formatFallbackIssue(error)],\n reason: \"guaranteed_json_failed\",\n },\n protectedSurfaces: [],\n };\n }\n}\n\nfunction normalizeProtectionResponse(\n response: z.infer<typeof protectionResponseSchema>,\n): WikimatchSurfaceProtectionResponse {\n return {\n protectedSurfaces: response.protectedSurfaces.map((surface) => ({\n ...(surface.note === undefined ? {} : { note: surface.note }),\n surfaceId: surface.surfaceId,\n })),\n };\n}\n\nexport function parseSurfaceProtectionResponse(\n surfaces: readonly WikimatchSurface[],\n response: WikimatchSurfaceProtectionResponse,\n): WikimatchSurfaceProtectionResult {\n const issues = validateSurfaceProtectionResponse(surfaces, response);\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n const surfacesById = createSurfaceMap(surfaces);\n const protectedSurfaces: WikimatchProtectedSurface[] = [];\n\n for (const surface of response.protectedSurfaces) {\n const source = surfacesById.get(surface.surfaceId)!;\n\n protectedSurfaces.push({\n ...(surface.note === undefined ? {} : { note: surface.note }),\n surfaceId: surface.surfaceId,\n text: source.text,\n });\n }\n\n return { protectedSurfaces };\n}\n\nexport function validateSurfaceProtectionResponse(\n surfaces: readonly WikimatchSurface[],\n response: WikimatchSurfaceProtectionResponse,\n): readonly string[] {\n const issues: string[] = [];\n const surfacesById = createSurfaceMap(surfaces);\n const seenIds = new Set<string>();\n\n for (const surface of response.protectedSurfaces) {\n if (!surfacesById.has(surface.surfaceId)) {\n issues.push(\n `Unknown surfaceId \"${surface.surfaceId}\". Use only these surface IDs: ${[\n ...surfacesById.keys(),\n ].join(\", \")}.`,\n );\n continue;\n }\n if (seenIds.has(surface.surfaceId)) {\n issues.push(`Duplicate protected surface ${surface.surfaceId}.`);\n continue;\n }\n\n seenIds.add(surface.surfaceId);\n }\n\n return issues;\n}\n\nfunction buildProtectionMessages(\n input: WikimatchSurfaceProtectionInput,\n): LLMessage[] {\n return [\n {\n role: \"system\",\n content: formatProtectionSystemPrompt(input),\n },\n {\n role: \"user\",\n content: formatProtectionPrompt(input),\n },\n ];\n}\n\nfunction formatProtectionSystemPrompt(\n input: WikimatchSurfaceProtectionInput,\n): string {\n return [\n \"You protect useful high-frequency surface strings before Wikidata grounding.\",\n \"\",\n \"The input surfaces are suspicious only because they are frequent after range suppression.\",\n \"Most high-frequency function words, fragments, punctuation, generic words, and discourse words should NOT be protected.\",\n \"A surface that is not protected will be removed before grounding.\",\n \"\",\n \"User recall policy:\",\n input.policyPrompt,\n \"\",\n \"Protect a surface only when it is likely to be a meaningful entity, named concept, named event, place, person, organization, work, or domain term under the policy.\",\n \"\",\n \"Rules:\",\n \"- Return JSON only.\",\n \"- Return only protected surface IDs.\",\n \"- Do not return a result for every input surface.\",\n \"- Do not protect generic grammar words merely because they appear often.\",\n \"- Do not infer or choose Wikidata QIDs in this stage.\",\n ].join(\"\\n\");\n}\n\nfunction formatProtectionPrompt(\n input: WikimatchSurfaceProtectionInput,\n): string {\n return [\n \"Suspicious high-frequency surfaces:\",\n JSON.stringify(\n input.suspiciousSurfaces.map((surface) => ({\n count: surface.count,\n surfaceId: surface.id,\n text: surface.text,\n })),\n null,\n 2,\n ),\n \"\",\n \"Return this JSON shape:\",\n JSON.stringify(\n {\n protectedSurfaces: [\n {\n note: \"optional, <= 12 Chinese chars or 6 English words\",\n surfaceId: \"surface id from the input\",\n },\n ],\n },\n null,\n 2,\n ),\n ].join(\"\\n\");\n}\n\nfunction createSurfaceMap(\n surfaces: readonly WikimatchSurface[],\n): ReadonlyMap<string, WikimatchSurface> {\n return new Map(surfaces.map((surface) => [surface.id, surface]));\n}\n\nfunction formatFallbackIssue(error: unknown): string {\n if (error instanceof ParsedJsonError) {\n return error.issues.join(\"\\n\");\n }\n if (error instanceof Error) {\n const issues = (error as { readonly issues?: unknown }).issues;\n\n if (\n Array.isArray(issues) &&\n issues.every((issue) => typeof issue === \"string\")\n ) {\n return issues.join(\"\\n\");\n }\n\n return error.message;\n }\n\n return String(error);\n}\n","export interface SuppressContainedRangeInput {\n readonly range: {\n readonly end: number;\n readonly start: number;\n };\n}\n\nexport function suppressContainedRanges<T extends SuppressContainedRangeInput>(\n items: readonly T[],\n): readonly T[] {\n const sorted = [...items]\n .filter((item) => item.range.start < item.range.end)\n .sort(compareByRangeForSuppression);\n const kept: T[] = [];\n\n for (const item of sorted) {\n if (kept.some((candidate) => containsRange(candidate, item))) {\n continue;\n }\n\n kept.push(item);\n }\n\n return kept.sort(compareByOriginalOrder);\n}\n\nfunction compareByRangeForSuppression<T extends SuppressContainedRangeInput>(\n left: T,\n right: T,\n): number {\n return (\n left.range.start - right.range.start ||\n right.range.end - left.range.end ||\n rangeLength(right) - rangeLength(left)\n );\n}\n\nfunction compareByOriginalOrder<T extends SuppressContainedRangeInput>(\n left: T,\n right: T,\n): number {\n return (\n left.range.start - right.range.start ||\n left.range.end - right.range.end ||\n rangeLength(left) - rangeLength(right)\n );\n}\n\nfunction containsRange<T extends SuppressContainedRangeInput>(\n container: T,\n item: T,\n): boolean {\n return (\n container.range.start <= item.range.start &&\n item.range.end <= container.range.end &&\n rangeLength(container) > rangeLength(item)\n );\n}\n\nfunction rangeLength(item: SuppressContainedRangeInput): number {\n return item.range.end - item.range.start;\n}\n","import type {\n BuildWikimatchSurfaceProtectionInputOptions,\n WikimatchCandidate,\n WikimatchSurface,\n WikimatchSurfaceProtectionBuildResult,\n} from \"./types.js\";\nimport { suppressContainedRanges } from \"./range-suppression.js\";\n\nexport function buildWikimatchSurfaceProtectionInput(\n options: BuildWikimatchSurfaceProtectionInputOptions,\n): WikimatchSurfaceProtectionBuildResult {\n const suppressedCandidates = suppressContainedRanges(options.candidates);\n const surfaces = listSurfaces(suppressedCandidates, options.text);\n const suspiciousSurfaces = selectTopSurfaces(surfaces, options.percentile);\n const suspiciousTexts = new Set(\n suspiciousSurfaces.map((surface) => surface.text),\n );\n\n return {\n candidates: suppressedCandidates.filter(\n (candidate) =>\n !suspiciousTexts.has(\n options.text.slice(candidate.range.start, candidate.range.end),\n ),\n ),\n suppressedCandidates,\n suspiciousSurfaces,\n };\n}\n\nfunction listSurfaces(\n candidates: readonly WikimatchCandidate[],\n text: string,\n): readonly WikimatchSurface[] {\n const surfaces = new Map<string, number>();\n\n for (const candidate of candidates) {\n const surface = text.slice(candidate.range.start, candidate.range.end);\n\n surfaces.set(surface, (surfaces.get(surface) ?? 0) + 1);\n }\n\n return [...surfaces.entries()]\n .map(([surface, count], index) => ({\n count,\n id: `s${index + 1}`,\n text: surface,\n }))\n .sort(compareSurface);\n}\n\nfunction selectTopSurfaces(\n surfaces: readonly WikimatchSurface[],\n percentile: number,\n): readonly WikimatchSurface[] {\n if (surfaces.length === 0 || percentile <= 0) {\n return [];\n }\n\n const count = Math.ceil(surfaces.length * Math.min(1, percentile));\n\n return surfaces.slice(0, count);\n}\n\nfunction compareSurface(\n left: WikimatchSurface,\n right: WikimatchSurface,\n): number {\n return (\n right.count - left.count ||\n left.text.localeCompare(right.text) ||\n left.id.localeCompare(right.id)\n );\n}\n","import { spawn } from \"child_process\";\n\nimport type {\n WikimatchCandidate,\n WikimatchQidOption,\n WikimatchSentence,\n} from \"./types.js\";\n\nexport interface MatchWikispineSentenceCandidatesOptions {\n readonly command?: string;\n readonly dataDir?: string;\n readonly endpoint?: string;\n readonly fetch?: typeof fetch;\n readonly includeDisambiguation?: boolean;\n readonly maxCandidatesPerSurface?: number;\n readonly onProgress?: (\n progress: WikispineMatchProgress,\n ) => Promise<void> | void;\n readonly provider?: WikispineProvider;\n readonly sentences: readonly WikimatchSentence[];\n}\n\nexport type WikispineProvider = \"cli\" | \"fetch\";\n\nexport interface WikispineMatchProgress {\n readonly coveredRangeEnd: number;\n}\n\nexport interface TestWikispineRuntimeOptions {\n readonly command?: string;\n readonly dataDir?: string;\n readonly endpoint?: string;\n readonly fetch?: typeof fetch;\n readonly provider?: WikispineProvider;\n}\n\nexport interface WikispineRuntimeTestResult {\n readonly durationMs: number;\n readonly metadata?: WikispineMetadata;\n readonly ok: true;\n readonly provider: WikispineProvider;\n}\n\ninterface WikispineMatchEvent {\n readonly match: WikispineMatchRecord;\n readonly type: \"match\";\n}\n\ninterface WikispineDoneEvent {\n readonly stats?: unknown;\n readonly type: \"done\";\n}\n\ntype WikispineEvent = WikispineDoneEvent | WikispineMatchEvent;\n\ninterface WikispineMatchRecord {\n readonly end: number;\n readonly qids: readonly WikispineQidCandidate[];\n readonly start: number;\n readonly surface_id: number;\n}\n\ninterface WikispineQidCandidate {\n readonly disambiguation?: boolean;\n readonly qid: string;\n}\n\ninterface WikispineMetadata {\n readonly automaton_shard_count: number;\n readonly format: string;\n readonly qid_count: number;\n readonly surface_count: number;\n readonly surface_normalization: string;\n}\n\nconst WIKISPINE_RUNTIME_GUIDE_URL =\n \"https://raw.githubusercontent.com/oomol-lab/wiki-graph/refs/heads/main/docs/wikispine-runtime.md\";\nexport const DEFAULT_WIKISPINE_FETCH_ENDPOINT =\n \"https://wikispi-service-cxbfjlteab.cn-hangzhou.fcapp.run\";\n\nexport async function matchWikispineSentenceCandidates(\n options: MatchWikispineSentenceCandidatesOptions,\n): Promise<readonly WikimatchCandidate[]> {\n const candidates: WikimatchCandidate[] = [];\n let candidateIndex = 1;\n\n for (const sentence of options.sentences) {\n for (const matched of await matchSentence(sentence, options)) {\n const surface = sentence.text.slice(matched.start, matched.end);\n\n candidates.push({\n id: `c${candidateIndex}`,\n qidOptions: matched.qids.map(toQidOption),\n range: {\n end: sentence.range.start + matched.end,\n start: sentence.range.start + matched.start,\n },\n surface,\n });\n candidateIndex += 1;\n }\n await options.onProgress?.({ coveredRangeEnd: sentence.range.end });\n }\n\n return candidates;\n}\n\nasync function matchSentence(\n sentence: WikimatchSentence,\n options: MatchWikispineSentenceCandidatesOptions,\n): Promise<readonly WikispineMatchRecord[]> {\n return resolveProvider(options) === \"fetch\"\n ? await fetchWikispineMatch(options, sentence)\n : await runWikispineMatch(\n options.command ?? \"wikispine\",\n buildMatchArgs(options),\n sentence,\n options,\n );\n}\n\nexport async function testWikispineRuntime(\n options: TestWikispineRuntimeOptions,\n): Promise<WikispineRuntimeTestResult> {\n const provider = resolveProvider(options);\n const startedAt = Date.now();\n\n if (provider === \"fetch\") {\n const endpoint = requireEndpoint(options.endpoint);\n const metadata = await fetchWikispineMetadata(endpoint, options.fetch);\n\n await fetchWikispineMatch(\n {\n ...options,\n endpoint,\n maxCandidatesPerSurface: 1,\n },\n {\n id: \"test\",\n range: { end: 7, start: 0 },\n text: \"北京大学位于北京。\",\n },\n );\n\n return {\n durationMs: Date.now() - startedAt,\n metadata,\n ok: true,\n provider,\n };\n }\n\n await runWikispineMatch(\n options.command ?? \"wikispine\",\n buildMatchArgs({\n ...options,\n maxCandidatesPerSurface: 1,\n }),\n {\n id: \"test\",\n range: { end: 7, start: 0 },\n text: \"北京大学位于北京。\",\n },\n {},\n );\n\n return {\n durationMs: Date.now() - startedAt,\n ok: true,\n provider,\n };\n}\n\nasync function runWikispineMatch(\n command: string,\n args: readonly string[],\n sentence: WikimatchSentence,\n options: Pick<MatchWikispineSentenceCandidatesOptions, \"onProgress\">,\n): Promise<readonly WikispineMatchRecord[]> {\n return await new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n const progress = createWikispineProgressReporter(options.onProgress, {\n onFailure: (error) => {\n reject(error);\n child.kill();\n },\n });\n const parser = createWikispineNdjsonParser({\n onMatch: (match) => {\n progress.report({\n coveredRangeEnd: sentence.range.start + match.end,\n });\n },\n });\n const stderr: Buffer[] = [];\n\n child.stdout.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => {\n try {\n parser.push(chunk);\n } catch (error) {\n reject(toError(error));\n child.kill();\n }\n });\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr.push(chunk);\n });\n child.on(\"error\", (error) => {\n reject(\n new Error(\n formatWikispineRuntimeError(\n `Failed to start wikispine command: ${error.message}`,\n ),\n ),\n );\n });\n child.on(\"close\", (code) => {\n if (code !== 0) {\n reject(\n new Error(\n formatWikispineRuntimeError(\n `wikispine match failed with exit code ${code}: ${Buffer.concat(stderr).toString(\"utf8\")}`,\n ),\n ),\n );\n return;\n }\n\n void (async () => {\n try {\n const matches = parser.finish();\n await progress.wait();\n resolve(matches);\n } catch (error) {\n reject(toError(error));\n }\n })();\n });\n\n child.stdin.end(sentence.text);\n });\n}\n\nfunction buildMatchArgs(\n options: Pick<\n MatchWikispineSentenceCandidatesOptions,\n \"dataDir\" | \"includeDisambiguation\" | \"maxCandidatesPerSurface\"\n >,\n): string[] {\n const args = [\"match\"];\n\n if (options.dataDir !== undefined) {\n args.push(\"--data-dir\", options.dataDir);\n }\n if (options.includeDisambiguation === false) {\n args.push(\"--exclude-disambiguation\");\n }\n if (options.maxCandidatesPerSurface !== undefined) {\n args.push(\n \"--max-candidates-per-surface\",\n String(options.maxCandidatesPerSurface),\n );\n }\n\n return args;\n}\n\nasync function fetchWikispineMatch(\n options: Pick<\n MatchWikispineSentenceCandidatesOptions,\n | \"endpoint\"\n | \"fetch\"\n | \"includeDisambiguation\"\n | \"maxCandidatesPerSurface\"\n | \"onProgress\"\n >,\n sentence: WikimatchSentence,\n): Promise<readonly WikispineMatchRecord[]> {\n const endpoint = requireEndpoint(options.endpoint);\n const response = await (options.fetch ?? fetch)(`${endpoint}/match`, {\n body: JSON.stringify({\n options: {\n ...(options.includeDisambiguation === undefined\n ? {}\n : { include_disambiguation: options.includeDisambiguation }),\n ...(options.maxCandidatesPerSurface === undefined\n ? {}\n : {\n max_candidates_per_surface: options.maxCandidatesPerSurface,\n }),\n },\n text: sentence.text,\n }),\n headers: {\n accept: \"application/x-ndjson\",\n \"content-type\": \"application/json\",\n },\n method: \"POST\",\n });\n\n if (!response.ok) {\n throw new Error(\n formatWikispineRuntimeError(\n `WikiSpine fetch provider failed with HTTP ${response.status}: ${await response.text()}`,\n ),\n );\n }\n\n const progress = createWikispineProgressReporter(options.onProgress);\n const parser = createWikispineNdjsonParser({\n onMatch: (match) => {\n progress.report({\n coveredRangeEnd: sentence.range.start + match.end,\n });\n },\n });\n\n if (response.body === null) {\n parser.push(await response.text());\n const matches = parser.finish();\n await progress.wait();\n return matches;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n parser.push(decoder.decode(value, { stream: true }));\n progress.throwIfFailed();\n }\n\n parser.push(decoder.decode());\n const matches = parser.finish();\n await progress.wait();\n return matches;\n}\n\nfunction createWikispineProgressReporter(\n onProgress:\n | ((progress: WikispineMatchProgress) => Promise<void> | void)\n | undefined,\n options?: {\n readonly onFailure?: (error: Error) => void;\n },\n): {\n readonly report: (progress: WikispineMatchProgress) => void;\n readonly throwIfFailed: () => void;\n readonly wait: () => Promise<void>;\n} {\n const tasks: Promise<void>[] = [];\n let failure: Error | undefined;\n\n function fail(error: unknown): void {\n if (failure !== undefined) {\n return;\n }\n failure = toError(error);\n options?.onFailure?.(failure);\n }\n\n return {\n report: (progress) => {\n if (onProgress === undefined || failure !== undefined) {\n return;\n }\n\n try {\n tasks.push(Promise.resolve(onProgress(progress)).catch(fail));\n } catch (error) {\n fail(error);\n }\n },\n throwIfFailed: () => {\n if (failure !== undefined) {\n throw failure;\n }\n },\n wait: async () => {\n await Promise.all(tasks);\n if (failure !== undefined) {\n throw failure;\n }\n },\n };\n}\n\nasync function fetchWikispineMetadata(\n endpoint: string,\n fetchFn: typeof fetch = fetch,\n): Promise<WikispineMetadata> {\n const ready = await fetchFn(`${endpoint}/readyz`);\n\n if (!ready.ok) {\n throw new Error(\n formatWikispineRuntimeError(\n `WikiSpine fetch provider is not ready: HTTP ${ready.status}.`,\n ),\n );\n }\n\n const response = await fetchFn(`${endpoint}/metadata`);\n\n if (!response.ok) {\n throw new Error(\n formatWikispineRuntimeError(\n `WikiSpine metadata request failed with HTTP ${response.status}.`,\n ),\n );\n }\n\n try {\n return parseWikispineMetadata(await response.json());\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"setup guide\")) {\n throw error;\n }\n\n throw new Error(\n formatWikispineRuntimeError(\n `Invalid WikiSpine metadata response: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n }\n}\n\nfunction createWikispineNdjsonParser(input?: {\n readonly onMatch?: (match: WikispineMatchRecord) => void;\n}): {\n readonly finish: () => readonly WikispineMatchRecord[];\n readonly push: (chunk: string) => void;\n} {\n const matches: WikispineMatchRecord[] = [];\n let buffer = \"\";\n\n function parseLine(line: string): void {\n if (line.trim() === \"\") {\n return;\n }\n\n const event = parseWikispineEvent(JSON.parse(line));\n\n if (event.type === \"match\") {\n matches.push(event.match);\n input?.onMatch?.(event.match);\n }\n }\n\n function wrapParseError(error: unknown): Error {\n return new Error(\n formatWikispineRuntimeError(\n `Invalid WikiSpine match response: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n }\n\n return {\n finish: () => {\n try {\n parseLine(buffer);\n buffer = \"\";\n return matches;\n } catch (error) {\n throw wrapParseError(error);\n }\n },\n push: (chunk) => {\n try {\n buffer += chunk;\n const lines = buffer.split(/\\r?\\n/u);\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n parseLine(line);\n }\n } catch (error) {\n throw wrapParseError(error);\n }\n },\n };\n}\n\nfunction parseWikispineEvent(value: unknown): WikispineEvent {\n if (typeof value !== \"object\" || value === null) {\n throw new Error(\"Expected wikispine event to be an object.\");\n }\n\n const record = value as Record<string, unknown>;\n\n if (record.type === \"done\") {\n return { type: \"done\" };\n }\n if (record.type === \"match\" && isWikispineMatchRecord(record.match)) {\n return {\n match: record.match,\n type: \"match\",\n };\n }\n\n throw new Error(`Unexpected wikispine event: ${JSON.stringify(value)}`);\n}\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction parseWikispineMetadata(value: unknown): WikispineMetadata {\n if (typeof value !== \"object\" || value === null) {\n throw new Error(\n formatWikispineRuntimeError(\"Expected WikiSpine metadata.\"),\n );\n }\n\n const record = value as Record<string, unknown>;\n\n if (\n typeof record.automaton_shard_count !== \"number\" ||\n typeof record.format !== \"string\" ||\n typeof record.qid_count !== \"number\" ||\n typeof record.surface_count !== \"number\" ||\n typeof record.surface_normalization !== \"string\"\n ) {\n throw new Error(formatWikispineRuntimeError(\"Invalid WikiSpine metadata.\"));\n }\n\n return {\n automaton_shard_count: record.automaton_shard_count,\n format: record.format,\n qid_count: record.qid_count,\n surface_count: record.surface_count,\n surface_normalization: record.surface_normalization,\n };\n}\n\nfunction isWikispineMatchRecord(value: unknown): value is WikispineMatchRecord {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n typeof record.end === \"number\" &&\n Array.isArray(record.qids) &&\n record.qids.every(isWikispineQidCandidate) &&\n typeof record.start === \"number\" &&\n typeof record.surface_id === \"number\"\n );\n}\n\nfunction isWikispineQidCandidate(\n value: unknown,\n): value is WikispineQidCandidate {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const record = value as Record<string, unknown>;\n\n return (\n (!(\"disambiguation\" in record) ||\n typeof record.disambiguation === \"boolean\") &&\n typeof record.qid === \"string\" &&\n /^Q[1-9]\\d*$/u.test(record.qid)\n );\n}\n\nfunction toQidOption(candidate: WikispineQidCandidate): WikimatchQidOption {\n return {\n isDisambiguation: candidate.disambiguation === true,\n qid: candidate.qid,\n };\n}\n\nfunction resolveProvider(options: {\n readonly provider?: WikispineProvider;\n}): WikispineProvider {\n return options.provider ?? \"cli\";\n}\n\nfunction requireEndpoint(endpoint: string | undefined): string {\n const normalized = (endpoint ?? DEFAULT_WIKISPINE_FETCH_ENDPOINT)\n .trim()\n .replace(/\\/+$/u, \"\");\n\n if (normalized === \"\") {\n throw new Error(\n formatWikispineRuntimeError(\n \"WikiSpine fetch provider has no default endpoint.\",\n ),\n );\n }\n\n return normalized;\n}\n\nfunction formatWikispineRuntimeError(message: string): string {\n return `${message}\\nWikiSpine setup guide: ${WIKISPINE_RUNTIME_GUIDE_URL}`;\n}\n","export interface WordBoundary {\n readonly end: number;\n readonly start: number;\n}\n\nexport function listWordBoundaries(text: string): readonly WordBoundary[] {\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const graphemeSegmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n const words: WordBoundary[] = [];\n\n for (const segment of segmenter.segment(text)) {\n if (\"isWordLike\" in segment && segment.isWordLike) {\n words.push({\n end: segment.index + segment.segment.length,\n start: segment.index,\n });\n }\n }\n\n if (words.length > 0) {\n return words;\n }\n\n return [...graphemeSegmenter.segment(text)]\n .filter((segment) => segment.segment.trim() !== \"\")\n .map((segment) => ({\n end: segment.index + segment.segment.length,\n start: segment.index,\n }));\n}\n\nexport function expandRangeByWords(input: {\n readonly rangeEnd: number;\n readonly rangeStart: number;\n readonly text: string;\n readonly words: number;\n}): { readonly end: number; readonly start: number } {\n const wordBoundaries = listWordBoundaries(input.text);\n\n if (wordBoundaries.length === 0) {\n return {\n end: input.text.length,\n start: 0,\n };\n }\n\n const firstWordIndex = findWordIndexAtOrAfter(\n wordBoundaries,\n input.rangeStart,\n );\n const lastWordIndex = findWordIndexBeforeOrAt(wordBoundaries, input.rangeEnd);\n const halfBudget = Math.max(0, Math.floor(input.words / 2));\n const startIndex = Math.max(0, firstWordIndex - halfBudget);\n const endIndex = Math.min(\n wordBoundaries.length - 1,\n lastWordIndex + halfBudget,\n );\n\n return {\n end: wordBoundaries[endIndex]?.end ?? input.text.length,\n start: wordBoundaries[startIndex]?.start ?? 0,\n };\n}\n\nfunction findWordIndexAtOrAfter(\n words: readonly WordBoundary[],\n offset: number,\n): number {\n const matched = words.findIndex((word) => word.end > offset);\n\n return matched === -1 ? words.length - 1 : matched;\n}\n\nfunction findWordIndexBeforeOrAt(\n words: readonly WordBoundary[],\n offset: number,\n): number {\n for (let index = words.length - 1; index >= 0; index -= 1) {\n const word = words[index];\n\n if (word !== undefined && word.start < offset) {\n return index;\n }\n }\n\n return 0;\n}\n","import { expandRangeByWords } from \"./words.js\";\nimport { countWikimatchCandidateOptions } from \"./options.js\";\nimport type {\n BuildWikimatchWindowsOptions,\n WikimatchCandidate,\n WikimatchWindow,\n} from \"./types.js\";\n\ninterface CandidateGroup {\n readonly candidates: readonly WikimatchCandidate[];\n readonly range: {\n readonly end: number;\n readonly start: number;\n };\n}\n\nexport function buildWikimatchWindows(\n options: BuildWikimatchWindowsOptions,\n): readonly WikimatchWindow[] {\n const groups = groupOverlappingCandidates(\n normalizeCandidates(options.candidates ?? []),\n );\n const windows: WikimatchWindow[] = [];\n let pendingGroups: CandidateGroup[] = [];\n let pendingOptionCount = 0;\n\n for (const group of groups) {\n const groupOptionCount = countGroupOptions(group);\n\n if (\n pendingGroups.length > 0 &&\n pendingOptionCount + groupOptionCount > options.optionBudget\n ) {\n windows.push(createWindow(options.text, pendingGroups, options));\n pendingGroups = [];\n pendingOptionCount = 0;\n }\n\n pendingGroups.push(group);\n pendingOptionCount += groupOptionCount;\n }\n\n if (pendingGroups.length > 0) {\n windows.push(createWindow(options.text, pendingGroups, options));\n }\n\n return windows;\n}\n\nfunction countGroupOptions(group: CandidateGroup): number {\n return group.candidates.reduce(\n (total, candidate) => total + countWikimatchCandidateOptions(candidate),\n 0,\n );\n}\n\nfunction createWindow(\n text: string,\n groups: readonly CandidateGroup[],\n options: BuildWikimatchWindowsOptions,\n): WikimatchWindow {\n const groupStart = Math.min(...groups.map((group) => group.range.start));\n const groupEnd = Math.max(...groups.map((group) => group.range.end));\n const expandedRange = expandRangeByWords({\n rangeEnd: groupEnd,\n rangeStart: groupStart,\n text,\n words: options.contextWords,\n });\n\n return {\n baseOffset: expandedRange.start,\n candidates: groups.flatMap((group) => group.candidates),\n groups: groups.map((group, index) => ({\n candidateIds: group.candidates.map((candidate) => candidate.id),\n id: `g${index + 1}`,\n range: group.range,\n })),\n text: text.slice(expandedRange.start, expandedRange.end),\n };\n}\n\nfunction groupOverlappingCandidates(\n candidates: readonly WikimatchCandidate[],\n): readonly CandidateGroup[] {\n const groups: CandidateGroup[] = [];\n let current: CandidateGroup | undefined;\n\n for (const candidate of candidates) {\n if (current === undefined || candidate.range.start >= current.range.end) {\n current = {\n candidates: [candidate],\n range: candidate.range,\n };\n groups.push(current);\n continue;\n }\n\n current = {\n candidates: [...current.candidates, candidate],\n range: {\n end: Math.max(current.range.end, candidate.range.end),\n start: Math.min(current.range.start, candidate.range.start),\n },\n };\n groups[groups.length - 1] = current;\n }\n\n return groups;\n}\n\nfunction normalizeCandidates(\n candidates: readonly WikimatchCandidate[],\n): readonly WikimatchCandidate[] {\n return candidates\n .filter((candidate) => candidate.range.start < candidate.range.end)\n .sort(compareCandidate);\n}\n\nfunction compareCandidate(\n left: WikimatchCandidate,\n right: WikimatchCandidate,\n): number {\n return (\n left.range.start - right.range.start ||\n right.range.end - left.range.end ||\n left.id.localeCompare(right.id)\n );\n}\n","export {\n judgeWikimatchPolicy,\n formatCandidateForPrompt,\n parsePolicyResponse,\n validatePolicyResponse,\n type JudgeWikimatchPolicyOptions,\n} from \"./policy-judge.js\";\nexport {\n narrowWikimatchCandidateOptions,\n parseNarrowingResponse,\n validateNarrowingResponse,\n type NarrowWikimatchCandidateOptionsOptions,\n} from \"./option-narrowing.js\";\nexport {\n countWikimatchCandidateOptions,\n countWikimatchQidOption,\n filterCandidateQidOptions,\n listCandidateSelectableQids,\n sliceCandidateByOptionBudget,\n splitCandidateByOptionBudget,\n} from \"./options.js\";\nexport {\n applyQidResolutions,\n enrichWikimatchCandidates,\n} from \"./enrichment.js\";\nexport {\n judgeWikimatchSurfaceProtection,\n parseSurfaceProtectionResponse,\n validateSurfaceProtectionResponse,\n type JudgeWikimatchSurfaceProtectionOptions,\n} from \"./surface-screening.js\";\nexport { buildWikimatchSurfaceProtectionInput } from \"./surface-window.js\";\nexport { suppressContainedRanges } from \"./range-suppression.js\";\nexport {\n DEFAULT_WIKISPINE_FETCH_ENDPOINT,\n matchWikispineSentenceCandidates,\n type MatchWikispineSentenceCandidatesOptions,\n testWikispineRuntime,\n type TestWikispineRuntimeOptions,\n type WikispineProvider,\n type WikispineRuntimeTestResult,\n} from \"./wikispine.js\";\nexport { buildWikimatchWindows } from \"./window.js\";\nexport { expandRangeByWords, listWordBoundaries } from \"./words.js\";\nexport type {\n BuildWikimatchWindowsOptions,\n BuildWikimatchSurfaceProtectionInputOptions,\n WikimatchAcceptedMention,\n WikimatchCandidate,\n WikimatchCandidateOptionNarrowingFallback,\n WikimatchCandidateOptionNarrowingItemOutput,\n WikimatchCandidateOptionNarrowingResponse,\n WikimatchCandidateOptionNarrowingResult,\n WikimatchConflictGroup,\n NarrowWikimatchCandidateOptionsInput,\n WikimatchPolicyDecision,\n WikimatchPolicyContinuation,\n WikimatchPolicyDecisionOutput,\n WikimatchPolicyFallback,\n WikimatchPolicyJudgeInput,\n WikimatchPolicyJudgeResult,\n WikimatchPolicyResponse,\n WikimatchPolicyUpdate,\n WikimatchQidOption,\n WikimatchSentence,\n WikimatchProtectedSurface,\n WikimatchSurface,\n WikimatchSurfaceProtectionBuildResult,\n WikimatchSurfaceProtectionFallback,\n WikimatchSurfaceProtectionInput,\n WikimatchSurfaceProtectionResponse,\n WikimatchSurfaceProtectionResult,\n WikimatchTextRange,\n WikimatchWindow,\n} from \"./types.js\";\n","export {\n extractBookCoherenceChunkBatch,\n extractUserFocusedChunkBatch,\n} from \"./extract.js\";\nexport { EvidenceResolver } from \"../../evidence-selection/index.js\";\nexport { FragmentProjection } from \"./fragment-projection.js\";\nexport type { TextSpan } from \"./fragment-projection.js\";\nexport type {\n ChunkBatch,\n ChunkBatchOptions,\n ChunkExtractionScopes,\n ChunkExtractionSentence,\n ChunkImportanceAnnotation,\n ChunkLink,\n ChunkTranslationInput,\n ChunkTranslationOutput,\n CognitiveChunk,\n ExtractBookCoherenceInput,\n ExtractUserFocusedInput,\n ExtractUserFocusedResult,\n SentenceTextSource,\n} from \"./types.js\";\nexport type {\n EvidenceResolutionFailure,\n EvidenceResolutionResult,\n RankedSentenceCandidate,\n} from \"../../evidence-selection/index.js\";\n","import { z } from \"zod\";\n\nimport {\n ParsedJsonError,\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n requestGuaranteedJson,\n type GuaranteedRequest,\n} from \"../guaranteed/index.js\";\nimport type { LLMessage } from \"../llm/index.js\";\nimport {\n EVIDENCE_SELECTION_JSON_SHAPE,\n EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n EvidenceResolver,\n normalizeEvidenceDisplayText,\n resolveEvidenceSelectionList,\n type EvidenceSelectionCandidate,\n type EvidenceSelectionList,\n type EvidenceSelectionSentence,\n type EvidenceResolutionFailure,\n} from \"../evidence-selection/index.js\";\nimport { FragmentProjection } from \"../reader/chunk-batch/index.js\";\nimport type { SentenceId } from \"../document/index.js\";\n\nimport type { WikilinkEvidenceWindow, WikilinkMention } from \"./types.js\";\n\nexport interface WikilinkSentence {\n readonly text: string;\n readonly wordsCount: number;\n}\n\nexport interface WikilinkDiscoveredRelation {\n readonly confidence?: number;\n readonly evidenceSentenceIds: readonly SentenceId[];\n readonly predicate: string;\n readonly sourceMentionId: string;\n readonly targetMentionId: string;\n}\n\nexport interface DiscoverWikilinkRelationsOptions {\n readonly chapterId: number;\n readonly fragmentId?: number;\n readonly maxRetries?: number;\n readonly request: GuaranteedRequest;\n readonly sentences: readonly WikilinkSentence[];\n readonly window: WikilinkEvidenceWindow;\n}\n\nconst evidenceAnchorSchema = z\n .object({\n head: z.string().optional(),\n mode: z.enum([\"full\", \"head_tail\"]).optional(),\n tail: z.string().optional(),\n text: z.string().optional(),\n })\n .strict();\nconst evidenceSelectionItemSchema = z\n .object({\n quote: z.string().optional(),\n sentence_id: z.string().optional(),\n })\n .strict();\nconst relationEvidenceSchema = z.union([\n z\n .object({\n end_anchor: z.union([z.string(), evidenceAnchorSchema]).optional(),\n quote: z.string().optional(),\n sentence_id: z.string().optional(),\n start_anchor: z.union([z.string(), evidenceAnchorSchema]).optional(),\n })\n .strict(),\n z.array(evidenceSelectionItemSchema),\n]);\n\nconst relationSchema = z\n .object({\n confidence: z.number().min(0).max(1).optional(),\n evidence: relationEvidenceSchema,\n predicate: z.string().min(1).max(64),\n sourceMentionId: z.string().min(1),\n targetMentionId: z.string().min(1),\n })\n .strict();\n\nconst relationResponseSchema = z\n .object({\n relations: z.array(relationSchema),\n })\n .strict();\n\ntype RelationEvidenceData = z.infer<typeof relationEvidenceSchema>;\n\nconst SUGGESTED_PREDICATES = [\n \"instance_of\",\n \"subclass_of\",\n \"part_of\",\n \"has_part\",\n \"located_in\",\n \"contains\",\n \"member_of\",\n \"owned_by\",\n \"creator\",\n \"author\",\n \"developer\",\n \"founded_by\",\n \"leader\",\n \"position_held\",\n \"employer\",\n \"educated_at\",\n \"student_of\",\n \"parent\",\n \"child\",\n \"spouse\",\n \"sibling\",\n \"relative\",\n \"replaces\",\n \"replaced_by\",\n \"precedes\",\n \"follows\",\n \"influenced_by\",\n \"influences\",\n \"participant_in\",\n \"participant\",\n \"conflict\",\n \"opposes\",\n \"allied_with\",\n \"supports\",\n \"causes\",\n \"caused_by\",\n \"results_in\",\n \"has_effect\",\n \"uses\",\n \"used_by\",\n \"applies_to\",\n \"based_on\",\n \"derived_from\",\n \"depicts\",\n \"named_after\",\n \"has_characteristic\",\n \"related_to\",\n \"associated_with\",\n \"connected_to\",\n \"corresponds_to\",\n \"main_subject\",\n \"about\",\n \"context_for\",\n \"described_as\",\n \"refers_to\",\n \"concerns\",\n \"discusses\",\n \"compares_with\",\n \"overlaps_with\",\n \"coexists_with\",\n \"parallel_to\",\n \"analogous_to\",\n \"mentioned_with\",\n \"points_to\",\n \"interpreted_as\",\n \"seen_as\",\n \"different_from\",\n \"distinct_from\",\n \"not_same_as\",\n \"not_to_be_confused_with\",\n \"contrasts_with\",\n \"opposite_of\",\n \"conflicts_with\",\n \"contradicts\",\n \"excludes\",\n \"incompatible_with\",\n \"rejects\",\n \"criticizes\",\n \"denies\",\n \"not_reducible_to\",\n] as const;\n\nconst resolver = new EvidenceResolver();\n\nexport async function discoverWikilinkRelations(\n options: DiscoverWikilinkRelationsOptions,\n): Promise<readonly WikilinkDiscoveredRelation[]> {\n if (options.window.mentions.length < 2) {\n return [];\n }\n\n try {\n return await requestGuaranteedJson({\n messages: buildRelationMessages(options),\n parse: (response) => parseRelationResponse(options, response),\n request: options.request,\n responseIntentClassifierPrompt:\n RESPONSE_INTENT_CLASSIFIER_PROMPT_TEMPLATE,\n schema: relationResponseSchema,\n ...(options.maxRetries === undefined\n ? {}\n : { maxRetries: options.maxRetries }),\n });\n } catch {\n return [];\n }\n}\n\nfunction parseRelationResponse(\n options: DiscoverWikilinkRelationsOptions,\n response: z.infer<typeof relationResponseSchema>,\n): readonly WikilinkDiscoveredRelation[] {\n const mentionsById = new Map(\n options.window.mentions.map((mention) => [mention.id, mention]),\n );\n const projection = new FragmentProjection(\n options.sentences.map((sentence, index) => ({\n sentenceId: [options.chapterId, (options.fragmentId ?? 0) + index],\n text: sentence.text,\n wordsCount: sentence.wordsCount,\n })),\n );\n const sentenceOffsets = buildSentenceOffsets(options.sentences);\n const evidenceSentences = projection.sentences.flatMap(\n (sentence, sentenceIndex) => {\n const offset = sentenceOffsets[sentenceIndex];\n\n if (\n offset === undefined ||\n !rangesOverlap(offset, options.window.range)\n ) {\n return [];\n }\n\n return [\n {\n id: `S${sentenceIndex + 1}`,\n sentenceId: sentence.sentenceId,\n text: sentence.projectedText,\n },\n ];\n },\n );\n const issues: string[] = [];\n const links: WikilinkDiscoveredRelation[] = [];\n const seenKeys = new Set<string>();\n const sentenceIds = evidenceSentences.map((sentence) => sentence.sentenceId);\n const sentenceTexts = evidenceSentences.map((sentence) => sentence.text);\n\n for (const [index, relation] of response.relations.entries()) {\n const prefix = `relations[${index}]`;\n const source = mentionsById.get(relation.sourceMentionId);\n const target = mentionsById.get(relation.targetMentionId);\n\n if (source === undefined) {\n issues.push(`${prefix}.sourceMentionId references an unknown mention.`);\n continue;\n }\n if (target === undefined) {\n issues.push(`${prefix}.targetMentionId references an unknown mention.`);\n continue;\n }\n if (source.id === target.id) {\n issues.push(\n `${prefix} links a mention to itself. ` +\n \"Use two different tagged mention IDs, or remove the relation if the other endpoint is not tagged.\",\n );\n continue;\n }\n\n const predicate = normalizePredicate(relation.predicate);\n\n if (predicate === \"\") {\n issues.push(`${prefix}.predicate normalizes to an empty label.`);\n continue;\n }\n\n if (predicate === \"mentions\") {\n issues.push(\n `${prefix}.predicate must be semantic; \"mentions\" is not a relation predicate.`,\n );\n continue;\n }\n\n const [selectionResolution, selectionFailure] = resolveRelationEvidence({\n evidence: createRelationEvidenceSelection(relation.evidence),\n sentences: evidenceSentences,\n });\n const [anchorResolution, anchorFailure] =\n !Array.isArray(relation.evidence) &&\n selectionResolution === undefined &&\n selectionFailure === undefined\n ? resolver.resolve(relation.evidence, sentenceIds, sentenceTexts)\n : [undefined, undefined];\n const effectiveResolution = selectionResolution ?? anchorResolution;\n const effectiveFailure =\n selectionFailure === undefined\n ? anchorFailure\n : toEvidenceResolutionFailure(selectionFailure);\n\n if (effectiveResolution === undefined) {\n issues.push(\n `${prefix}.evidence could not be resolved: ${\n effectiveFailure?.message ?? \"unknown failure\"\n }`,\n );\n continue;\n }\n\n const evidenceSentenceIds = dedupeSentenceIds(\n effectiveResolution.sentenceIds,\n );\n\n if (evidenceSentenceIds.length === 0) {\n issues.push(`${prefix}.evidence resolved to no sentences.`);\n continue;\n }\n\n if (\n evidenceSentenceIds.some(\n ([chapterId, sentenceIndex]) =>\n chapterId !== options.chapterId ||\n sentenceOffsets[sentenceIndex - (options.fragmentId ?? 0)] ===\n undefined,\n )\n ) {\n issues.push(`${prefix}.evidence resolved outside this fragment.`);\n continue;\n }\n\n const key = `${source.id}\\0${target.id}\\0${predicate}\\0${evidenceSentenceIds.map((sentenceId) => sentenceId.join(\":\")).join(\",\")}`;\n\n if (seenKeys.has(key)) {\n continue;\n }\n seenKeys.add(key);\n\n links.push({\n ...(relation.confidence === undefined\n ? {}\n : { confidence: relation.confidence }),\n evidenceSentenceIds,\n predicate,\n sourceMentionId: source.id,\n targetMentionId: target.id,\n });\n }\n\n if (issues.length > 0) {\n throw new ParsedJsonError(issues);\n }\n\n return links;\n}\n\nfunction dedupeSentenceIds(\n sentenceIds: readonly SentenceId[],\n): readonly SentenceId[] {\n const seen = new Set<string>();\n const deduped: SentenceId[] = [];\n\n for (const sentenceId of sentenceIds) {\n const key = sentenceId.join(\":\");\n\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n deduped.push(sentenceId);\n }\n\n return deduped.sort(\n ([leftChapter, leftSentence], [rightChapter, rightSentence]) =>\n leftChapter - rightChapter || leftSentence - rightSentence,\n );\n}\n\nfunction buildRelationMessages(\n input: DiscoverWikilinkRelationsOptions,\n): LLMessage[] {\n return [\n {\n content: formatRelationSystemPrompt(),\n role: \"system\",\n },\n {\n content: formatRelationUserPrompt(input),\n role: \"user\",\n },\n ];\n}\n\nfunction formatRelationSystemPrompt(): string {\n return [\n \"You discover semantic relations between grounded entity mentions in source text.\",\n \"\",\n \"Rules:\",\n \"- Return JSON only.\",\n \"- Return only relations that are directly supported by the source text.\",\n '- Every relation must connect two mention IDs from the <mention id=\"...\" qid=\"...\"> tags.',\n \"- A relation is valid only when both endpoints are explicitly tagged mentions in the source context.\",\n \"- If a supported relation would require a subject or object that is not tagged with a mention ID, skip that relation.\",\n \"- Do not invent mention IDs, and do not reuse another mention as a placeholder for an untagged endpoint.\",\n \"- sourceMentionId and targetMentionId must be different mention IDs; never link a mention to itself.\",\n \"- Do not create a relation just because two mentions are nearby.\",\n \"- If the text explicitly states both a positive relation and a negated/distinction relation between mentions, return both relations when both are directly supported.\",\n \"- Prefer the suggested predicates when one fits.\",\n \"- If none fits, create one short snake_case predicate.\",\n \"- Never use mentions as a predicate.\",\n EVIDENCE_SELECTION_PROMPT_FRAGMENT,\n \"- Source sentences may contain XML-like mention tags. Use those tags for mention IDs, but ignore the tags when copying evidence quote text.\",\n \"- Do not use offsets or invented paraphrases as evidence.\",\n \"\",\n \"Suggested predicates:\",\n SUGGESTED_PREDICATES.join(\", \"),\n ].join(\"\\n\");\n}\n\nfunction formatRelationUserPrompt(\n input: DiscoverWikilinkRelationsOptions,\n): string {\n return [\n \"Source sentences with mention tags:\",\n formatTaggedEvidenceSentences(input),\n \"\",\n \"Return this JSON shape:\",\n JSON.stringify(\n {\n relations: [\n {\n confidence: 0.86,\n evidence: EVIDENCE_SELECTION_JSON_SHAPE,\n predicate: \"predicate_name\",\n sourceMentionId: \"source mention id\",\n targetMentionId: \"target mention id\",\n },\n ],\n },\n null,\n 2,\n ),\n \"\",\n \"If the source text does not support any semantic relation, return:\",\n JSON.stringify({ relations: [] }),\n ].join(\"\\n\");\n}\n\nfunction formatTaggedEvidenceSentences(\n input: DiscoverWikilinkRelationsOptions,\n): string {\n const sourceOffsets = buildSentenceOffsets(input.sentences);\n const lines = input.sentences.flatMap((sentence, index) => {\n const offset = sourceOffsets[index];\n\n if (offset === undefined || !rangesOverlap(offset, input.window.range)) {\n return [];\n }\n\n return [\n `S${index + 1}: ${normalizeEvidenceDisplayText(\n formatTaggedSentence({\n mentions: input.window.mentions,\n sentence,\n sentenceOffset: offset,\n }),\n )}`,\n ];\n });\n\n return lines.length === 0\n ? formatTaggedContext(input.window)\n : lines.join(\"\\n\");\n}\n\nfunction formatTaggedSentence(input: {\n readonly mentions: readonly WikilinkMention[];\n readonly sentence: WikilinkSentence;\n readonly sentenceOffset: { readonly end: number; readonly start: number };\n}): string {\n const mentions = input.mentions\n .filter(\n (mention) =>\n mention.range.start >= input.sentenceOffset.start &&\n mention.range.end <= input.sentenceOffset.end,\n )\n .sort((left, right) => left.range.start - right.range.start);\n const parts: string[] = [];\n let cursor = input.sentenceOffset.start;\n\n for (const mention of mentions) {\n parts.push(\n escapeXmlText(\n input.sentence.text.slice(\n cursor - input.sentenceOffset.start,\n mention.range.start - input.sentenceOffset.start,\n ),\n ),\n );\n parts.push(\n `<mention id=\"${escapeXmlAttribute(mention.id)}\" qid=\"${escapeXmlAttribute(\n mention.qid ?? \"\",\n )}\">${escapeXmlText(\n input.sentence.text.slice(\n mention.range.start - input.sentenceOffset.start,\n mention.range.end - input.sentenceOffset.start,\n ),\n )}</mention>`,\n );\n cursor = mention.range.end;\n }\n\n parts.push(\n escapeXmlText(\n input.sentence.text.slice(cursor - input.sentenceOffset.start),\n ),\n );\n\n return parts.join(\"\");\n}\n\nfunction rangesOverlap(\n left: { readonly end: number; readonly start: number },\n right: { readonly end: number; readonly start: number },\n): boolean {\n return left.start < right.end && right.start < left.end;\n}\n\nfunction resolveRelationEvidence(input: {\n readonly evidence: EvidenceSelectionList | undefined;\n readonly sentences: readonly EvidenceSelectionSentence[];\n}): readonly [\n resolution:\n | {\n readonly sentenceIds: readonly (readonly [number, number])[];\n }\n | undefined,\n failure:\n | {\n readonly candidates: readonly EvidenceSelectionCandidate[];\n readonly code: string;\n readonly message: string;\n }\n | undefined,\n] {\n if (input.evidence === undefined) {\n return [undefined, undefined];\n }\n\n return resolveEvidenceSelectionList({\n evidence: input.evidence,\n sentences: input.sentences,\n });\n}\n\nfunction createRelationEvidenceSelection(\n evidence: RelationEvidenceData,\n): EvidenceSelectionList | undefined {\n if (Array.isArray(evidence)) {\n return evidence.map(createRelationEvidenceSelectionItem);\n }\n\n const hasSelectionEvidence =\n typeof evidence.quote === \"string\" ||\n typeof evidence.sentence_id === \"string\";\n\n return hasSelectionEvidence\n ? createRelationEvidenceSelectionItem(evidence)\n : undefined;\n}\n\nfunction createRelationEvidenceSelectionItem(evidence: {\n readonly quote?: unknown;\n readonly sentence_id?: unknown;\n}): {\n readonly quote?: string;\n readonly sentence_id?: string;\n} {\n return {\n ...(typeof evidence.quote === \"string\" ? { quote: evidence.quote } : {}),\n ...(typeof evidence.sentence_id === \"string\"\n ? { sentence_id: evidence.sentence_id }\n : {}),\n };\n}\n\nfunction toEvidenceResolutionFailure(failure: {\n readonly candidates: readonly EvidenceSelectionCandidate[];\n readonly code: string;\n readonly message: string;\n}): EvidenceResolutionFailure {\n return {\n candidates: failure.candidates.map((candidate) => ({\n exactNormalized: candidate.exactNormalized,\n exactRaw: candidate.exactRaw,\n exactSubstring: candidate.exactSubstring,\n index: candidate.index,\n nextText: candidate.nextText,\n occurrenceId: candidate.occurrenceId,\n prevText: candidate.prevText,\n score: candidate.score,\n sentenceId: candidate.sentence.sentenceId,\n text: candidate.sentence.text,\n })),\n code: failure.code,\n fieldName: \"evidence\",\n message: failure.message,\n };\n}\n\nfunction formatTaggedContext(window: WikilinkEvidenceWindow): string {\n const mentions = [...window.mentions].sort(\n (left, right) => left.range.start - right.range.start,\n );\n const parts: string[] = [];\n let cursor = window.range.start;\n\n for (const mention of mentions) {\n parts.push(\n escapeXmlText(\n window.text.slice(\n cursor - window.range.start,\n mention.range.start - window.range.start,\n ),\n ),\n );\n parts.push(\n `<mention id=\"${escapeXmlAttribute(mention.id)}\" qid=\"${escapeXmlAttribute(\n mention.qid ?? \"\",\n )}\">${escapeXmlText(\n window.text.slice(\n mention.range.start - window.range.start,\n mention.range.end - window.range.start,\n ),\n )}</mention>`,\n );\n cursor = mention.range.end;\n }\n\n parts.push(escapeXmlText(window.text.slice(cursor - window.range.start)));\n\n return parts.join(\"\");\n}\n\nfunction buildSentenceOffsets(\n sentences: readonly WikilinkSentence[],\n): ReadonlyArray<{ readonly end: number; readonly start: number }> {\n const offsets: Array<{ readonly end: number; readonly start: number }> = [];\n let cursor = 0;\n\n for (const sentence of sentences) {\n const start = cursor;\n const end = start + sentence.text.length;\n\n offsets.push({ end, start });\n cursor = end + 1;\n }\n\n return offsets;\n}\n\nfunction normalizePredicate(predicate: string): string {\n return predicate\n .trim()\n .replace(/[\\s-]+/gu, \"_\")\n .replace(/_+/gu, \"_\")\n .replace(/^_+|_+$/gu, \"\")\n .toLowerCase();\n}\n\nfunction escapeXmlAttribute(value: string): string {\n return escapeXmlText(value).replaceAll('\"', \""\");\n}\n\nfunction escapeXmlText(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\");\n}\n","import type {\n BuildWikilinkEvidenceWindowsOptions,\n WikilinkEvidenceWindow,\n WikilinkMention,\n} from \"./types.js\";\n\nexport function buildWikilinkEvidenceWindows(\n options: BuildWikilinkEvidenceWindowsOptions,\n): readonly WikilinkEvidenceWindow[] {\n validateOptions(options);\n\n const mentions = normalizeMentions(options.mentions);\n\n if (mentions.length === 0 || options.text.length === 0) {\n return [];\n }\n\n const stride = options.windowLength - options.maxEvidenceDistance;\n const windows: WikilinkEvidenceWindow[] = [];\n\n for (\n let start = 0;\n start < options.text.length;\n start = nextWindowStart(start, stride, options.text.length)\n ) {\n const end = Math.min(start + options.windowLength, options.text.length);\n const windowMentions = mentions.filter(\n (mention) => mention.range.start >= start && mention.range.end <= end,\n );\n\n if (windowMentions.length >= 2) {\n windows.push({\n baseOffset: start,\n mentions: windowMentions,\n range: { end, start },\n text: options.text.slice(start, end),\n });\n }\n if (end >= options.text.length) {\n break;\n }\n }\n\n return windows;\n}\n\nfunction nextWindowStart(\n current: number,\n stride: number,\n textLength: number,\n): number {\n return Math.min(current + stride, textLength);\n}\n\nfunction validateOptions(options: BuildWikilinkEvidenceWindowsOptions): void {\n if (!Number.isInteger(options.windowLength) || options.windowLength <= 0) {\n throw new Error(\"windowLength must be a positive integer.\");\n }\n if (\n !Number.isInteger(options.maxEvidenceDistance) ||\n options.maxEvidenceDistance <= 0\n ) {\n throw new Error(\"maxEvidenceDistance must be a positive integer.\");\n }\n if (options.windowLength <= options.maxEvidenceDistance) {\n throw new Error(\n \"windowLength must be greater than maxEvidenceDistance so eligible mention pairs are guaranteed to share at least one evidence window.\",\n );\n }\n}\n\nfunction normalizeMentions(\n mentions: readonly WikilinkMention[],\n): readonly WikilinkMention[] {\n return mentions\n .filter((mention) => mention.range.start < mention.range.end)\n .sort(compareMention);\n}\n\nfunction compareMention(left: WikilinkMention, right: WikilinkMention): number {\n return (\n left.range.start - right.range.start ||\n left.range.end - right.range.end ||\n left.id.localeCompare(right.id)\n );\n}\n","export { discoverWikilinkRelations } from \"./relation-discovery.js\";\nexport { buildWikilinkEvidenceWindows } from \"./window.js\";\nexport type {\n DiscoverWikilinkRelationsOptions,\n WikilinkDiscoveredRelation,\n WikilinkSentence,\n} from \"./relation-discovery.js\";\nexport type {\n BuildWikilinkEvidenceWindowsOptions,\n WikilinkEvidenceWindow,\n WikilinkMention,\n} from \"./types.js\";\n","import { createReadStream, createWriteStream } from \"fs\";\nimport { mkdir, rm } from \"fs/promises\";\nimport { createInterface } from \"readline\";\nimport { join } from \"path\";\nimport { z } from \"zod\";\n\nimport type {\n GuaranteedRequest,\n GuaranteedRequestController,\n} from \"../guaranteed/index.js\";\nimport type {\n Document,\n FragmentRecord,\n MentionLinkRecord,\n MentionRecord,\n ReadonlyDocument,\n SentenceId,\n} from \"../document/index.js\";\nimport { LanguageCode, normalizeLanguageCode } from \"../common/language.js\";\nimport type { WikipageResolveProgress } from \"../wikipage/index.js\";\nimport {\n buildWikimatchSurfaceProtectionInput,\n buildWikimatchWindows,\n enrichWikimatchCandidates,\n filterCandidateQidOptions,\n judgeWikimatchPolicy,\n judgeWikimatchSurfaceProtection,\n listCandidateSelectableQids,\n matchWikispineSentenceCandidates,\n type WikimatchAcceptedMention,\n type WikimatchCandidate,\n type WikimatchSentence,\n type MatchWikispineSentenceCandidatesOptions,\n} from \"../wikimatch/index.js\";\nimport type { WikipageResolverOptions } from \"../wikipage/index.js\";\nimport {\n buildWikilinkEvidenceWindows,\n discoverWikilinkRelations,\n type WikilinkEvidenceWindow,\n type WikilinkMention,\n type WikilinkSentence,\n} from \"../wikilink/index.js\";\n\nimport { getChapterDetails, type ChapterDetails } from \"./chapter.js\";\nimport type { BuildJobProgressReporter } from \"./build-queue.js\";\n\nexport interface ChapterKnowledgeGraphBuildArtifact {\n readonly chapterId: number;\n readonly mentionLinksPath: string;\n readonly mentionsPath: string;\n readonly parameter: GraphBuildParameterInput;\n readonly workspacePath: string;\n}\n\nexport interface ChapterKnowledgeGraphInputSnapshot {\n readonly details: ChapterDetails;\n readonly fragments: readonly FragmentRecord[];\n}\n\nexport interface GraphBuildParameterInput {\n readonly language?: string;\n readonly prompt: string;\n}\n\nexport interface BuildChapterKnowledgeGraphArtifactOptions {\n readonly mentionLinks:\n | AsyncIterable<MentionLinkRecord>\n | Iterable<MentionLinkRecord>;\n readonly mentions: AsyncIterable<MentionRecord> | Iterable<MentionRecord>;\n readonly parameter?: GraphBuildParameterInput;\n readonly workspacePath: string;\n}\n\nexport interface GenerateChapterKnowledgeGraphArtifactOptions {\n readonly policyPrompt: string;\n readonly progressTracker?: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >;\n readonly request: GuaranteedRequestController;\n readonly resolverOptions?: Omit<WikipageResolverOptions, \"progress\">;\n readonly wikispine?: Pick<\n MatchWikispineSentenceCandidatesOptions,\n \"command\" | \"dataDir\" | \"endpoint\" | \"provider\"\n >;\n readonly workspacePath: string;\n}\n\nconst mentionRecordSchema = z.object({\n id: z.string().min(1),\n chapterId: z.number().int(),\n sentenceIndex: z.number().int().nonnegative().optional(),\n rangeStart: z.number().int().nonnegative(),\n rangeEnd: z.number().int().nonnegative(),\n surface: z.string().min(1),\n qid: z.string().regex(/^Q[1-9][0-9]*$/),\n confidence: z.number().min(0).max(1).optional(),\n note: z.string().optional(),\n});\n\nconst sentenceIdSchema = z\n .tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])\n .readonly();\n\nconst mentionLinkRecordSchema = z.object({\n id: z.string().min(1),\n sourceMentionId: z.string().min(1),\n targetMentionId: z.string().min(1),\n predicate: z.string().min(1),\n evidenceSentenceIds: z.array(sentenceIdSchema).min(1),\n confidence: z.number().min(0).max(1).optional(),\n note: z.string().optional(),\n});\n\nconst WIKIMATCH_GROUNDING_DEFAULT_OPTION_BUDGETS = [5, 10, 20, 35] as const;\nconst WIKIMATCH_GROUNDING_PRIOR_OPTION_BUDGETS = [3, 5, 10, 20, 35] as const;\nconst WIKIMATCH_GROUNDING_MAX_OPTION_BUDGET = 50;\nconst WIKIMATCH_GROUNDING_SURFACE_PRIOR_THRESHOLD = 3;\nconst WIKIMATCH_SURFACE_PROTECTION_PERCENTILE = 0.1;\nconst WIKILINK_EVIDENCE_DISTANCE = 700;\nconst WIKILINK_WINDOW_LENGTH = 1800;\n\nexport async function generateChapterKnowledgeGraphArtifact(\n document: ReadonlyDocument,\n chapterId: number,\n options: GenerateChapterKnowledgeGraphArtifactOptions,\n): Promise<ChapterKnowledgeGraphBuildArtifact> {\n return await generateChapterKnowledgeGraphArtifactFromSnapshot(\n chapterId,\n await snapshotChapterKnowledgeGraphInput(document, chapterId),\n options,\n );\n}\n\nexport async function snapshotChapterKnowledgeGraphInput(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<ChapterKnowledgeGraphInputSnapshot> {\n const details = await getChapterDetails(document, chapterId);\n\n if (details.stage === \"planned\") {\n throw new Error(\n `Chapter ${chapterId} is planned. Set source before generating Knowledge Graph.`,\n );\n }\n\n return {\n details,\n fragments: await readChapterFragments(document, chapterId),\n };\n}\n\nexport async function generateChapterKnowledgeGraphArtifactFromSnapshot(\n chapterId: number,\n snapshot: ChapterKnowledgeGraphInputSnapshot,\n options: GenerateChapterKnowledgeGraphArtifactOptions,\n): Promise<ChapterKnowledgeGraphBuildArtifact> {\n if (snapshot.details.chapterId !== chapterId) {\n throw new Error(\n `Knowledge Graph snapshot belongs to chapter ${snapshot.details.chapterId}, not chapter ${chapterId}.`,\n );\n }\n if (snapshot.details.stage === \"planned\") {\n throw new Error(\n `Chapter ${chapterId} is planned. Set source before generating Knowledge Graph.`,\n );\n }\n\n const fragments = snapshot.fragments;\n const text = joinFragmentText(fragments);\n const sentences = createWikimatchSentences(fragments);\n await options.progressTracker?.updatePhase({\n done: 0,\n phase: \"matching\",\n phaseDetail: \"text\",\n total: text.length,\n unit: \"char\",\n });\n const rawCandidates = await matchWikispineSentenceCandidates({\n includeDisambiguation: true,\n onProgress: async (progress) => {\n await options.progressTracker?.updatePhase({\n done: progress.coveredRangeEnd,\n force: false,\n phase: \"matching\",\n phaseDetail: \"text\",\n total: text.length,\n unit: \"char\",\n });\n },\n ...(options.wikispine ?? {}),\n sentences,\n });\n await options.progressTracker?.throwIfStopped();\n await options.progressTracker?.updatePhase({\n done: text.length,\n phase: \"matching\",\n phaseDetail: \"text\",\n total: text.length,\n unit: \"char\",\n });\n const screenedCandidates = await screenCandidates({\n candidates: rawCandidates,\n policyPrompt: options.policyPrompt,\n ...(options.progressTracker === undefined\n ? {}\n : { progressTracker: options.progressTracker }),\n request: options.request,\n text,\n });\n await options.progressTracker?.throwIfStopped();\n const qidCount = countUniqueQids(screenedCandidates);\n await options.progressTracker?.updatePhase({\n done: 0,\n phase: \"enrichment\",\n total: qidCount,\n unit: \"qid\",\n });\n const enrichedCandidates = await enrichWikimatchCandidates(\n screenedCandidates,\n {\n ...(options.progressTracker === undefined\n ? {}\n : {\n progress: createEnrichmentProgressReporter(options.progressTracker),\n }),\n ...(options.resolverOptions === undefined\n ? {}\n : { resolverOptions: options.resolverOptions }),\n },\n );\n await options.progressTracker?.throwIfStopped();\n await options.progressTracker?.updatePhase({\n done: qidCount,\n phase: \"enrichment\",\n total: qidCount,\n unit: \"qid\",\n });\n const mentions = await judgeCandidates({\n candidates: enrichedCandidates,\n chapterId,\n fragments,\n policyPrompt: options.policyPrompt,\n ...(options.progressTracker === undefined\n ? {}\n : { progressTracker: options.progressTracker }),\n request: options.request,\n text,\n });\n await options.progressTracker?.throwIfStopped();\n const mentionLinks = await discoverMentionLinks({\n fragments,\n mentions,\n ...(options.progressTracker === undefined\n ? {}\n : { progressTracker: options.progressTracker }),\n request: options.request,\n });\n await options.progressTracker?.throwIfStopped();\n const artifact = await buildChapterKnowledgeGraphArtifact(chapterId, {\n mentionLinks,\n mentions,\n parameter: createKnowledgeGraphParameterInput(options),\n workspacePath: options.workspacePath,\n });\n\n return artifact;\n}\n\nexport async function buildChapterKnowledgeGraphArtifact(\n chapterId: number,\n options: BuildChapterKnowledgeGraphArtifactOptions,\n): Promise<ChapterKnowledgeGraphBuildArtifact> {\n const workspacePath = join(\n options.workspacePath,\n \"knowledge-graph\",\n `chapter-${chapterId}`,\n );\n const mentionsPath = join(workspacePath, \"mentions.jsonl\");\n const mentionLinksPath = join(workspacePath, \"mention-links.jsonl\");\n\n await rm(workspacePath, { force: true, recursive: true });\n await mkdir(workspacePath, { recursive: true });\n await writeJsonl(mentionsPath, options.mentions, parseMentionRecord);\n await writeJsonl(\n mentionLinksPath,\n options.mentionLinks,\n parseMentionLinkRecord,\n );\n\n return {\n chapterId,\n mentionLinksPath,\n mentionsPath,\n parameter: options.parameter ?? {\n language: LanguageCode.Chinese,\n prompt: \"\",\n },\n workspacePath,\n };\n}\n\nfunction createKnowledgeGraphParameterInput(\n options: Pick<\n GenerateChapterKnowledgeGraphArtifactOptions,\n \"policyPrompt\" | \"resolverOptions\"\n >,\n): GraphBuildParameterInput {\n return {\n language:\n normalizeLanguageCode(options.resolverOptions?.language) ??\n LanguageCode.Chinese,\n prompt: options.policyPrompt,\n };\n}\n\nasync function screenCandidates(input: {\n readonly candidates: readonly WikimatchCandidate[];\n readonly policyPrompt: string;\n readonly progressTracker?: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >;\n readonly request: GuaranteedRequestController;\n readonly text: string;\n}): Promise<readonly WikimatchCandidate[]> {\n if (input.candidates.length === 0) {\n return [];\n }\n\n const protectionInput = buildWikimatchSurfaceProtectionInput({\n candidates: input.candidates,\n percentile: WIKIMATCH_SURFACE_PROTECTION_PERCENTILE,\n text: input.text,\n });\n\n await input.progressTracker?.updatePhase({\n done: 0,\n phase: \"screening\",\n phaseDetail: `${protectionInput.suspiciousSurfaces.length} high-frequency surfaces`,\n total: 1,\n unit: \"window\",\n });\n await input.progressTracker?.throwIfStopped();\n const protection = await judgeWikimatchSurfaceProtection({\n policyPrompt: input.policyPrompt,\n request: input.request,\n suspiciousSurfaces: protectionInput.suspiciousSurfaces,\n });\n await input.progressTracker?.throwIfStopped();\n const protectedSurfaces = new Set(\n protection.protectedSurfaces.map((surface) => surface.text),\n );\n const allowedCandidateKeys = new Set(\n protectionInput.candidates.map(createCandidateRangeKey),\n );\n\n await input.progressTracker?.updatePhase({\n done: 1,\n phase: \"screening\",\n phaseDetail: `${protectedSurfaces.size} protected surfaces`,\n total: 1,\n unit: \"window\",\n });\n\n return protectionInput.suppressedCandidates.filter(\n (candidate) =>\n allowedCandidateKeys.has(createCandidateRangeKey(candidate)) ||\n protectedSurfaces.has(candidate.surface),\n );\n}\n\nfunction createCandidateRangeKey(candidate: WikimatchCandidate): string {\n return `${candidate.range.start}\\0${candidate.range.end}\\0${candidate.surface}`;\n}\n\nasync function judgeCandidates(input: {\n readonly candidates: readonly WikimatchCandidate[];\n readonly chapterId: number;\n readonly fragments: readonly FragmentRecord[];\n readonly policyPrompt: string;\n readonly progressTracker?: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >;\n readonly request: GuaranteedRequestController;\n readonly text: string;\n}): Promise<readonly MentionRecord[]> {\n const mentions: MentionRecord[] = [];\n const sentenceLocations = buildSentenceLocations(input.fragments);\n const acceptedMentions = await groundWikimatchCandidates({\n candidates: input.candidates,\n policyPrompt: input.policyPrompt,\n ...(input.progressTracker === undefined\n ? {}\n : { progressTracker: input.progressTracker }),\n request: input.request,\n text: input.text,\n });\n let mentionIndex = 1;\n\n for (const mention of acceptedMentions) {\n const location = locateMention(sentenceLocations, mention.range.start);\n\n mentions.push(\n toMentionRecord(input.chapterId, mention, location, mentionIndex),\n );\n mentionIndex += 1;\n }\n\n return mentions;\n}\n\nexport async function groundWikimatchCandidates(input: {\n readonly candidates: readonly WikimatchCandidate[];\n readonly policyPrompt: string;\n readonly progressTracker?: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >;\n readonly request: GuaranteedRequestController;\n readonly text: string;\n}): Promise<readonly WikimatchAcceptedMention[]> {\n const mentions: WikimatchAcceptedMention[] = [];\n const candidatePages = createGroundingCandidatePages(input.candidates);\n let completedWindows = 0;\n let totalWindows = 0;\n\n let activeCandidates = candidatePages.nextPage();\n\n while (activeCandidates.length > 0) {\n const windows = buildWikimatchWindows({\n candidates: activeCandidates,\n contextWords: 220,\n optionBudget: WIKIMATCH_GROUNDING_MAX_OPTION_BUDGET,\n text: input.text,\n });\n\n totalWindows += windows.length;\n if (windows.length === 0) {\n break;\n }\n await input.progressTracker?.updatePhase({\n done: completedWindows,\n phase: \"grounding\",\n total: totalWindows,\n unit: \"window\",\n });\n\n const results = await mapLazyGuaranteedRequests(\n input.request,\n windows,\n async (window, request) => {\n try {\n await input.progressTracker?.throwIfStopped();\n return await judgeWikimatchPolicy({\n candidates: window.candidates,\n policyPrompt: input.policyPrompt,\n request,\n window,\n });\n } finally {\n completedWindows += 1;\n await input.progressTracker?.updatePhase({\n done: completedWindows,\n phase: \"grounding\",\n total: totalWindows,\n unit: \"window\",\n });\n }\n },\n );\n\n const continuedCandidateIds = new Set<string>();\n\n for (const result of results) {\n for (const mention of result.mentions) {\n mentions.push(mention);\n candidatePages.accept(mention);\n }\n for (const update of result.policyUpdates) {\n candidatePages.close(update.candidateId, update.decision);\n }\n for (const continuation of result.continuations) {\n for (const candidateId of continuation.candidateIds) {\n continuedCandidateIds.add(candidateId);\n candidatePages.continue(candidateId);\n }\n }\n }\n\n activeCandidates = candidatePages.nextPage(continuedCandidateIds);\n }\n\n await input.progressTracker?.updatePhase({\n done: completedWindows,\n phase: \"grounding\",\n phaseDetail: formatGroundingEfficiency(\n candidatePages.getStats(),\n mentions.length,\n ),\n total: totalWindows,\n unit: \"window\",\n });\n\n return mentions;\n}\n\nfunction createGroundingCandidatePages(\n candidates: readonly WikimatchCandidate[],\n): {\n readonly accept: (mention: WikimatchAcceptedMention) => void;\n readonly close: (\n candidateId: string,\n decision?: \"skip_this_time\" | \"never_recall\",\n ) => void;\n readonly continue: (candidateId: string) => void;\n readonly getStats: () => GroundingCandidatePageStats;\n readonly nextPage: (\n continuedCandidateIds?: ReadonlySet<string>,\n ) => readonly WikimatchCandidate[];\n} {\n const candidatesById = new Map(\n candidates.map((candidate) => [candidate.id, candidate]),\n );\n const shownQidsByCandidateId = new Map(\n candidates.map((candidate) => [candidate.id, new Set<string>()]),\n );\n const pageIndexesByCandidateId = new Map(\n candidates.map((candidate) => [candidate.id, 0]),\n );\n const closedCandidateIds = new Set<string>();\n const recallCounts = new Map<string, number>();\n const surfaceStats = new Map<string, GroundingSurfaceStats>();\n const stats: GroundingCandidatePageStats = {\n candidatePageCount: 0,\n qidAppearanceCount: 0,\n };\n\n return {\n accept(mention) {\n closedCandidateIds.add(mention.candidateId);\n getSurfaceStats(surfaceStats, mention.surface).recallCount += 1;\n recallCounts.set(\n createSurfaceQidKey(mention.surface, mention.qid),\n (recallCounts.get(createSurfaceQidKey(mention.surface, mention.qid)) ??\n 0) + 1,\n );\n },\n close(candidateId, decision) {\n closedCandidateIds.add(candidateId);\n const candidate = candidatesById.get(candidateId);\n\n if (\n candidate !== undefined &&\n (decision === \"skip_this_time\" || decision === \"never_recall\")\n ) {\n getSurfaceStats(surfaceStats, candidate.surface).rejectCount += 1;\n }\n },\n continue(candidateId) {\n const candidate = candidatesById.get(candidateId);\n\n if (candidate !== undefined) {\n getSurfaceStats(surfaceStats, candidate.surface).continueCount += 1;\n }\n },\n getStats() {\n return { ...stats };\n },\n nextPage(continuedCandidateIds) {\n const pageCandidates: WikimatchCandidate[] = [];\n const candidateIds =\n continuedCandidateIds === undefined\n ? candidates.map((candidate) => candidate.id)\n : [...continuedCandidateIds];\n\n for (const candidateId of candidateIds) {\n if (closedCandidateIds.has(candidateId)) {\n continue;\n }\n\n const candidate = candidatesById.get(candidateId);\n const shownQids = shownQidsByCandidateId.get(candidateId);\n\n if (candidate === undefined || shownQids === undefined) {\n continue;\n }\n\n const sortedCandidate = sortCandidateOptionsByRecall(\n candidate,\n recallCounts,\n );\n const selectableQids = listCandidateSelectableQids(sortedCandidate);\n const pageIndex = pageIndexesByCandidateId.get(candidateId) ?? 0;\n const optionBudget = getGroundingCandidateOptionBudget(\n getSurfaceStats(surfaceStats, candidate.surface),\n pageIndex,\n );\n const selectedQids = selectableQids\n .filter((qid) => !shownQids.has(qid))\n .slice(0, optionBudget);\n\n if (selectedQids.length === 0) {\n closedCandidateIds.add(candidateId);\n continue;\n }\n\n for (const qid of selectedQids) {\n shownQids.add(qid);\n }\n\n const hasMoreOptions = selectableQids.some(\n (qid) => !shownQids.has(qid),\n );\n const pageCandidate = filterCandidateQidOptions(\n sortedCandidate,\n new Set(selectedQids),\n );\n\n if (!hasMoreOptions) {\n closedCandidateIds.add(candidateId);\n }\n getSurfaceStats(surfaceStats, candidate.surface).seenCount += 1;\n pageIndexesByCandidateId.set(candidateId, pageIndex + 1);\n stats.candidatePageCount += 1;\n stats.qidAppearanceCount += selectedQids.length;\n pageCandidates.push({\n ...pageCandidate,\n ...(hasMoreOptions ? { hasMoreOptions: true } : {}),\n });\n }\n\n return pageCandidates;\n },\n };\n}\n\ninterface GroundingSurfaceStats {\n continueCount: number;\n recallCount: number;\n rejectCount: number;\n seenCount: number;\n}\n\ninterface GroundingCandidatePageStats {\n candidatePageCount: number;\n qidAppearanceCount: number;\n}\n\nfunction getSurfaceStats(\n surfaceStats: Map<string, GroundingSurfaceStats>,\n surface: string,\n): GroundingSurfaceStats {\n const existing = surfaceStats.get(surface);\n\n if (existing !== undefined) {\n return existing;\n }\n\n const created = {\n continueCount: 0,\n recallCount: 0,\n rejectCount: 0,\n seenCount: 0,\n };\n surfaceStats.set(surface, created);\n\n return created;\n}\n\nfunction getGroundingCandidateOptionBudget(\n stats: GroundingSurfaceStats,\n pageIndex: number,\n): number {\n const budgets = hasStrongGroundingSurfacePrior(stats)\n ? WIKIMATCH_GROUNDING_PRIOR_OPTION_BUDGETS\n : WIKIMATCH_GROUNDING_DEFAULT_OPTION_BUDGETS;\n\n return budgets[Math.min(pageIndex, budgets.length - 1)]!;\n}\n\nfunction hasStrongGroundingSurfacePrior(stats: GroundingSurfaceStats): boolean {\n if (stats.seenCount < WIKIMATCH_GROUNDING_SURFACE_PRIOR_THRESHOLD) {\n return false;\n }\n\n return (\n (stats.recallCount >= 2 && stats.continueCount === 0) ||\n (stats.rejectCount >= 2 && stats.recallCount === 0) ||\n stats.continueCount >= WIKIMATCH_GROUNDING_SURFACE_PRIOR_THRESHOLD\n );\n}\n\nfunction formatGroundingEfficiency(\n stats: GroundingCandidatePageStats,\n mentionCount: number,\n): string {\n const qidPerMention =\n mentionCount === 0\n ? \"n/a\"\n : (stats.qidAppearanceCount / mentionCount).toFixed(1);\n\n return `efficiency qid/mention=${qidPerMention} qids=${stats.qidAppearanceCount} mentions=${mentionCount} pages=${stats.candidatePageCount}`;\n}\n\nfunction sortCandidateOptionsByRecall(\n candidate: WikimatchCandidate,\n recallCounts: ReadonlyMap<string, number>,\n): WikimatchCandidate {\n return {\n ...candidate,\n qidOptions: [...candidate.qidOptions].sort((left, right) => {\n return (\n getOptionRecallScore(candidate.surface, right, recallCounts) -\n getOptionRecallScore(candidate.surface, left, recallCounts)\n );\n }),\n };\n}\n\nfunction getOptionRecallScore(\n surface: string,\n option: WikimatchCandidate[\"qidOptions\"][number],\n recallCounts: ReadonlyMap<string, number>,\n): number {\n const directScore =\n recallCounts.get(createSurfaceQidKey(surface, option.qid)) ?? 0;\n const disambiguationScore =\n option.disambiguation?.linkedQids.reduce(\n (total, item) =>\n total + (recallCounts.get(createSurfaceQidKey(surface, item.qid)) ?? 0),\n 0,\n ) ?? 0;\n const profileScore =\n option.disambiguation?.profile?.meanings.reduce(\n (total, item) =>\n total + (recallCounts.get(createSurfaceQidKey(surface, item.qid)) ?? 0),\n 0,\n ) ?? 0;\n\n return directScore + disambiguationScore + profileScore;\n}\n\nfunction createSurfaceQidKey(surface: string, qid: string): string {\n return `${surface}\\0${qid}`;\n}\n\nexport function createEnrichmentProgressReporter(\n progressTracker: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >,\n): (event: WikipageResolveProgress) => Promise<void> {\n return async (event) => {\n await progressTracker.throwIfStopped();\n const phase = formatEnrichmentProgressPhase(event);\n\n await progressTracker.updatePhase({\n done: event.done,\n ...(phase.detail === undefined ? {} : { phaseDetail: phase.detail }),\n phase: \"enrichment\",\n total: event.total,\n unit: phase.unit,\n });\n };\n}\n\nfunction formatEnrichmentProgressPhase(event: WikipageResolveProgress): {\n readonly detail?: string;\n readonly unit: \"page\" | \"qid\" | \"record\";\n} {\n switch (event.detail) {\n case \"disambiguation-page\":\n return {\n detail: \"disambiguation\",\n unit: \"page\",\n };\n case \"entity\":\n return {\n detail: \"entity\",\n unit: \"record\",\n };\n case \"linked-page\":\n return {\n detail: \"linked\",\n unit: \"page\",\n };\n case \"page\":\n return {\n detail: \"page\",\n unit: \"page\",\n };\n case \"qid\":\n return {\n unit: \"qid\",\n };\n }\n}\n\nasync function mapLazyGuaranteedRequests<TItem, TResult>(\n request: GuaranteedRequestController,\n items: readonly TItem[],\n operation: (item: TItem, request: GuaranteedRequest) => Promise<TResult>,\n): Promise<readonly TResult[]> {\n const lazy = request.lazy;\n\n if (lazy !== undefined) {\n return await Promise.all(\n items.map(\n async (item) =>\n await lazy(async (request) => await operation(item, request)),\n ),\n );\n }\n\n const results: TResult[] = [];\n\n for (const item of items) {\n results.push(await operation(item, request));\n }\n\n return results;\n}\n\nasync function discoverMentionLinks(input: {\n readonly fragments: readonly FragmentRecord[];\n readonly mentions: readonly MentionRecord[];\n readonly progressTracker?: Pick<\n BuildJobProgressReporter,\n \"throwIfStopped\" | \"updatePhase\"\n >;\n readonly request: GuaranteedRequestController;\n}): Promise<readonly MentionLinkRecord[]> {\n const fragmentWindows = buildMentionLinkWindows(\n input.fragments,\n input.mentions,\n );\n let completedWindows = 0;\n\n await input.progressTracker?.updatePhase({\n done: 0,\n phase: \"relation-discovery\",\n total: fragmentWindows.length,\n unit: \"window\",\n });\n\n const discoveredLinks = (\n await mapLazyGuaranteedRequests(\n input.request,\n fragmentWindows,\n async (item, request) => {\n try {\n await input.progressTracker?.throwIfStopped();\n return await discoverWikilinkRelations({\n chapterId: item.fragment.serialId,\n fragmentId: item.fragment.fragmentId,\n request,\n sentences: item.fragment.sentences,\n window: item.window,\n });\n } finally {\n completedWindows += 1;\n await input.progressTracker?.updatePhase({\n done: completedWindows,\n phase: \"relation-discovery\",\n total: fragmentWindows.length,\n unit: \"window\",\n });\n }\n },\n )\n ).flat();\n\n return discoveredLinks.map((link, index) => ({\n ...(link.confidence === undefined ? {} : { confidence: link.confidence }),\n evidenceSentenceIds: link.evidenceSentenceIds,\n id: `l${input.fragments[0]?.serialId ?? 0}-${index + 1}`,\n predicate: link.predicate,\n sourceMentionId: link.sourceMentionId,\n targetMentionId: link.targetMentionId,\n }));\n}\n\nfunction buildMentionLinkWindows(\n fragments: readonly FragmentRecord[],\n mentions: readonly MentionRecord[],\n): ReadonlyArray<{\n readonly fragment: FragmentRecord;\n readonly window: WikilinkEvidenceWindow;\n}> {\n return fragments.flatMap((fragment) => {\n const startSentenceIndex = fragment.fragmentId;\n const endSentenceIndex = startSentenceIndex + fragment.sentences.length - 1;\n const fragmentMentions = toWikilinkMentions(\n fragment.sentences,\n mentions.filter(\n (mention) =>\n mention.sentenceIndex !== undefined &&\n mention.sentenceIndex >= startSentenceIndex &&\n mention.sentenceIndex <= endSentenceIndex,\n ),\n startSentenceIndex,\n );\n const windows = buildWikilinkEvidenceWindows({\n maxEvidenceDistance: WIKILINK_EVIDENCE_DISTANCE,\n mentions: fragmentMentions,\n text: joinSentences(fragment.sentences),\n windowLength: WIKILINK_WINDOW_LENGTH,\n });\n\n return windows.map((window) => ({\n fragment,\n window,\n }));\n });\n}\n\nfunction toWikilinkMentions(\n sentences: readonly WikilinkSentence[],\n mentions: readonly MentionRecord[],\n startSentenceIndex: number,\n): readonly WikilinkMention[] {\n const sentenceOffsets = buildFragmentSentenceOffsets(sentences);\n\n return mentions.flatMap((mention) => {\n if (mention.sentenceIndex === undefined) {\n return [];\n }\n\n const sentenceOffset =\n sentenceOffsets[mention.sentenceIndex - startSentenceIndex];\n\n if (sentenceOffset === undefined) {\n return [];\n }\n\n return [\n {\n id: mention.id,\n qid: mention.qid,\n range: {\n end: sentenceOffset.start + mention.rangeEnd,\n start: sentenceOffset.start + mention.rangeStart,\n },\n surface: mention.surface,\n },\n ];\n });\n}\n\nfunction buildFragmentSentenceOffsets(\n sentences: readonly WikilinkSentence[],\n): ReadonlyArray<{ readonly end: number; readonly start: number }> {\n const offsets: Array<{ readonly end: number; readonly start: number }> = [];\n let cursor = 0;\n\n for (const sentence of sentences) {\n const start = cursor;\n const end = start + sentence.text.length;\n\n offsets.push({ end, start });\n cursor = end + 1;\n }\n\n return offsets;\n}\n\nfunction joinSentences(sentences: readonly WikilinkSentence[]): string {\n return sentences.map((sentence) => sentence.text).join(\" \");\n}\n\nfunction countUniqueQids(candidates: readonly WikimatchCandidate[]): number {\n return new Set(\n candidates.flatMap((candidate) =>\n candidate.qidOptions.map((option) => option.qid),\n ),\n ).size;\n}\n\nfunction toMentionRecord(\n chapterId: number,\n mention: WikimatchAcceptedMention,\n location: {\n readonly rangeStart: number;\n readonly sentenceIndex: number;\n },\n index: number,\n): MentionRecord {\n return {\n chapterId,\n ...(mention.confidence === undefined\n ? {}\n : { confidence: mention.confidence }),\n id: `m${chapterId}-${index}`,\n ...(mention.note === undefined ? {} : { note: mention.note }),\n qid: mention.qid,\n rangeEnd: location.rangeStart + mention.surface.length,\n rangeStart: location.rangeStart,\n sentenceIndex: location.sentenceIndex,\n surface: mention.surface,\n };\n}\n\ninterface SentenceLocation {\n readonly absoluteStart: number;\n readonly length: number;\n readonly sentenceIndex: number;\n}\n\nfunction buildSentenceLocations(\n fragments: readonly FragmentRecord[],\n): readonly SentenceLocation[] {\n const locations: SentenceLocation[] = [];\n let offset = 0;\n\n for (const fragment of fragments) {\n for (\n let sentenceIndex = 0;\n sentenceIndex < fragment.sentences.length;\n sentenceIndex += 1\n ) {\n const length = fragment.sentences[sentenceIndex]!.text.length;\n\n locations.push({\n absoluteStart: offset,\n length,\n sentenceIndex: fragment.fragmentId + sentenceIndex,\n });\n offset += length + 1;\n }\n }\n\n return locations;\n}\n\nfunction locateMention(\n locations: readonly SentenceLocation[],\n absoluteOffset: number,\n): {\n readonly rangeStart: number;\n readonly sentenceIndex: number;\n} {\n for (const location of locations) {\n const rangeEnd = location.absoluteStart + location.length;\n\n if (absoluteOffset >= location.absoluteStart && absoluteOffset < rangeEnd) {\n return {\n rangeStart: absoluteOffset - location.absoluteStart,\n sentenceIndex: location.sentenceIndex,\n };\n }\n }\n\n throw new Error(`Mention offset ${absoluteOffset} is outside chapter text.`);\n}\n\nasync function readChapterFragments(\n document: ReadonlyDocument,\n chapterId: number,\n): Promise<readonly FragmentRecord[]> {\n const serialFragments = document.getSerialFragments(chapterId);\n\n return await Promise.all(\n (await serialFragments.listFragmentIds()).map(\n async (fragmentId) => await serialFragments.getFragment(fragmentId),\n ),\n );\n}\n\nfunction joinFragmentText(fragments: readonly FragmentRecord[]): string {\n return fragments\n .flatMap((fragment) => fragment.sentences.map((sentence) => sentence.text))\n .join(\" \");\n}\n\nfunction createWikimatchSentences(\n fragments: readonly FragmentRecord[],\n): readonly WikimatchSentence[] {\n const sentences: WikimatchSentence[] = [];\n let offset = 0;\n\n for (const fragment of fragments) {\n for (let index = 0; index < fragment.sentences.length; index += 1) {\n const sentence = fragment.sentences[index]!;\n\n sentences.push({\n id: `${fragment.serialId}:${fragment.fragmentId + index}`,\n range: {\n end: offset + sentence.text.length,\n start: offset,\n },\n text: sentence.text,\n });\n offset += sentence.text.length + 1;\n }\n }\n\n return sentences;\n}\n\nexport async function commitChapterKnowledgeGraphArtifact(\n document: Document,\n artifact: ChapterKnowledgeGraphBuildArtifact,\n): Promise<void> {\n const mentions = await readJsonl(artifact.mentionsPath, parseMentionRecord);\n const mentionLinks = await readJsonl(\n artifact.mentionLinksPath,\n parseMentionLinkRecord,\n );\n\n validateChapterKnowledgeGraphArtifact(artifact.chapterId, {\n mentionLinks,\n mentions,\n });\n\n await document.openSession(async (openedDocument) => {\n await openedDocument.serials.ensure(artifact.chapterId);\n const existingLinks = await openedDocument.mentionLinks.listByChapter(\n artifact.chapterId,\n );\n\n if (existingLinks.length > 0 && mentionLinks.length === 0) {\n throw new Error(\n `Refusing to replace chapter ${artifact.chapterId} knowledge graph with an artifact that contains no mention links.`,\n );\n }\n\n await openedDocument.mentionLinks.deleteByChapter(artifact.chapterId);\n await openedDocument.mentions.deleteByChapter(artifact.chapterId);\n await openedDocument.mentions.saveMany(mentions);\n await openedDocument.mentionLinks.saveMany(mentionLinks);\n const parameter = await openedDocument.graphBuildParameters.save(\n artifact.parameter,\n );\n await openedDocument.serials.setKnowledgeGraphReady(\n artifact.chapterId,\n true,\n parameter.hash,\n );\n });\n}\n\nexport async function clearChapterKnowledgeGraph(\n document: Document,\n chapterId: number,\n): Promise<void> {\n await document.openSession(async (openedDocument) => {\n await openedDocument.mentionLinks.deleteByChapter(chapterId);\n await openedDocument.mentions.deleteByChapter(chapterId);\n await openedDocument.serials.setKnowledgeGraphReady(chapterId, false);\n await openedDocument.graphBuildParameters.deleteUnreferenced();\n });\n}\n\nfunction validateChapterKnowledgeGraphArtifact(\n chapterId: number,\n records: {\n readonly mentionLinks: readonly MentionLinkRecord[];\n readonly mentions: readonly MentionRecord[];\n },\n): void {\n const mentionIds = new Set<string>();\n\n for (const mention of records.mentions) {\n if (mention.chapterId !== chapterId) {\n throw new Error(\n `Mention ${mention.id} belongs to chapter ${mention.chapterId}, expected chapter ${chapterId}.`,\n );\n }\n if (mention.rangeEnd <= mention.rangeStart) {\n throw new Error(\n `Mention ${mention.id} has invalid range [${mention.rangeStart}, ${mention.rangeEnd}).`,\n );\n }\n if (mentionIds.has(mention.id)) {\n throw new Error(`Duplicate mention id ${mention.id}.`);\n }\n\n mentionIds.add(mention.id);\n }\n\n const linkIds = new Set<string>();\n\n for (const link of records.mentionLinks) {\n if (linkIds.has(link.id)) {\n throw new Error(`Duplicate mention link id ${link.id}.`);\n }\n if (!mentionIds.has(link.sourceMentionId)) {\n throw new Error(\n `Mention link ${link.id} references unknown source mention ${link.sourceMentionId}.`,\n );\n }\n if (!mentionIds.has(link.targetMentionId)) {\n throw new Error(\n `Mention link ${link.id} references unknown target mention ${link.targetMentionId}.`,\n );\n }\n if (link.evidenceSentenceIds.length === 0) {\n throw new Error(`Mention link ${link.id} has no evidence sentences.`);\n }\n\n for (const sentenceId of link.evidenceSentenceIds) {\n if (sentenceId[0] !== chapterId) {\n throw new Error(\n `Mention link ${link.id} evidence sentence ${formatSentenceId(sentenceId)} is outside chapter ${chapterId}.`,\n );\n }\n }\n\n linkIds.add(link.id);\n }\n}\n\nfunction formatSentenceId(sentenceId: SentenceId): string {\n return sentenceId.join(\":\");\n}\n\nasync function writeJsonl<T>(\n path: string,\n records: AsyncIterable<T> | Iterable<T>,\n parseRecord: (record: unknown) => T,\n): Promise<void> {\n const stream = createWriteStream(path, { encoding: \"utf8\", flags: \"wx\" });\n\n try {\n for await (const record of records) {\n stream.write(`${JSON.stringify(parseRecord(record))}\\n`);\n }\n } finally {\n await closeWritableStream(stream);\n }\n}\n\nasync function readJsonl<T>(\n path: string,\n parseRecord: (record: unknown) => T,\n): Promise<T[]> {\n const records: T[] = [];\n const lines = createInterface({\n crlfDelay: Infinity,\n input: createReadStream(path, { encoding: \"utf8\" }),\n });\n let lineNumber = 0;\n\n for await (const line of lines) {\n lineNumber += 1;\n if (line.trim() === \"\") {\n continue;\n }\n\n try {\n records.push(parseRecord(JSON.parse(line)));\n } catch (error) {\n throw new Error(`Invalid JSONL record at ${path}:${lineNumber}`, {\n cause: error,\n });\n }\n }\n\n return records;\n}\n\nfunction parseMentionRecord(record: unknown): MentionRecord {\n const parsed = mentionRecordSchema.parse(record);\n\n return {\n chapterId: parsed.chapterId,\n ...(parsed.confidence === undefined\n ? {}\n : { confidence: parsed.confidence }),\n id: parsed.id,\n ...(parsed.note === undefined ? {} : { note: parsed.note }),\n qid: parsed.qid,\n rangeEnd: parsed.rangeEnd,\n rangeStart: parsed.rangeStart,\n ...(parsed.sentenceIndex === undefined\n ? {}\n : { sentenceIndex: parsed.sentenceIndex }),\n surface: parsed.surface,\n };\n}\n\nfunction parseMentionLinkRecord(record: unknown): MentionLinkRecord {\n const parsed = mentionLinkRecordSchema.parse(record);\n\n return {\n ...(parsed.confidence === undefined\n ? {}\n : { confidence: parsed.confidence }),\n evidenceSentenceIds: parsed.evidenceSentenceIds,\n id: parsed.id,\n ...(parsed.note === undefined ? {} : { note: parsed.note }),\n predicate: parsed.predicate,\n sourceMentionId: parsed.sourceMentionId,\n targetMentionId: parsed.targetMentionId,\n };\n}\n\nasync function closeWritableStream(\n stream: NodeJS.WritableStream,\n): Promise<void> {\n await new Promise<void>((resolveClose, rejectClose) => {\n stream.end((error?: Error | null) => {\n if (error !== undefined && error !== null) {\n rejectClose(error);\n return;\n }\n\n resolveClose();\n });\n });\n}\n","export {\n WikiGraph,\n type DigestProgressEvent,\n type SerialDiscoveryItem,\n type WikiGraphOptions,\n type WikiGraphLLMOptions,\n type WikiGraphOpenSessionOptions,\n type WikiGraphProgressCallback,\n type WikiGraphProgressEvent,\n type WikiGraphProgressEventType,\n type WikiGraphOperation,\n type SerialsDiscoveredEvent,\n type SerialProgressEvent,\n type WikiGraphSourceSessionOptions,\n type WikiGraphTextStreamSessionOptions,\n} from \"./app.js\";\nexport {\n findArchiveObjects,\n formatChapterId,\n formatEdgeId,\n formatNodeId,\n formatSummaryId,\n getArchiveIndex,\n grepArchiveObjects,\n listArchiveCollection,\n listArchiveEvidence,\n listAllArchiveLinks,\n listArchiveLinks,\n listArchiveObjects,\n listRelatedArchiveObjects,\n packArchiveContext,\n readArchivePage,\n readArchiveText,\n rebuildArchiveSearchIndex,\n} from \"../archive/query/index.js\";\nexport {\n addBuildJob,\n assertBuildJobInputRevision,\n assertNoActiveBuildJobConflicts,\n assertNoActiveBuildJobs,\n boostBuildJob,\n cancelBuildJob,\n cleanBuildJobs,\n getBuildJob,\n listBuildJobs,\n pauseBuildJob,\n readBuildJobEvents,\n recordBuildJobInputRevision,\n resumeBuildJob,\n resolveBuildJobId,\n runBuildQueueGc,\n runBuildJobWorker,\n updateBuildJobTarget,\n} from \"./build-queue.js\";\nexport type {\n AddBuildJobOptions,\n BuildJobConflictScope,\n BuildJobExecutionContext,\n BuildJob,\n BuildJobEvent,\n BuildJobListOptions,\n BuildJobProgressCounter,\n BuildJobProgressReporter,\n BuildJobState,\n BuildJobTarget,\n BuildJobWorkerOptions,\n} from \"./build-queue.js\";\nexport {\n buildChapterGraphArtifact,\n buildChapterSummaryArtifact,\n buildChapterSummaryArtifactFromSnapshot,\n commitChapterGraphArtifact,\n commitChapterSummaryArtifact,\n readChapterBuildInput,\n snapshotChapterSummaryInput,\n} from \"./chapter-build.js\";\nexport {\n buildChapterKnowledgeGraphArtifact,\n clearChapterKnowledgeGraph,\n commitChapterKnowledgeGraphArtifact,\n createEnrichmentProgressReporter,\n generateChapterKnowledgeGraphArtifact,\n generateChapterKnowledgeGraphArtifactFromSnapshot,\n groundWikimatchCandidates,\n snapshotChapterKnowledgeGraphInput,\n} from \"./knowledge-graph-build.js\";\nexport {\n createContinuationCursor,\n readContinuationCursor,\n} from \"../archive/query/index.js\";\nexport type { ContinuationCursor } from \"../archive/query/index.js\";\nexport type {\n BuildChapterGraphArtifactOptions,\n BuildChapterSummaryArtifactOptions,\n ChapterGraphBuildArtifact,\n} from \"./chapter-build.js\";\nexport type {\n BuildChapterKnowledgeGraphArtifactOptions,\n ChapterKnowledgeGraphBuildArtifact,\n ChapterKnowledgeGraphInputSnapshot,\n} from \"./knowledge-graph-build.js\";\nexport {\n addChapter,\n advanceChapterStages,\n applyChapterTree,\n CHAPTER_STAGES,\n generateChapterGraph,\n generateChapterSummary,\n getChapterDetails,\n getChapterTree,\n listChapters,\n moveChapter,\n parseChapterTreeInput,\n removeChapter,\n resetChapter,\n setChapterSource,\n setChapterSummary,\n setChapterTitle,\n} from \"./chapter.js\";\nexport {\n findGraphPath,\n getGraphEvidence,\n getGraphNode,\n getGraphStatus,\n listGraphNeighbors,\n listGraphNodes,\n searchGraphNodes,\n} from \"./graph.js\";\nexport type {\n ArchiveBacklinkBucket,\n ArchiveBacklinks,\n ArchiveEvidence,\n ArchiveEvidenceItem,\n ArchiveFindEvidencePreview,\n ArchiveCollectionOptions,\n ArchiveCollectionResult,\n ArchiveCollectionType,\n ArchiveFindField,\n ArchiveFindFilterType,\n ArchiveFindHit,\n ArchiveIndex,\n ArchiveListItem,\n ArchiveListKind,\n ArchiveNodeLabel,\n ArchiveNodeSourceFragment,\n ArchiveFindObjectType,\n ArchiveFindOptions,\n ArchiveFindOrder,\n ArchiveFindPosition,\n ArchiveFindResult,\n ArchiveObjectType,\n ArchivePack,\n ArchivePage,\n ArchiveRelatedResult,\n ArchiveTriplePattern,\n} from \"../archive/query/index.js\";\nexport type {\n AddChapterOptions,\n AdvanceChapterStagesOptions,\n AdvanceChapterStagesProgressCallback,\n AdvanceChapterStagesProgressEvent,\n AdvanceChapterStagesResult,\n ChapterDetails,\n ChapterEntry,\n ChapterStage,\n ChapterTree,\n ChapterTreeApplyResult,\n ChapterTreeInput,\n ChapterTreeInputNode,\n ChapterTreeMoveChange,\n ChapterTreeNode,\n ChapterTreeTitleChange,\n GenerateChapterGraphOptions,\n GenerateChapterSummaryOptions,\n MoveChapterOptions,\n} from \"./chapter.js\";\nexport type {\n GraphEdge,\n GraphEvidenceLine,\n GraphNeighbor,\n GraphNode,\n GraphPathStep,\n GraphSearchField,\n GraphSearchHit,\n GraphStatus,\n} from \"./graph.js\";\nexport { WikiGraphArchive } from \"./wiki-graph-archive.js\";\nexport type { WikiGraphSerialEntry } from \"./types.js\";\n","import { randomUUID } from \"crypto\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphStateRootPath } from \"../common/wiki-graph-temp.js\";\nimport { getNumber, getString, type SqlRow } from \"../document/database.js\";\nimport { openSharedStateDatabase } from \"../document/index.js\";\nimport type { Database } from \"../document/index.js\";\nimport { isNodeError } from \"../utils/node-error.js\";\n\nconst GC_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS gc_locks (\n scope TEXT PRIMARY KEY,\n owner_id TEXT NOT NULL,\n owner_pid INTEGER NOT NULL,\n heartbeat_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL\n);\n`;\n\nconst GC_LOCK_STALE_TIMEOUT_MS = 60_000;\nconst GC_HEARTBEAT_INTERVAL_MS = 20_000;\n\ninterface GcLock {\n readonly createdAt: number;\n readonly heartbeatAt: number;\n readonly ownerId: string;\n readonly ownerPid: number;\n}\n\nexport async function tryAcquireGcLock(\n scope = \"global\",\n): Promise<(() => Promise<void>) | undefined> {\n const database = await openGcStateDatabase();\n const ownerId = `${process.pid}-${randomUUID()}`;\n const now = Date.now();\n let acquired = false;\n\n try {\n await database.transaction(async () => {\n await cleanupStaleGcLocks(database);\n await database.run(\n `\nINSERT OR IGNORE INTO gc_locks (\n scope, owner_id, owner_pid, heartbeat_at, created_at\n) VALUES (?, ?, ?, ?, ?)\n`,\n [scope, ownerId, process.pid, now, now],\n );\n acquired =\n (await database.queryOne(\n \"SELECT owner_id FROM gc_locks WHERE scope = ?\",\n [scope],\n (row) => getString(row, \"owner_id\"),\n )) === ownerId;\n });\n } finally {\n await database.close();\n }\n\n if (!acquired) {\n return undefined;\n }\n\n const heartbeat = setInterval(() => {\n void heartbeatGcLock(scope, ownerId).catch(() => undefined);\n }, GC_HEARTBEAT_INTERVAL_MS);\n\n return async () => {\n clearInterval(heartbeat);\n const releaseDatabase = await openGcStateDatabase();\n\n try {\n await releaseDatabase.run(\n \"DELETE FROM gc_locks WHERE scope = ? AND owner_id = ?\",\n [scope, ownerId],\n );\n } finally {\n await releaseDatabase.close();\n }\n };\n}\n\nasync function heartbeatGcLock(scope: string, ownerId: string): Promise<void> {\n const database = await openGcStateDatabase();\n\n try {\n await database.run(\n `\nUPDATE gc_locks\nSET heartbeat_at = ?, owner_pid = ?\nWHERE scope = ? AND owner_id = ?\n`,\n [Date.now(), process.pid, scope, ownerId],\n );\n } finally {\n await database.close();\n }\n}\n\nasync function cleanupStaleGcLocks(database: Database): Promise<void> {\n const locks = await database.queryAll(\n \"SELECT * FROM gc_locks\",\n undefined,\n mapGcLockWithScope,\n );\n\n for (const { lock, scope } of locks.filter(({ lock }) =>\n isStaleGcLock(lock),\n )) {\n await database.run(\n `\nDELETE FROM gc_locks\nWHERE scope = ?\n AND owner_id = ?\n AND heartbeat_at = ?\n`,\n [scope, lock.ownerId, lock.heartbeatAt],\n );\n }\n}\n\nfunction isStaleGcLock(lock: GcLock): boolean {\n if (Date.now() - lock.heartbeatAt < GC_LOCK_STALE_TIMEOUT_MS) {\n return false;\n }\n\n return !isProcessAlive(lock.ownerPid);\n}\n\nasync function openGcStateDatabase(): Promise<Database> {\n return await openSharedStateDatabase(\n join(resolveWikiGraphStateRootPath(), \"gc.sqlite\"),\n GC_SCHEMA_SQL,\n );\n}\n\nfunction mapGcLock(row: SqlRow): GcLock {\n return {\n createdAt: getNumber(row, \"created_at\"),\n heartbeatAt: getNumber(row, \"heartbeat_at\"),\n ownerId: getString(row, \"owner_id\"),\n ownerPid: getNumber(row, \"owner_pid\"),\n };\n}\n\nfunction mapGcLockWithScope(row: SqlRow): {\n readonly lock: GcLock;\n readonly scope: string;\n} {\n return {\n lock: mapGcLock(row),\n scope: getString(row, \"scope\"),\n };\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ESRCH\") {\n return false;\n }\n\n return true;\n }\n}\n","import { readdir, rm, stat } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphTempDirectoryPath } from \"../common/wiki-graph-temp.js\";\nimport { isNodeError } from \"../utils/node-error.js\";\n\nimport { readPathSize } from \"./files.js\";\nimport type { GcContext, GcJobResult } from \"./types.js\";\n\nconst TEMP_DIRECTORY_TTL_MS = 60 * 60 * 1000;\n\nexport async function runTempDirectoryGc(\n context: GcContext,\n): Promise<GcJobResult> {\n const rootPath = resolveWikiGraphTempDirectoryPath();\n let scanned = 0;\n let removed = 0;\n let freedBytes = 0;\n\n for (const path of await listTempEntryPaths(rootPath)) {\n scanned += 1;\n const stats = await stat(path).catch((error: unknown) => {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return undefined;\n }\n\n throw error;\n });\n\n if (\n stats === undefined ||\n (!context.force && context.now - stats.mtimeMs < TEMP_DIRECTORY_TTL_MS)\n ) {\n continue;\n }\n\n const bytes = await readPathSize(path);\n\n if (!context.dryRun) {\n await rm(path, { force: true, recursive: true });\n }\n removed += 1;\n freedBytes += bytes;\n }\n\n return { freedBytes, removed, scanned };\n}\n\nasync function listTempEntryPaths(rootPath: string): Promise<string[]> {\n try {\n const categories = await readdir(rootPath, { withFileTypes: true });\n const paths: string[] = [];\n\n for (const category of categories) {\n if (!category.isDirectory()) {\n continue;\n }\n\n const categoryPath = join(rootPath, category.name);\n const entries = await readdir(categoryPath, { withFileTypes: true });\n\n for (const entry of entries) {\n paths.push(join(categoryPath, entry.name));\n }\n }\n\n return paths;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return [];\n }\n\n throw error;\n }\n}\n","import { mkdir, readFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { resolveWikiGraphStateRootPath } from \"../common/wiki-graph-temp.js\";\nimport { runSearchCacheGc } from \"../archive/query/index.js\";\nimport { runBuildQueueGc } from \"../facade/index.js\";\nimport { runWikgCoordinatorGc } from \"../wikg/index.js\";\nimport { runWikipageCacheGc } from \"../wikipage/index.js\";\nimport { formatError } from \"../utils/node-error.js\";\n\nimport { tryAcquireGcLock } from \"./lock.js\";\nimport { runTempDirectoryGc } from \"./tmp-gc.js\";\nimport type { GcContext, GcJob, GcJobReport, GcRunReport } from \"./types.js\";\n\ninterface NamedGcJob {\n readonly name: string;\n readonly run: GcJob;\n}\n\nconst GC_JOBS: readonly NamedGcJob[] = [\n {\n name: \"wikg-coordinator\",\n run: runWikgCoordinatorGc,\n },\n {\n name: \"search-cache\",\n run: runSearchCacheGc,\n },\n {\n name: \"wikipage-cache\",\n run: runWikipageCacheGc,\n },\n {\n name: \"build-queue\",\n run: runBuildQueueGc,\n },\n {\n name: \"tmp\",\n run: runTempDirectoryGc,\n },\n];\nconst OPPORTUNISTIC_GC_INTERVAL_MS = 10 * 60 * 1000;\n\nexport async function tryRunWikiGraphGc(\n options: {\n readonly dryRun?: boolean;\n readonly force?: boolean;\n readonly opportunistic?: boolean;\n } = {},\n): Promise<GcRunReport> {\n const startedAt = Date.now();\n const stateDirectoryPath = resolveWikiGraphStateRootPath();\n const release = await tryAcquireGcLock();\n\n if (release === undefined) {\n return {\n finishedAt: Date.now(),\n freedBytes: 0,\n jobs: [],\n removed: 0,\n scanned: 0,\n skipped: true,\n startedAt,\n };\n }\n\n try {\n if (\n options.opportunistic === true &&\n !(await shouldRunOpportunisticGc(stateDirectoryPath, startedAt))\n ) {\n return {\n finishedAt: Date.now(),\n freedBytes: 0,\n jobs: [],\n removed: 0,\n scanned: 0,\n skipped: true,\n startedAt,\n };\n }\n\n const context: GcContext = {\n dryRun: options.dryRun === true,\n force: options.force === true,\n now: startedAt,\n stateDirectoryPath,\n };\n const jobs: GcJobReport[] = [];\n\n for (const job of GC_JOBS) {\n jobs.push(await runJob(job, context));\n }\n\n const report = {\n finishedAt: Date.now(),\n freedBytes: sum(jobs, \"freedBytes\"),\n jobs,\n removed: sum(jobs, \"removed\"),\n scanned: sum(jobs, \"scanned\"),\n skipped: false,\n startedAt,\n };\n\n await writeLastGcRunAt(stateDirectoryPath, report.finishedAt).catch(\n () => undefined,\n );\n return report;\n } finally {\n await release();\n }\n}\n\nasync function runJob(\n job: NamedGcJob,\n context: GcContext,\n): Promise<GcJobReport> {\n try {\n return {\n name: job.name,\n ...(await job.run(context)),\n };\n } catch (error) {\n return {\n error: formatError(error),\n freedBytes: 0,\n name: job.name,\n removed: 0,\n scanned: 0,\n };\n }\n}\n\nfunction sum(jobs: readonly GcJobReport[], key: keyof GcJobReport): number {\n return jobs.reduce((total, job) => {\n const value = job[key];\n\n return total + (typeof value === \"number\" ? value : 0);\n }, 0);\n}\n\nasync function shouldRunOpportunisticGc(\n stateDirectoryPath: string,\n now: number,\n): Promise<boolean> {\n const lastRunAt = Number(\n (\n await readFile(createLastGcRunPath(stateDirectoryPath), \"utf8\").catch(\n () => \"\",\n )\n ).trim(),\n );\n\n return (\n !Number.isFinite(lastRunAt) ||\n now - lastRunAt >= OPPORTUNISTIC_GC_INTERVAL_MS\n );\n}\n\nasync function writeLastGcRunAt(\n stateDirectoryPath: string,\n at: number,\n): Promise<void> {\n await mkdir(stateDirectoryPath, { recursive: true });\n await writeFile(createLastGcRunPath(stateDirectoryPath), `${at}\\n`, \"utf8\");\n}\n\nfunction createLastGcRunPath(stateDirectoryPath: string): string {\n return join(stateDirectoryPath, \"gc.last-run\");\n}\n","export { tryRunWikiGraphGc } from \"./runner.js\";\nexport type {\n GcContext,\n GcJob,\n GcJobReport,\n GcJobResult,\n GcRunReport,\n} from \"./types.js\";\n","export { tryRunWikiGraphGc } from \"./gc/index.js\";\nexport type {\n GcContext,\n GcJob,\n GcJobReport,\n GcJobResult,\n GcRunReport,\n} from \"./gc/index.js\";\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmSnB,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC/Rh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","names":["import_path","module","import_promises","import_path","sleep","import_promises","import_path","ChunkRetention","ChunkImportance","import_promises","import_path","import_promises","import_path","import_zod","createHash","import_crypto","createNodeHash","createHash","import_async_hooks","import_promises","import_path","import_zod","options","parse","init_document","listTableColumns","getString","import_promises","init_document","import_zod","import_async_hooks","import_crypto","import_path","pino","import_crypto","import_promises","resolve","SENTENCE_SEGMENTER","head","tail","import_zod","init_document","init_language","import_zod","init_language","init_reader","init_prompt_templates","init_prompt_templates","init_types","ReviewSeverity","init_prompt_templates","init_types","init_types","import_promises","init_document","init_response","import_zod","init_prompt_templates","init_types","init_response","init_editor","init_document","init_topology","init_reader","init_editor","init_topology","resolve","import_crypto","import_path","countWords","import_htmlparser2","normalizeText","import_path","normalizeText","import_path","import_htmlparser2","init_document","init_document","import_fs","import_promises","import_path","import_zod","readFileSize","import_crypto","import_promises","import_path","init_document","import_crypto","HAN_RE","import_crypto","import_path","import_os","fragments","sentences","import_crypto","import_path","init_document","import_meta","import_fs","import_path","import_fs","import_path","import_promises","import_path","init_cache","init_errors","import_promises","import_fs","import_path","import_promises","init_cache","init_errors","init_errors","import_promises","import_path","init_plain_text","import_fs","import_path","import_promises","init_archive","init_content","init_navigation","init_package","init_archive","init_content","init_navigation","init_package","init_epub","init_archive","init_package","init_plain_text","init_epub","openFile","normalizeArchivePath","resolve","openZip","import_crypto","import_fs","import_promises","import_path","import_zod","import_yauzl","import_yazl","init_archive","init_search_index","import_promises","import_path","pathExists","getNumber","isProcessAlive","getString","getOptionalString","import_crypto","import_promises","import_os","import_path","init_document","init_search_index","init_archive","import_path","init_document","init_archive","import_promises","import_path","init_document","getNumber","getOptionalString","getString","import_crypto","import_promises","import_path","init_document","import_promises","import_path","import_zod","init_document","init_editor","init_reader","import_zod","import_zod","import_zod","import_zod","evidenceSelectionItemSchema","init_window","init_window","import_fs","import_promises","import_path","import_zod","sentenceIdSchema","lock","isProcessAlive","import_crypto","import_path","init_document","import_promises","import_path","import_promises","import_path"]}
|