@tenphi/akno-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +83 -0
- package/README.md +26 -0
- package/config/default.jsonc +548 -0
- package/dist/bench/answer-corpus.d.ts +48 -0
- package/dist/bench/answer-corpus.d.ts.map +1 -0
- package/dist/bench/answer-corpus.js +491 -0
- package/dist/bench/answer-corpus.js.map +1 -0
- package/dist/bench/answer.d.ts +156 -0
- package/dist/bench/answer.d.ts.map +1 -0
- package/dist/bench/answer.js +608 -0
- package/dist/bench/answer.js.map +1 -0
- package/dist/bench/auto-recall-answer-corpus.d.ts +21 -0
- package/dist/bench/auto-recall-answer-corpus.d.ts.map +1 -0
- package/dist/bench/auto-recall-answer-corpus.js +245 -0
- package/dist/bench/auto-recall-answer-corpus.js.map +1 -0
- package/dist/bench/auto-recall-answer.d.ts +198 -0
- package/dist/bench/auto-recall-answer.d.ts.map +1 -0
- package/dist/bench/auto-recall-answer.js +720 -0
- package/dist/bench/auto-recall-answer.js.map +1 -0
- package/dist/bench/auto-recall-corpus.d.ts +45 -0
- package/dist/bench/auto-recall-corpus.d.ts.map +1 -0
- package/dist/bench/auto-recall-corpus.js +221 -0
- package/dist/bench/auto-recall-corpus.js.map +1 -0
- package/dist/bench/auto-recall.d.ts +171 -0
- package/dist/bench/auto-recall.d.ts.map +1 -0
- package/dist/bench/auto-recall.js +651 -0
- package/dist/bench/auto-recall.js.map +1 -0
- package/dist/bench/entity-resolution.d.ts +37 -0
- package/dist/bench/entity-resolution.d.ts.map +1 -0
- package/dist/bench/entity-resolution.js +121 -0
- package/dist/bench/entity-resolution.js.map +1 -0
- package/dist/bench/graph.d.ts +57 -0
- package/dist/bench/graph.d.ts.map +1 -0
- package/dist/bench/graph.js +573 -0
- package/dist/bench/graph.js.map +1 -0
- package/dist/bench/llm-ranking-probe.d.ts +27 -0
- package/dist/bench/llm-ranking-probe.d.ts.map +1 -0
- package/dist/bench/llm-ranking-probe.js +105 -0
- package/dist/bench/llm-ranking-probe.js.map +1 -0
- package/dist/bench/merge-discovery-corpus.d.ts +26 -0
- package/dist/bench/merge-discovery-corpus.d.ts.map +1 -0
- package/dist/bench/merge-discovery-corpus.js +114 -0
- package/dist/bench/merge-discovery-corpus.js.map +1 -0
- package/dist/bench/merge-discovery-review.d.ts +81 -0
- package/dist/bench/merge-discovery-review.d.ts.map +1 -0
- package/dist/bench/merge-discovery-review.js +149 -0
- package/dist/bench/merge-discovery-review.js.map +1 -0
- package/dist/bench/merge-discovery.d.ts +122 -0
- package/dist/bench/merge-discovery.d.ts.map +1 -0
- package/dist/bench/merge-discovery.js +390 -0
- package/dist/bench/merge-discovery.js.map +1 -0
- package/dist/bench/mixed-retrieval.d.ts +35 -0
- package/dist/bench/mixed-retrieval.d.ts.map +1 -0
- package/dist/bench/mixed-retrieval.js +355 -0
- package/dist/bench/mixed-retrieval.js.map +1 -0
- package/dist/bench/ranking-corpus.d.ts +29 -0
- package/dist/bench/ranking-corpus.d.ts.map +1 -0
- package/dist/bench/ranking-corpus.js +514 -0
- package/dist/bench/ranking-corpus.js.map +1 -0
- package/dist/bench/ranking-end-to-end.d.ts +112 -0
- package/dist/bench/ranking-end-to-end.d.ts.map +1 -0
- package/dist/bench/ranking-end-to-end.js +479 -0
- package/dist/bench/ranking-end-to-end.js.map +1 -0
- package/dist/bench/ranking-latency.d.ts +71 -0
- package/dist/bench/ranking-latency.d.ts.map +1 -0
- package/dist/bench/ranking-latency.js +130 -0
- package/dist/bench/ranking-latency.js.map +1 -0
- package/dist/bench/ranking-matrix.d.ts +141 -0
- package/dist/bench/ranking-matrix.d.ts.map +1 -0
- package/dist/bench/ranking-matrix.js +526 -0
- package/dist/bench/ranking-matrix.js.map +1 -0
- package/dist/bench/ranking-review.d.ts +93 -0
- package/dist/bench/ranking-review.d.ts.map +1 -0
- package/dist/bench/ranking-review.js +249 -0
- package/dist/bench/ranking-review.js.map +1 -0
- package/dist/bench/ranking.d.ts +122 -0
- package/dist/bench/ranking.d.ts.map +1 -0
- package/dist/bench/ranking.js +547 -0
- package/dist/bench/ranking.js.map +1 -0
- package/dist/bench.d.ts +60 -0
- package/dist/bench.d.ts.map +1 -0
- package/dist/bench.js +167 -0
- package/dist/bench.js.map +1 -0
- package/dist/config/jsonc.d.ts +8 -0
- package/dist/config/jsonc.d.ts.map +1 -0
- package/dist/config/jsonc.js +85 -0
- package/dist/config/jsonc.js.map +1 -0
- package/dist/config/load.d.ts +43 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +608 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/paths.d.ts +10 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +38 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +698 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +394 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/write-rules.d.ts +41 -0
- package/dist/config/write-rules.d.ts.map +1 -0
- package/dist/config/write-rules.js +225 -0
- package/dist/config/write-rules.js.map +1 -0
- package/dist/config/write-setup.d.ts +42 -0
- package/dist/config/write-setup.d.ts.map +1 -0
- package/dist/config/write-setup.js +358 -0
- package/dist/config/write-setup.js.map +1 -0
- package/dist/context.d.ts +64 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +14 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +115 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +388 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index/chunk.d.ts +62 -0
- package/dist/index/chunk.d.ts.map +1 -0
- package/dist/index/chunk.js +307 -0
- package/dist/index/chunk.js.map +1 -0
- package/dist/index/defer.d.ts +32 -0
- package/dist/index/defer.d.ts.map +1 -0
- package/dist/index/defer.js +66 -0
- package/dist/index/defer.js.map +1 -0
- package/dist/index/derive.d.ts +99 -0
- package/dist/index/derive.d.ts.map +1 -0
- package/dist/index/derive.js +378 -0
- package/dist/index/derive.js.map +1 -0
- package/dist/index/entity-resolution.d.ts +68 -0
- package/dist/index/entity-resolution.d.ts.map +1 -0
- package/dist/index/entity-resolution.js +305 -0
- package/dist/index/entity-resolution.js.map +1 -0
- package/dist/index/graph.d.ts +46 -0
- package/dist/index/graph.d.ts.map +1 -0
- package/dist/index/graph.js +618 -0
- package/dist/index/graph.js.map +1 -0
- package/dist/index/indexer.d.ts +301 -0
- package/dist/index/indexer.d.ts.map +1 -0
- package/dist/index/indexer.js +1398 -0
- package/dist/index/indexer.js.map +1 -0
- package/dist/index/revision-barrier.d.ts +30 -0
- package/dist/index/revision-barrier.d.ts.map +1 -0
- package/dist/index/revision-barrier.js +140 -0
- package/dist/index/revision-barrier.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/adoption-eligibility.d.ts +4 -0
- package/dist/ingest/adoption-eligibility.d.ts.map +1 -0
- package/dist/ingest/adoption-eligibility.js +18 -0
- package/dist/ingest/adoption-eligibility.js.map +1 -0
- package/dist/ingest/availability.d.ts +15 -0
- package/dist/ingest/availability.d.ts.map +1 -0
- package/dist/ingest/availability.js +37 -0
- package/dist/ingest/availability.js.map +1 -0
- package/dist/ingest/extract.d.ts +58 -0
- package/dist/ingest/extract.d.ts.map +1 -0
- package/dist/ingest/extract.js +255 -0
- package/dist/ingest/extract.js.map +1 -0
- package/dist/ingest/fetch.d.ts +35 -0
- package/dist/ingest/fetch.d.ts.map +1 -0
- package/dist/ingest/fetch.js +142 -0
- package/dist/ingest/fetch.js.map +1 -0
- package/dist/ingest/inbox.d.ts +53 -0
- package/dist/ingest/inbox.d.ts.map +1 -0
- package/dist/ingest/inbox.js +110 -0
- package/dist/ingest/inbox.js.map +1 -0
- package/dist/ingest/name.d.ts +72 -0
- package/dist/ingest/name.d.ts.map +1 -0
- package/dist/ingest/name.js +208 -0
- package/dist/ingest/name.js.map +1 -0
- package/dist/ingest/parts.d.ts +78 -0
- package/dist/ingest/parts.d.ts.map +1 -0
- package/dist/ingest/parts.js +52 -0
- package/dist/ingest/parts.js.map +1 -0
- package/dist/ingest/rendition.d.ts +75 -0
- package/dist/ingest/rendition.d.ts.map +1 -0
- package/dist/ingest/rendition.js +92 -0
- package/dist/ingest/rendition.js.map +1 -0
- package/dist/ingest/store.d.ts +66 -0
- package/dist/ingest/store.d.ts.map +1 -0
- package/dist/ingest/store.js +92 -0
- package/dist/ingest/store.js.map +1 -0
- package/dist/kb/folders.d.ts +37 -0
- package/dist/kb/folders.d.ts.map +1 -0
- package/dist/kb/folders.js +133 -0
- package/dist/kb/folders.js.map +1 -0
- package/dist/kb/frontmatter.d.ts +91 -0
- package/dist/kb/frontmatter.d.ts.map +1 -0
- package/dist/kb/frontmatter.js +274 -0
- package/dist/kb/frontmatter.js.map +1 -0
- package/dist/kb/line-facts.d.ts +29 -0
- package/dist/kb/line-facts.d.ts.map +1 -0
- package/dist/kb/line-facts.js +23 -0
- package/dist/kb/line-facts.js.map +1 -0
- package/dist/kb/page.d.ts +92 -0
- package/dist/kb/page.d.ts.map +1 -0
- package/dist/kb/page.js +243 -0
- package/dist/kb/page.js.map +1 -0
- package/dist/kb/scan.d.ts +38 -0
- package/dist/kb/scan.d.ts.map +1 -0
- package/dist/kb/scan.js +109 -0
- package/dist/kb/scan.js.map +1 -0
- package/dist/kb/words.d.ts +30 -0
- package/dist/kb/words.d.ts.map +1 -0
- package/dist/kb/words.js +107 -0
- package/dist/kb/words.js.map +1 -0
- package/dist/maintenance/adopt.d.ts +65 -0
- package/dist/maintenance/adopt.d.ts.map +1 -0
- package/dist/maintenance/adopt.js +238 -0
- package/dist/maintenance/adopt.js.map +1 -0
- package/dist/maintenance/budget.d.ts +55 -0
- package/dist/maintenance/budget.d.ts.map +1 -0
- package/dist/maintenance/budget.js +61 -0
- package/dist/maintenance/budget.js.map +1 -0
- package/dist/maintenance/conflicts.d.ts +105 -0
- package/dist/maintenance/conflicts.d.ts.map +1 -0
- package/dist/maintenance/conflicts.js +368 -0
- package/dist/maintenance/conflicts.js.map +1 -0
- package/dist/maintenance/contradictions.d.ts +34 -0
- package/dist/maintenance/contradictions.d.ts.map +1 -0
- package/dist/maintenance/contradictions.js +199 -0
- package/dist/maintenance/contradictions.js.map +1 -0
- package/dist/maintenance/curate.d.ts +156 -0
- package/dist/maintenance/curate.d.ts.map +1 -0
- package/dist/maintenance/curate.js +2070 -0
- package/dist/maintenance/curate.js.map +1 -0
- package/dist/maintenance/dream.d.ts +139 -0
- package/dist/maintenance/dream.d.ts.map +1 -0
- package/dist/maintenance/dream.js +1632 -0
- package/dist/maintenance/dream.js.map +1 -0
- package/dist/maintenance/graph-candidates.d.ts +19 -0
- package/dist/maintenance/graph-candidates.d.ts.map +1 -0
- package/dist/maintenance/graph-candidates.js +120 -0
- package/dist/maintenance/graph-candidates.js.map +1 -0
- package/dist/maintenance/housekeeping.d.ts +81 -0
- package/dist/maintenance/housekeeping.d.ts.map +1 -0
- package/dist/maintenance/housekeeping.js +281 -0
- package/dist/maintenance/housekeeping.js.map +1 -0
- package/dist/maintenance/link-repairs.d.ts +58 -0
- package/dist/maintenance/link-repairs.d.ts.map +1 -0
- package/dist/maintenance/link-repairs.js +281 -0
- package/dist/maintenance/link-repairs.js.map +1 -0
- package/dist/maintenance/log.d.ts +41 -0
- package/dist/maintenance/log.d.ts.map +1 -0
- package/dist/maintenance/log.js +72 -0
- package/dist/maintenance/log.js.map +1 -0
- package/dist/maintenance/managed-item-routing.d.ts +48 -0
- package/dist/maintenance/managed-item-routing.d.ts.map +1 -0
- package/dist/maintenance/managed-item-routing.js +281 -0
- package/dist/maintenance/managed-item-routing.js.map +1 -0
- package/dist/maintenance/managed-item-sources.d.ts +38 -0
- package/dist/maintenance/managed-item-sources.d.ts.map +1 -0
- package/dist/maintenance/managed-item-sources.js +324 -0
- package/dist/maintenance/managed-item-sources.js.map +1 -0
- package/dist/maintenance/managed-items.d.ts +162 -0
- package/dist/maintenance/managed-items.d.ts.map +1 -0
- package/dist/maintenance/managed-items.js +1329 -0
- package/dist/maintenance/managed-items.js.map +1 -0
- package/dist/maintenance/merge-classifier.d.ts +19 -0
- package/dist/maintenance/merge-classifier.d.ts.map +1 -0
- package/dist/maintenance/merge-classifier.js +49 -0
- package/dist/maintenance/merge-classifier.js.map +1 -0
- package/dist/maintenance/model-telemetry.d.ts +45 -0
- package/dist/maintenance/model-telemetry.d.ts.map +1 -0
- package/dist/maintenance/model-telemetry.js +107 -0
- package/dist/maintenance/model-telemetry.js.map +1 -0
- package/dist/maintenance/observe.d.ts +72 -0
- package/dist/maintenance/observe.d.ts.map +1 -0
- package/dist/maintenance/observe.js +265 -0
- package/dist/maintenance/observe.js.map +1 -0
- package/dist/maintenance/path-policy.d.ts +83 -0
- package/dist/maintenance/path-policy.d.ts.map +1 -0
- package/dist/maintenance/path-policy.js +360 -0
- package/dist/maintenance/path-policy.js.map +1 -0
- package/dist/maintenance/plans.d.ts +366 -0
- package/dist/maintenance/plans.d.ts.map +1 -0
- package/dist/maintenance/plans.js +4060 -0
- package/dist/maintenance/plans.js.map +1 -0
- package/dist/maintenance/profile.d.ts +33 -0
- package/dist/maintenance/profile.d.ts.map +1 -0
- package/dist/maintenance/profile.js +99 -0
- package/dist/maintenance/profile.js.map +1 -0
- package/dist/maintenance/recovery.d.ts +38 -0
- package/dist/maintenance/recovery.d.ts.map +1 -0
- package/dist/maintenance/recovery.js +217 -0
- package/dist/maintenance/recovery.js.map +1 -0
- package/dist/maintenance/repair.d.ts +37 -0
- package/dist/maintenance/repair.d.ts.map +1 -0
- package/dist/maintenance/repair.js +183 -0
- package/dist/maintenance/repair.js.map +1 -0
- package/dist/maintenance/rule-drift.d.ts +104 -0
- package/dist/maintenance/rule-drift.d.ts.map +1 -0
- package/dist/maintenance/rule-drift.js +507 -0
- package/dist/maintenance/rule-drift.js.map +1 -0
- package/dist/maintenance/run-verification.d.ts +44 -0
- package/dist/maintenance/run-verification.d.ts.map +1 -0
- package/dist/maintenance/run-verification.js +253 -0
- package/dist/maintenance/run-verification.js.map +1 -0
- package/dist/maintenance/runs.d.ts +142 -0
- package/dist/maintenance/runs.d.ts.map +1 -0
- package/dist/maintenance/runs.js +439 -0
- package/dist/maintenance/runs.js.map +1 -0
- package/dist/maintenance/semantic-merge-discovery.d.ts +54 -0
- package/dist/maintenance/semantic-merge-discovery.d.ts.map +1 -0
- package/dist/maintenance/semantic-merge-discovery.js +308 -0
- package/dist/maintenance/semantic-merge-discovery.js.map +1 -0
- package/dist/maintenance/temporal.d.ts +44 -0
- package/dist/maintenance/temporal.d.ts.map +1 -0
- package/dist/maintenance/temporal.js +295 -0
- package/dist/maintenance/temporal.js.map +1 -0
- package/dist/models/client.d.ts +214 -0
- package/dist/models/client.d.ts.map +1 -0
- package/dist/models/client.js +904 -0
- package/dist/models/client.js.map +1 -0
- package/dist/models/provider-api.d.ts +28 -0
- package/dist/models/provider-api.d.ts.map +1 -0
- package/dist/models/provider-api.js +320 -0
- package/dist/models/provider-api.js.map +1 -0
- package/dist/open.d.ts +163 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +382 -0
- package/dist/open.js.map +1 -0
- package/dist/ops/adopt.d.ts +12 -0
- package/dist/ops/adopt.d.ts.map +1 -0
- package/dist/ops/adopt.js +138 -0
- package/dist/ops/adopt.js.map +1 -0
- package/dist/ops/answer.d.ts +29 -0
- package/dist/ops/answer.d.ts.map +1 -0
- package/dist/ops/answer.js +557 -0
- package/dist/ops/answer.js.map +1 -0
- package/dist/ops/context.d.ts +18 -0
- package/dist/ops/context.d.ts.map +1 -0
- package/dist/ops/context.js +713 -0
- package/dist/ops/context.js.map +1 -0
- package/dist/ops/folder.d.ts +23 -0
- package/dist/ops/folder.d.ts.map +1 -0
- package/dist/ops/folder.js +130 -0
- package/dist/ops/folder.js.map +1 -0
- package/dist/ops/forget.d.ts +14 -0
- package/dist/ops/forget.d.ts.map +1 -0
- package/dist/ops/forget.js +212 -0
- package/dist/ops/forget.js.map +1 -0
- package/dist/ops/graph.d.ts +10 -0
- package/dist/ops/graph.d.ts.map +1 -0
- package/dist/ops/graph.js +480 -0
- package/dist/ops/graph.js.map +1 -0
- package/dist/ops/ingest.d.ts +40 -0
- package/dist/ops/ingest.d.ts.map +1 -0
- package/dist/ops/ingest.js +434 -0
- package/dist/ops/ingest.js.map +1 -0
- package/dist/ops/list.d.ts +9 -0
- package/dist/ops/list.d.ts.map +1 -0
- package/dist/ops/list.js +217 -0
- package/dist/ops/list.js.map +1 -0
- package/dist/ops/move.d.ts +20 -0
- package/dist/ops/move.d.ts.map +1 -0
- package/dist/ops/move.js +173 -0
- package/dist/ops/move.js.map +1 -0
- package/dist/ops/read.d.ts +9 -0
- package/dist/ops/read.d.ts.map +1 -0
- package/dist/ops/read.js +271 -0
- package/dist/ops/read.js.map +1 -0
- package/dist/ops/recall.d.ts +13 -0
- package/dist/ops/recall.d.ts.map +1 -0
- package/dist/ops/recall.js +297 -0
- package/dist/ops/recall.js.map +1 -0
- package/dist/ops/remember.d.ts +34 -0
- package/dist/ops/remember.d.ts.map +1 -0
- package/dist/ops/remember.js +660 -0
- package/dist/ops/remember.js.map +1 -0
- package/dist/ops/timeline.d.ts +11 -0
- package/dist/ops/timeline.d.ts.map +1 -0
- package/dist/ops/timeline.js +111 -0
- package/dist/ops/timeline.js.map +1 -0
- package/dist/ops/undo.d.ts +9 -0
- package/dist/ops/undo.d.ts.map +1 -0
- package/dist/ops/undo.js +38 -0
- package/dist/ops/undo.js.map +1 -0
- package/dist/ops/write.d.ts +55 -0
- package/dist/ops/write.d.ts.map +1 -0
- package/dist/ops/write.js +463 -0
- package/dist/ops/write.js.map +1 -0
- package/dist/recall/assemble.d.ts +81 -0
- package/dist/recall/assemble.d.ts.map +1 -0
- package/dist/recall/assemble.js +603 -0
- package/dist/recall/assemble.js.map +1 -0
- package/dist/recall/expand.d.ts +55 -0
- package/dist/recall/expand.d.ts.map +1 -0
- package/dist/recall/expand.js +282 -0
- package/dist/recall/expand.js.map +1 -0
- package/dist/recall/graph-arm.d.ts +17 -0
- package/dist/recall/graph-arm.d.ts.map +1 -0
- package/dist/recall/graph-arm.js +227 -0
- package/dist/recall/graph-arm.js.map +1 -0
- package/dist/recall/llm-rerank.d.ts +55 -0
- package/dist/recall/llm-rerank.d.ts.map +1 -0
- package/dist/recall/llm-rerank.js +213 -0
- package/dist/recall/llm-rerank.js.map +1 -0
- package/dist/recall/reranker-calibration.d.ts +23 -0
- package/dist/recall/reranker-calibration.d.ts.map +1 -0
- package/dist/recall/reranker-calibration.js +161 -0
- package/dist/recall/reranker-calibration.js.map +1 -0
- package/dist/recall/search.d.ts +107 -0
- package/dist/recall/search.d.ts.map +1 -0
- package/dist/recall/search.js +530 -0
- package/dist/recall/search.js.map +1 -0
- package/dist/reserved.d.ts +39 -0
- package/dist/reserved.d.ts.map +1 -0
- package/dist/reserved.js +63 -0
- package/dist/reserved.js.map +1 -0
- package/dist/rules/compile.d.ts +27 -0
- package/dist/rules/compile.d.ts.map +1 -0
- package/dist/rules/compile.js +105 -0
- package/dist/rules/compile.js.map +1 -0
- package/dist/setup/model-free.d.ts +15 -0
- package/dist/setup/model-free.d.ts.map +1 -0
- package/dist/setup/model-free.js +26 -0
- package/dist/setup/model-free.js.map +1 -0
- package/dist/setup/openai.d.ts +46 -0
- package/dist/setup/openai.d.ts.map +1 -0
- package/dist/setup/openai.js +165 -0
- package/dist/setup/openai.js.map +1 -0
- package/dist/store/db.d.ts +61 -0
- package/dist/store/db.d.ts.map +1 -0
- package/dist/store/db.js +265 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/ids.d.ts +29 -0
- package/dist/store/ids.d.ts.map +1 -0
- package/dist/store/ids.js +0 -0
- package/dist/store/ids.js.map +1 -0
- package/dist/store/migrations.d.ts +54 -0
- package/dist/store/migrations.d.ts.map +1 -0
- package/dist/store/migrations.js +923 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/vectors.d.ts +34 -0
- package/dist/store/vectors.d.ts.map +1 -0
- package/dist/store/vectors.js +158 -0
- package/dist/store/vectors.js.map +1 -0
- package/dist/timeline/documents.d.ts +23 -0
- package/dist/timeline/documents.d.ts.map +1 -0
- package/dist/timeline/documents.js +262 -0
- package/dist/timeline/documents.js.map +1 -0
- package/dist/watch/watcher.d.ts +43 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +143 -0
- package/dist/watch/watcher.js.map +1 -0
- package/dist/write/atomic.d.ts +34 -0
- package/dist/write/atomic.d.ts.map +1 -0
- package/dist/write/atomic.js +58 -0
- package/dist/write/atomic.js.map +1 -0
- package/dist/write/conflict.d.ts +43 -0
- package/dist/write/conflict.d.ts.map +1 -0
- package/dist/write/conflict.js +149 -0
- package/dist/write/conflict.js.map +1 -0
- package/dist/write/edit.d.ts +59 -0
- package/dist/write/edit.d.ts.map +1 -0
- package/dist/write/edit.js +231 -0
- package/dist/write/edit.js.map +1 -0
- package/dist/write/gate.d.ts +59 -0
- package/dist/write/gate.d.ts.map +1 -0
- package/dist/write/gate.js +99 -0
- package/dist/write/gate.js.map +1 -0
- package/dist/write/journal.d.ts +90 -0
- package/dist/write/journal.d.ts.map +1 -0
- package/dist/write/journal.js +187 -0
- package/dist/write/journal.js.map +1 -0
- package/dist/write/ledger.d.ts +28 -0
- package/dist/write/ledger.d.ts.map +1 -0
- package/dist/write/ledger.js +141 -0
- package/dist/write/ledger.js.map +1 -0
- package/dist/write/placement.d.ts +29 -0
- package/dist/write/placement.d.ts.map +1 -0
- package/dist/write/placement.js +140 -0
- package/dist/write/placement.js.map +1 -0
- package/dist/write/remember-fallback.d.ts +18 -0
- package/dist/write/remember-fallback.d.ts.map +1 -0
- package/dist/write/remember-fallback.js +48 -0
- package/dist/write/remember-fallback.js.map +1 -0
- package/dist/write/retain.d.ts +61 -0
- package/dist/write/retain.d.ts.map +1 -0
- package/dist/write/retain.js +272 -0
- package/dist/write/retain.js.map +1 -0
- package/package.json +50 -0
- package/swift/extract.swift +232 -0
|
@@ -0,0 +1,1632 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AknoError } from '@tenphi/akno-protocol';
|
|
4
|
+
import { normalizeSlug } from "../ops/write.js";
|
|
5
|
+
import { sha256 } from "../store/ids.js";
|
|
6
|
+
import { rebuildEvidenceGraph } from "../index/graph.js";
|
|
7
|
+
import { runObserveMission } from "./observe.js";
|
|
8
|
+
import { planBrokenLinks } from "./link-repairs.js";
|
|
9
|
+
import { claimKey, findCrossPageConflicts, ineligibleConflictClaims, verifyConflicts, } from "./conflicts.js";
|
|
10
|
+
import { planContradictions } from "./contradictions.js";
|
|
11
|
+
import { housekeeping } from "./housekeeping.js";
|
|
12
|
+
import { ModelClient } from "../models/client.js";
|
|
13
|
+
import { planOrphanAdoptions } from "./adopt.js";
|
|
14
|
+
import { addedLines, logDreamRun } from "./log.js";
|
|
15
|
+
import { curatePages } from "./curate.js";
|
|
16
|
+
import { addSemanticMergeDiscoveryMetrics, } from "./semantic-merge-discovery.js";
|
|
17
|
+
import { applyMaintenancePlan, blockMaintenanceDependencies, createAdoptionPlan, createCurationPlan, createObservationPlan, createReflectionPlan, decideMaintenancePlanWithCurator, estimateAuditAutoCuratorWork, deferUnmetMaintenanceDependency, deferStaleMaintenanceItems, finalizeRetryableMaintenancePlans, findActiveMaintenancePlan, getMaintenancePlan, maintenanceItemApplySchedule, pruneMaintenancePlans, supersedeDependencyMaintenancePlan, } from "./plans.js";
|
|
18
|
+
import { beginDreamRun, completeDreamRun, dreamRunFileManifest, dreamRunIndexRevisionMatches, failDreamRun, refreshDreamRunSnapshot, } from "./runs.js";
|
|
19
|
+
import { assertMaintenanceModeAllowed, effectiveTransformPolicy, highestPolicyMode, policyMode, profileMode, } from "./profile.js";
|
|
20
|
+
import { createMaintenanceBudget, maintenanceBudgetReceipt, } from "./budget.js";
|
|
21
|
+
import { DreamModelTelemetry, } from "./model-telemetry.js";
|
|
22
|
+
import { verifyDreamRun } from "./run-verification.js";
|
|
23
|
+
import { Indexer } from "../index/indexer.js";
|
|
24
|
+
import { planManagedItems } from "./managed-items.js";
|
|
25
|
+
import { planRuleDrifts, ruleDriftPaths } from "./rule-drift.js";
|
|
26
|
+
import { assertProfileAutomaticApplyAvailable, configWithMaintenanceRecovery, maintenanceRecoveryStatus, recordMaintenanceRecovery, } from "./recovery.js";
|
|
27
|
+
/**
|
|
28
|
+
* Order matters twice.
|
|
29
|
+
*
|
|
30
|
+
* `conflicts` runs first because unresolved knowledge must be filtered before observation,
|
|
31
|
+
* reflection, or synthesis sees it. The retention ladder remains a user-facing hierarchy, not
|
|
32
|
+
* permission for a disputed claim to influence a higher tier before inspection.
|
|
33
|
+
*
|
|
34
|
+
* `housekeeping` runs last, so its report describes the knowledge base after plan-backed curation and
|
|
35
|
+
* adoption. Broken-link planning queries the complete structural index directly; it never consumes the
|
|
36
|
+
* housekeeping list, which is capped for readability.
|
|
37
|
+
*/
|
|
38
|
+
export const DREAM_PHASES = [
|
|
39
|
+
'conflicts',
|
|
40
|
+
'observe',
|
|
41
|
+
'reflect',
|
|
42
|
+
'curate',
|
|
43
|
+
'adopt',
|
|
44
|
+
'repair',
|
|
45
|
+
'housekeeping',
|
|
46
|
+
];
|
|
47
|
+
const CURATE_POLICY_KINDS = [
|
|
48
|
+
'hygiene',
|
|
49
|
+
'managed_item',
|
|
50
|
+
'synthesis',
|
|
51
|
+
'split',
|
|
52
|
+
'extract',
|
|
53
|
+
'merge',
|
|
54
|
+
'contradiction',
|
|
55
|
+
'broken_link',
|
|
56
|
+
'rule_drift',
|
|
57
|
+
];
|
|
58
|
+
export async function dream(ctx, options = {}) {
|
|
59
|
+
if (options.mode && options.dryRun) {
|
|
60
|
+
throw new AknoError('invalid', 'choose `mode: audit` instead of combining a mode with dryRun');
|
|
61
|
+
}
|
|
62
|
+
assertMaintenanceModeAllowed(ctx.config, options);
|
|
63
|
+
const started = performance.now();
|
|
64
|
+
// The tiers run unattended and are worth a better model than indexing needs — measured:
|
|
65
|
+
// the same observe pass over one knowledge base produced 15 candidates worth about four with a
|
|
66
|
+
// local 3B, and 8 candidates with no guard violations at all with a strong one. When
|
|
67
|
+
// `maintenance.model` is set, the whole cycle uses it and nothing else does.
|
|
68
|
+
const baseCycleModel = ctx.config.maintenance.model
|
|
69
|
+
? new ModelClient(ctx.config.maintenance.model)
|
|
70
|
+
: ctx.models.derive;
|
|
71
|
+
const telemetry = new DreamModelTelemetry(baseCycleModel.modelId);
|
|
72
|
+
let modelStage = options.phase ?? 'conflicts';
|
|
73
|
+
const pendingDerivation = new QueuedDeriveScheduler();
|
|
74
|
+
const runMode = dreamRunMode(ctx, options);
|
|
75
|
+
const recovery = maintenanceRecoveryStatus(ctx);
|
|
76
|
+
const cycle = {
|
|
77
|
+
...ctx,
|
|
78
|
+
config: configWithMaintenanceRecovery(ctx.config, recovery, runMode),
|
|
79
|
+
models: {
|
|
80
|
+
...ctx.models,
|
|
81
|
+
derive: baseCycleModel.withOutcomeObserver((observation) => telemetry.observe(modelStage, observation)),
|
|
82
|
+
},
|
|
83
|
+
// A maintenance write still indexes structure before it is verified. Hold its expensive
|
|
84
|
+
// follow-up until every planned write has landed, so changed facts can be re-derived and
|
|
85
|
+
// reclassified once, synchronously, before this run certifies its outcome.
|
|
86
|
+
derive: pendingDerivation,
|
|
87
|
+
};
|
|
88
|
+
const wanted = options.phase ? [options.phase] : DREAM_PHASES;
|
|
89
|
+
const startedRun = beginDreamRun(cycle, {
|
|
90
|
+
requestedPhase: options.phase ?? null,
|
|
91
|
+
requestedPhases: wanted,
|
|
92
|
+
mode: runMode,
|
|
93
|
+
dryRun: options.dryRun ?? false,
|
|
94
|
+
modelId: cycle.models.derive.modelId,
|
|
95
|
+
});
|
|
96
|
+
const report = {
|
|
97
|
+
run: startedRun,
|
|
98
|
+
phases: [],
|
|
99
|
+
observations: [],
|
|
100
|
+
curated: [],
|
|
101
|
+
managedItems: {
|
|
102
|
+
eligiblePages: 0,
|
|
103
|
+
inspectedMarkers: 0,
|
|
104
|
+
plannedPages: 0,
|
|
105
|
+
suppressedPages: 0,
|
|
106
|
+
findings: {
|
|
107
|
+
empty_marker: 0,
|
|
108
|
+
malformed_marker: 0,
|
|
109
|
+
duplicate_item: 0,
|
|
110
|
+
misplaced_item: 0,
|
|
111
|
+
placement_uncertain: 0,
|
|
112
|
+
placement_unavailable: 0,
|
|
113
|
+
section_created: 0,
|
|
114
|
+
misrouted_item: 0,
|
|
115
|
+
routing_deferred: 0,
|
|
116
|
+
routing_uncertain: 0,
|
|
117
|
+
routing_unavailable: 0,
|
|
118
|
+
wording_corrected: 0,
|
|
119
|
+
wording_uncertain: 0,
|
|
120
|
+
source_unavailable: 0,
|
|
121
|
+
item_conflict: 0,
|
|
122
|
+
valid: 0,
|
|
123
|
+
},
|
|
124
|
+
outcomes: { planned: 0, held: 0, valid: 0, suppressed: 0 },
|
|
125
|
+
placement: {
|
|
126
|
+
pagesConsidered: 0,
|
|
127
|
+
classifierCalls: 0,
|
|
128
|
+
cacheHits: 0,
|
|
129
|
+
kept: 0,
|
|
130
|
+
moved: 0,
|
|
131
|
+
sectionsCreated: 0,
|
|
132
|
+
uncertain: 0,
|
|
133
|
+
unavailable: 0,
|
|
134
|
+
},
|
|
135
|
+
routing: {
|
|
136
|
+
pagesConsidered: 0,
|
|
137
|
+
itemsConsidered: 0,
|
|
138
|
+
candidatesConsidered: 0,
|
|
139
|
+
classifierCalls: 0,
|
|
140
|
+
cacheHits: 0,
|
|
141
|
+
kept: 0,
|
|
142
|
+
moved: 0,
|
|
143
|
+
sectionsCreated: 0,
|
|
144
|
+
deferred: 0,
|
|
145
|
+
uncertain: 0,
|
|
146
|
+
unavailable: 0,
|
|
147
|
+
},
|
|
148
|
+
source: {
|
|
149
|
+
pagesConsidered: 0,
|
|
150
|
+
classifierCalls: 0,
|
|
151
|
+
cacheHits: 0,
|
|
152
|
+
supported: 0,
|
|
153
|
+
corrected: 0,
|
|
154
|
+
uncertain: 0,
|
|
155
|
+
unavailable: 0,
|
|
156
|
+
},
|
|
157
|
+
details: [],
|
|
158
|
+
},
|
|
159
|
+
rejected: [],
|
|
160
|
+
adopted: [],
|
|
161
|
+
conflicts: [],
|
|
162
|
+
repaired: null,
|
|
163
|
+
repairChangeId: null,
|
|
164
|
+
housekeeping: null,
|
|
165
|
+
changeId: null,
|
|
166
|
+
adoptChangeId: null,
|
|
167
|
+
curateChangeId: null,
|
|
168
|
+
maintenancePlan: null,
|
|
169
|
+
maintenancePlans: [],
|
|
170
|
+
planPrune: null,
|
|
171
|
+
budget: maintenanceBudgetReceipt(createMaintenanceBudget(ctx.config.maintenance.limits)),
|
|
172
|
+
modelUsage: telemetry.usage(),
|
|
173
|
+
degraded: telemetry.degradation(),
|
|
174
|
+
semanticMerge: null,
|
|
175
|
+
verification: null,
|
|
176
|
+
conflictRefresh: null,
|
|
177
|
+
autoEstimate: null,
|
|
178
|
+
warnings: [],
|
|
179
|
+
durationMs: 0,
|
|
180
|
+
};
|
|
181
|
+
for (const entry of recovery.transforms.filter((candidate) => candidate.pausedAt !== null)) {
|
|
182
|
+
report.warnings.push(`automatic ${entry.transform} maintenance is paused after ${entry.consecutiveFailures} consecutive verification rollbacks; resume it explicitly after inspection`);
|
|
183
|
+
}
|
|
184
|
+
// Collected whether or not anything reads it, because the phases are where the information
|
|
185
|
+
// is and threading it out conditionally is how a debugging flag ends up logging half a run.
|
|
186
|
+
const applied = [];
|
|
187
|
+
const budget = createMaintenanceBudget(ctx.config.maintenance.limits);
|
|
188
|
+
const deferAutomaticApply = !options.phase && !options.dryRun;
|
|
189
|
+
const lastWritablePlanner = Math.max(...['observe', 'reflect', 'curate', 'adopt'].map((phase) => wanted.lastIndexOf(phase)));
|
|
190
|
+
let plannerBarrier = null;
|
|
191
|
+
const releasePlannerBarrier = async () => {
|
|
192
|
+
if (!plannerBarrier)
|
|
193
|
+
return;
|
|
194
|
+
const barrier = plannerBarrier;
|
|
195
|
+
let revisionMatches = false;
|
|
196
|
+
let invalidated = false;
|
|
197
|
+
try {
|
|
198
|
+
revisionMatches = dreamRunIndexRevisionMatches(ctx, startedRun);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
plannerBarrier = null;
|
|
202
|
+
invalidated = (await barrier.release()).invalidated;
|
|
203
|
+
}
|
|
204
|
+
if (invalidated) {
|
|
205
|
+
// A foreground mutation has already won and structurally indexed. Recheck the exact
|
|
206
|
+
// sealed inputs so affected proposals become typed stale work; unrelated proposals may be
|
|
207
|
+
// reused by a fresh run, but this mixed-revision invocation never reaches a curator.
|
|
208
|
+
await deferStaleMaintenanceItems(cycle, report.maintenancePlans.map((plan) => plan.id));
|
|
209
|
+
throw new AknoError('conflict', 'a foreground memory write changed the indexed revision during dream planning; nothing was applied', { retryable: true });
|
|
210
|
+
}
|
|
211
|
+
if (!revisionMatches) {
|
|
212
|
+
throw new AknoError('internal', 'the indexed revision changed inside the dream planner barrier');
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
assertProfileAutomaticApplyAvailable(recovery, runMode);
|
|
217
|
+
if (!options.phase && ctx.writable) {
|
|
218
|
+
plannerBarrier = await ctx.indexer.acquireRevisionBarrier();
|
|
219
|
+
// `beginDreamRun` claims exclusivity before waiting for an in-flight index pass. Once the
|
|
220
|
+
// barrier owns the lane, this becomes the exact revision every full-run planner reads.
|
|
221
|
+
refreshDreamRunSnapshot(ctx, startedRun);
|
|
222
|
+
}
|
|
223
|
+
if (cycle.writable)
|
|
224
|
+
finalizeRetryableMaintenancePlans(cycle);
|
|
225
|
+
// A selected inference or curation phase still gets the same safety boundary as a full run.
|
|
226
|
+
// It does not add a second visible phase to the report; it supplies that phase's prerequisites.
|
|
227
|
+
if (options.phase &&
|
|
228
|
+
options.phase !== 'conflicts' &&
|
|
229
|
+
['observe', 'reflect', 'curate'].includes(options.phase) &&
|
|
230
|
+
cycle.config.maintenance.conflicts.enabled) {
|
|
231
|
+
modelStage = 'conflicts';
|
|
232
|
+
await inspectConflicts(cycle, report, telemetry);
|
|
233
|
+
}
|
|
234
|
+
for (const [phaseIndex, phase] of wanted.entries()) {
|
|
235
|
+
modelStage = phase;
|
|
236
|
+
const phaseStarted = performance.now();
|
|
237
|
+
const skipped = await runPhase(cycle, phase, options, report, applied, budget, deferAutomaticApply, telemetry);
|
|
238
|
+
report.phases.push({
|
|
239
|
+
phase,
|
|
240
|
+
ran: skipped === null,
|
|
241
|
+
...(skipped ? { skipped } : {}),
|
|
242
|
+
durationMs: Math.round(performance.now() - phaseStarted),
|
|
243
|
+
});
|
|
244
|
+
if (plannerBarrier?.invalidated)
|
|
245
|
+
await releasePlannerBarrier();
|
|
246
|
+
if (deferAutomaticApply && phaseIndex === lastWritablePlanner) {
|
|
247
|
+
await releasePlannerBarrier();
|
|
248
|
+
await decideAndApplyPlannedPhases(cycle, report, applied, budget, telemetry);
|
|
249
|
+
await retryDependencyDeferredPhases(cycle, options, report, applied, budget, telemetry, (stage) => {
|
|
250
|
+
modelStage = stage;
|
|
251
|
+
});
|
|
252
|
+
modelStage = 'conflicts';
|
|
253
|
+
report.conflictRefresh = await refreshChangedClaimEligibility(cycle, pendingDerivation, report, telemetry);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
await releasePlannerBarrier();
|
|
257
|
+
if (!deferAutomaticApply) {
|
|
258
|
+
modelStage = 'conflicts';
|
|
259
|
+
report.conflictRefresh = await refreshChangedClaimEligibility(cycle, pendingDerivation, report, telemetry);
|
|
260
|
+
}
|
|
261
|
+
report.budget = maintenanceBudgetReceipt(budget);
|
|
262
|
+
report.modelUsage = telemetry.usage();
|
|
263
|
+
report.degraded = telemetry.degradation();
|
|
264
|
+
report.verification = await verifyDreamRun(cycle, report.maintenancePlans.map((plan) => plan.id), budget, report.budget, report.modelUsage, dreamRunFileManifest(startedRun));
|
|
265
|
+
report.autoEstimate =
|
|
266
|
+
report.run.mode === 'audit'
|
|
267
|
+
? estimateAuditAutoCuratorWork(cycle, report.maintenancePlans.map((plan) => plan.id), { sealedPlans: !report.run.dryRun })
|
|
268
|
+
: null;
|
|
269
|
+
report.durationMs = Math.round(performance.now() - started);
|
|
270
|
+
recordMaintenanceRecovery(cycle, {
|
|
271
|
+
runId: startedRun.id,
|
|
272
|
+
mode: runMode,
|
|
273
|
+
planIds: report.maintenancePlans.map((plan) => plan.id),
|
|
274
|
+
verification: report.verification,
|
|
275
|
+
});
|
|
276
|
+
report.planPrune = pruneMaintenancePlans(cycle, {
|
|
277
|
+
apply: cycle.writable && !(options.dryRun ?? false),
|
|
278
|
+
});
|
|
279
|
+
if (report.planPrune.applied && report.planPrune.payloads.plans > 0) {
|
|
280
|
+
for (const planned of [...report.maintenancePlans]) {
|
|
281
|
+
try {
|
|
282
|
+
recordMaintenancePlan(report, getMaintenancePlan(cycle, planned.id));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (AknoError.from(error).code !== 'not_found')
|
|
286
|
+
throw error;
|
|
287
|
+
// A zero-day compact receipt window deliberately removes even this run's terminal
|
|
288
|
+
// plan. The run receipt still records its id and the pruning receipt explains why.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (ctx.config.maintenance.logChanges) {
|
|
293
|
+
const logPath = await logDreamRun(ctx, report, applied, { dryRun: options.dryRun ?? false });
|
|
294
|
+
if (logPath)
|
|
295
|
+
report.logPath = logPath;
|
|
296
|
+
}
|
|
297
|
+
report.run = completeDreamRun(ctx, startedRun, report);
|
|
298
|
+
// Any non-knowledge work, conflicts-disabled work, or failed derivation resumes through the
|
|
299
|
+
// ordinary post-response worker only after the run has sealed its observed final state.
|
|
300
|
+
ctx.derive.schedule(pendingDerivation.take());
|
|
301
|
+
return report;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
try {
|
|
305
|
+
await releasePlannerBarrier();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// The original failure remains the run error. Releasing the in-process lane is mandatory;
|
|
309
|
+
// queued index callers keep their own typed outcome and final tree verification did not run.
|
|
310
|
+
}
|
|
311
|
+
// A valid write must not lose its ordinary background indexing merely because a later
|
|
312
|
+
// planner or final verifier failed before the changed-claim barrier could consume it.
|
|
313
|
+
ctx.derive.schedule(pendingDerivation.take());
|
|
314
|
+
report.durationMs = Math.round(performance.now() - started);
|
|
315
|
+
report.budget = maintenanceBudgetReceipt(budget);
|
|
316
|
+
report.modelUsage = telemetry.usage();
|
|
317
|
+
report.degraded = telemetry.degradation();
|
|
318
|
+
report.run = failDreamRun(ctx, startedRun, error, report.durationMs, report.phases, report.budget, report);
|
|
319
|
+
try {
|
|
320
|
+
recordMaintenanceRecovery(cycle, {
|
|
321
|
+
runId: startedRun.id,
|
|
322
|
+
mode: runMode,
|
|
323
|
+
planIds: report.maintenancePlans.map((plan) => plan.id),
|
|
324
|
+
verification: report.verification,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Preserve the original lifecycle error. A store that cannot retain recovery state is
|
|
329
|
+
// already surfaced by that failure and must not replace its more specific cause.
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Collect paths without starting the ordinary post-response worker. */
|
|
335
|
+
class QueuedDeriveScheduler {
|
|
336
|
+
#paths = new Set();
|
|
337
|
+
schedule(relPaths) {
|
|
338
|
+
for (const relPath of relPaths)
|
|
339
|
+
this.#paths.add(relPath);
|
|
340
|
+
}
|
|
341
|
+
async flush() {
|
|
342
|
+
// Deliberately idle: `dream` owns the barrier and drains the queue explicitly.
|
|
343
|
+
}
|
|
344
|
+
take() {
|
|
345
|
+
const paths = [...this.#paths];
|
|
346
|
+
this.#paths.clear();
|
|
347
|
+
return paths;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Re-derive facts changed by successful maintenance writes and rebuild conflict eligibility.
|
|
352
|
+
*
|
|
353
|
+
* Structural indexing deliberately leaves a changed page's previous `derived_hash` in place.
|
|
354
|
+
* That mismatch is the durable stale marker: until a complete fact derivation succeeds, readers
|
|
355
|
+
* must exclude the page's old facts rather than treating pre-write claims as current evidence.
|
|
356
|
+
*/
|
|
357
|
+
async function refreshChangedClaimEligibility(ctx, pending, report, telemetry) {
|
|
358
|
+
const paths = pending.take();
|
|
359
|
+
const checkedAt = new Date().toISOString();
|
|
360
|
+
if (paths.length === 0) {
|
|
361
|
+
return emptyConflictRefresh('no_claim_changes', checkedAt);
|
|
362
|
+
}
|
|
363
|
+
const pages = livePagesForPaths(ctx, paths);
|
|
364
|
+
const knowledge = pages.filter((page) => page.role === 'knowledge');
|
|
365
|
+
if (knowledge.length === 0) {
|
|
366
|
+
pending.schedule(paths);
|
|
367
|
+
return {
|
|
368
|
+
...emptyConflictRefresh('no_claim_changes', checkedAt),
|
|
369
|
+
changedFiles: paths.length,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (!ctx.config.maintenance.conflicts.enabled) {
|
|
373
|
+
pending.schedule(paths);
|
|
374
|
+
return {
|
|
375
|
+
...emptyConflictRefresh('conflicts_disabled', checkedAt),
|
|
376
|
+
changedFiles: paths.length,
|
|
377
|
+
knowledgePages: knowledge.length,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (!ctx.config.index.facts) {
|
|
381
|
+
// The structural pass has already rebuilt the graph. With fact mining disabled there is no
|
|
382
|
+
// changed claim set to derive; summaries may still finish through the ordinary worker.
|
|
383
|
+
await inspectConflicts(ctx, report, telemetry);
|
|
384
|
+
pending.schedule(paths);
|
|
385
|
+
return {
|
|
386
|
+
status: 'passed',
|
|
387
|
+
cause: 'facts_disabled',
|
|
388
|
+
changedFiles: paths.length,
|
|
389
|
+
knowledgePages: knowledge.length,
|
|
390
|
+
currentPages: 0,
|
|
391
|
+
stalePages: 0,
|
|
392
|
+
candidates: report.conflicts.length,
|
|
393
|
+
unverified: report.conflicts.filter((conflict) => conflict.verdict === 'unverified').length,
|
|
394
|
+
checkedAt,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (!ctx.models.derive.available) {
|
|
398
|
+
telemetry.degrade('conflicts', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
try {
|
|
402
|
+
const indexer = new Indexer(ctx.config, ctx.store, {
|
|
403
|
+
embedding: ctx.models.embedding,
|
|
404
|
+
derive: ctx.models.derive,
|
|
405
|
+
});
|
|
406
|
+
await indexer.run({ only: paths, modelPaths: paths, reindexUnchanged: true });
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
// Preserve the ordinary retry path for infrastructure failures. Semantic model failures
|
|
410
|
+
// return an index report instead and are represented by the stale hash below.
|
|
411
|
+
pending.schedule(paths);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const refreshed = livePagesForPaths(ctx, paths).filter((page) => page.role === 'knowledge');
|
|
416
|
+
const current = refreshed.filter((page) => page.derived_hash === page.body_hash);
|
|
417
|
+
const stale = refreshed.length - current.length;
|
|
418
|
+
if (stale > 0 && ctx.models.derive.available) {
|
|
419
|
+
telemetry.degrade('conflicts', 'derive_failed');
|
|
420
|
+
}
|
|
421
|
+
if (stale > 0) {
|
|
422
|
+
pending.schedule(ctx.models.derive.available
|
|
423
|
+
? refreshed.filter((page) => page.derived_hash !== page.body_hash).map((page) => page.rel_path)
|
|
424
|
+
: paths);
|
|
425
|
+
}
|
|
426
|
+
// This replaces the pre-write report as well as rebuilding the graph. New claim fingerprints
|
|
427
|
+
// naturally miss old cached verdicts, while unchanged fingerprints may safely reuse them.
|
|
428
|
+
await inspectConflicts(ctx, report, telemetry);
|
|
429
|
+
const unverified = report.conflicts.filter((conflict) => conflict.verdict === 'unverified').length;
|
|
430
|
+
return {
|
|
431
|
+
status: stale > 0 || (ctx.config.maintenance.conflicts.verify && unverified > 0) ? 'degraded' : 'passed',
|
|
432
|
+
cause: null,
|
|
433
|
+
changedFiles: paths.length,
|
|
434
|
+
knowledgePages: refreshed.length,
|
|
435
|
+
currentPages: current.length,
|
|
436
|
+
stalePages: stale,
|
|
437
|
+
candidates: report.conflicts.length,
|
|
438
|
+
unverified,
|
|
439
|
+
checkedAt,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function emptyConflictRefresh(cause, checkedAt) {
|
|
443
|
+
return {
|
|
444
|
+
status: 'not_needed',
|
|
445
|
+
cause,
|
|
446
|
+
changedFiles: 0,
|
|
447
|
+
knowledgePages: 0,
|
|
448
|
+
currentPages: 0,
|
|
449
|
+
stalePages: 0,
|
|
450
|
+
candidates: 0,
|
|
451
|
+
unverified: 0,
|
|
452
|
+
checkedAt,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function livePagesForPaths(ctx, paths) {
|
|
456
|
+
if (paths.length === 0)
|
|
457
|
+
return [];
|
|
458
|
+
return ctx.store.db
|
|
459
|
+
.prepare(`SELECT rel_path, role, body_hash, derived_hash FROM pages
|
|
460
|
+
WHERE rel_path IN (${paths.map(() => '?').join(',')})`)
|
|
461
|
+
.all(...paths);
|
|
462
|
+
}
|
|
463
|
+
function dreamRunMode(ctx, options) {
|
|
464
|
+
if (options.dryRun)
|
|
465
|
+
return 'audit';
|
|
466
|
+
if (options.mode)
|
|
467
|
+
return options.mode;
|
|
468
|
+
return profileMode(ctx.config.maintenance.profile);
|
|
469
|
+
}
|
|
470
|
+
function curationPolicies(ctx, options) {
|
|
471
|
+
return Object.fromEntries(CURATE_POLICY_KINDS.map((kind) => {
|
|
472
|
+
const effective = effectiveTransformPolicy(ctx.config, kind, options.mode);
|
|
473
|
+
return [kind, policyMode(effective) ?? 'off'];
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
function planMatchesPolicies(plan, policies) {
|
|
477
|
+
return plan.items.every((item) => item.policy === policies[item.kind]);
|
|
478
|
+
}
|
|
479
|
+
async function runPhase(ctx, phase, options, report, applied, budget, deferAutomaticApply, telemetry) {
|
|
480
|
+
switch (phase) {
|
|
481
|
+
case 'observe': {
|
|
482
|
+
if (!ctx.config.maintenance.observe.enabled)
|
|
483
|
+
return 'disabled in config';
|
|
484
|
+
if (!policyMode(effectiveTransformPolicy(ctx.config, 'observe', options.mode))) {
|
|
485
|
+
return 'observe policy is off';
|
|
486
|
+
}
|
|
487
|
+
if (!ctx.models.derive.available) {
|
|
488
|
+
telemetry.degrade('observe', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
489
|
+
return `no model for the cycle: ${ctx.models.derive.unavailableReason ?? 'unavailable'}`;
|
|
490
|
+
}
|
|
491
|
+
await observePhase(ctx, options, report, applied, budget, deferAutomaticApply, telemetry);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
case 'reflect': {
|
|
495
|
+
// Reflect ships as an extension point, off by default. At a few hundred pages a
|
|
496
|
+
// "pattern" is one coincidence away from noise, so the default is not a placeholder —
|
|
497
|
+
// it is the recommendation.
|
|
498
|
+
if (!ctx.config.maintenance.reflect.enabled) {
|
|
499
|
+
return 'off by default — enable it once the knowledge base has the volume for it';
|
|
500
|
+
}
|
|
501
|
+
if (!policyMode(effectiveTransformPolicy(ctx.config, 'reflect', options.mode))) {
|
|
502
|
+
return 'reflect policy is off';
|
|
503
|
+
}
|
|
504
|
+
if (!ctx.models.derive.available) {
|
|
505
|
+
telemetry.degrade('reflect', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
506
|
+
return `no model for the cycle: ${ctx.models.derive.unavailableReason ?? 'unavailable'}`;
|
|
507
|
+
}
|
|
508
|
+
await reflectPhase(ctx, options, report, applied, budget, deferAutomaticApply, telemetry);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
case 'curate': {
|
|
512
|
+
const policyMatrix = !options.dryRun;
|
|
513
|
+
const policies = curationPolicies(ctx, options);
|
|
514
|
+
const managedItemResult = policies.managed_item === 'off'
|
|
515
|
+
? { drafts: [], report: report.managedItems }
|
|
516
|
+
: await planManagedItems(ctx, {
|
|
517
|
+
conflictClaims: ineligibleConflictClaims(report.conflicts),
|
|
518
|
+
});
|
|
519
|
+
report.managedItems = managedItemResult.report;
|
|
520
|
+
if (managedItemResult.report.outcomes.held > 0) {
|
|
521
|
+
report.warnings.push(`${managedItemResult.report.outcomes.held} managed-item finding${managedItemResult.report.outcomes.held === 1 ? '' : 's'} require${managedItemResult.report.outcomes.held === 1 ? 's' : ''} inspection; surrounding page bytes were not changed`);
|
|
522
|
+
}
|
|
523
|
+
const allowedKinds = new Set(['hygiene', 'synthesis', 'split', 'extract', 'merge'].filter((kind) => policies[kind] !== 'off'));
|
|
524
|
+
// A configured mode is how a full scheduled run gets plan-backed curation without
|
|
525
|
+
// turning the scheduler into a curate-only command. An explicit dry run keeps its
|
|
526
|
+
// existing read-only path; creating a durable plan needs the service's write handle.
|
|
527
|
+
const mode = policyMatrix ? highestPolicyMode(Object.values(policies)) : null;
|
|
528
|
+
if (policyMatrix && !mode)
|
|
529
|
+
return 'all curation policies are off';
|
|
530
|
+
if (!ctx.models.derive.available && !mode) {
|
|
531
|
+
telemetry.degrade('curate', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
532
|
+
return `no model for the cycle: ${ctx.models.derive.unavailableReason ?? 'unavailable'}`;
|
|
533
|
+
}
|
|
534
|
+
if (mode) {
|
|
535
|
+
// Reuse every unfinished plan, not only autonomous ones. Re-running audit or review must
|
|
536
|
+
// not spend another model call to rediscover a decision already waiting in the queue.
|
|
537
|
+
let plan = findActiveMaintenancePlan(ctx, mode, 'curate');
|
|
538
|
+
if (plan && policyMatrix && !planMatchesPolicies(plan, policies))
|
|
539
|
+
plan = null;
|
|
540
|
+
if (plan) {
|
|
541
|
+
report.curated = plan.items.filter(isGeneralCurationItem).map((item) => ({
|
|
542
|
+
slug: item.subject,
|
|
543
|
+
mode: item.kind === 'hygiene' ? 'hygiene' : 'synthesize',
|
|
544
|
+
action: 'would-update',
|
|
545
|
+
splits: item.kind === 'split'
|
|
546
|
+
? item.operations
|
|
547
|
+
.filter((operation) => operation.type === 'create')
|
|
548
|
+
.map((operation) => operation.relPath.replace(/\\/g, '/').replace(/\.(md|markdown)$/i, ''))
|
|
549
|
+
: [],
|
|
550
|
+
extractions: item.kind === 'extract'
|
|
551
|
+
? item.operations
|
|
552
|
+
.filter((operation) => operation.type === 'create')
|
|
553
|
+
.map((operation) => operation.relPath.replace(/\\/g, '/').replace(/\.(md|markdown)$/i, ''))
|
|
554
|
+
: [],
|
|
555
|
+
merges: item.kind === 'merge'
|
|
556
|
+
? item.operations
|
|
557
|
+
.filter((operation) => operation.type === 'delete')
|
|
558
|
+
.map((operation) => operation.relPath.replace(/\\/g, '/').replace(/\.(md|markdown)$/i, ''))
|
|
559
|
+
: [],
|
|
560
|
+
issues: [],
|
|
561
|
+
}));
|
|
562
|
+
report.repaired = repairResultFromPlan(plan);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (!ctx.models.derive.available) {
|
|
566
|
+
telemetry.degrade('curate', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
567
|
+
}
|
|
568
|
+
const contradictionResult = ctx.config.maintenance.conflicts.resolve && (!policyMatrix || policies.contradiction !== 'off')
|
|
569
|
+
? await planContradictions(ctx, report.conflicts)
|
|
570
|
+
: { drafts: [], warnings: [] };
|
|
571
|
+
report.warnings.push(...contradictionResult.warnings);
|
|
572
|
+
const managedPaths = new Set(managedItemResult.drafts.map((draft) => draft.relPath));
|
|
573
|
+
const contradictionDrafts = contradictionResult.drafts.filter((draft) => draft.operations.every((operation) => !managedPaths.has(operation.relPath)));
|
|
574
|
+
if (contradictionDrafts.length !== contradictionResult.drafts.length) {
|
|
575
|
+
report.warnings.push(`${contradictionResult.drafts.length - contradictionDrafts.length} contradiction repair${contradictionResult.drafts.length - contradictionDrafts.length === 1 ? '' : 's'} deferred behind an owned managed-item repair`);
|
|
576
|
+
}
|
|
577
|
+
const linkResult = ctx.config.maintenance.repair.links && (!policyMatrix || policies.broken_link !== 'off')
|
|
578
|
+
? await planBrokenLinks(ctx, ctx.config.maintenance.repair.maxChanges)
|
|
579
|
+
: { drafts: [], report: { links: [], claims: [], declined: [] } };
|
|
580
|
+
report.repaired = linkResult.report;
|
|
581
|
+
const ruleDriftDrafts = !policyMatrix || policies.rule_drift !== 'off'
|
|
582
|
+
? await planRuleDrifts(ctx, { limit: ctx.config.maintenance.curate.maxRuleDrifts })
|
|
583
|
+
: [];
|
|
584
|
+
const result = ctx.models.derive.available
|
|
585
|
+
? await curatePages(ctx, {
|
|
586
|
+
dryRun: true,
|
|
587
|
+
// Planning may update derived curation fingerprints even though it cannot touch KB
|
|
588
|
+
// bytes. Persisting unchanged and guard-rejected inputs is what makes the next cycle
|
|
589
|
+
// converge; staged pages remain `preview` until their durable item is decided.
|
|
590
|
+
recordState: true,
|
|
591
|
+
includePreviewed: true,
|
|
592
|
+
...(policyMatrix ? { allowedKinds } : {}),
|
|
593
|
+
})
|
|
594
|
+
: {
|
|
595
|
+
pages: [],
|
|
596
|
+
drafts: [],
|
|
597
|
+
degraded: [],
|
|
598
|
+
semanticMerge: null,
|
|
599
|
+
warnings: [
|
|
600
|
+
`page transformations were skipped because no maintenance model is available: ${ctx.models.derive.unavailableReason ?? 'unavailable'}`,
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
report.curated = result.pages;
|
|
604
|
+
report.warnings.push(...result.warnings);
|
|
605
|
+
report.semanticMerge = addSemanticMergeDiscoveryMetrics(report.semanticMerge, result.semanticMerge);
|
|
606
|
+
for (const degradation of result.degraded) {
|
|
607
|
+
telemetry.degrade('curate', degradation.reason, degradation.failure);
|
|
608
|
+
}
|
|
609
|
+
// A contradiction item has priority over a general synthesis of the same page. Two
|
|
610
|
+
// sealed items replacing one input would make the second stale by construction.
|
|
611
|
+
const contradictionPaths = new Set(contradictionDrafts.flatMap((draft) => draft.operations.map((operation) => operation.relPath)));
|
|
612
|
+
const linkDrafts = [];
|
|
613
|
+
const linkMutations = new Set([...managedPaths, ...contradictionPaths]);
|
|
614
|
+
const linkSeals = new Set();
|
|
615
|
+
for (const draft of linkResult.drafts) {
|
|
616
|
+
const mutations = draft.operations.map((operation) => operation.relPath);
|
|
617
|
+
const seals = draft.targets.map((target) => target.relPath);
|
|
618
|
+
const overlaps = mutations.some((relPath) => linkMutations.has(relPath) || linkSeals.has(relPath)) ||
|
|
619
|
+
seals.some((relPath) => linkMutations.has(relPath));
|
|
620
|
+
if (overlaps)
|
|
621
|
+
continue;
|
|
622
|
+
linkDrafts.push(draft);
|
|
623
|
+
for (const relPath of mutations)
|
|
624
|
+
linkMutations.add(relPath);
|
|
625
|
+
for (const relPath of seals)
|
|
626
|
+
linkSeals.add(relPath);
|
|
627
|
+
}
|
|
628
|
+
for (const draft of linkResult.drafts.filter((candidate) => !linkDrafts.includes(candidate))) {
|
|
629
|
+
for (const repair of draft.repairs) {
|
|
630
|
+
report.repaired.links = report.repaired.links.filter((entry) => entry !== repair);
|
|
631
|
+
report.repaired.declined.push({
|
|
632
|
+
what: `[[${repair.brokenTarget}]]`,
|
|
633
|
+
reason: 'deferred until an overlapping maintenance item settles',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const occupiedRuleDriftPaths = new Set([...linkMutations, ...linkSeals]);
|
|
638
|
+
const selectedRuleDrifts = [];
|
|
639
|
+
for (const draft of ruleDriftDrafts) {
|
|
640
|
+
const paths = ruleDriftPaths(draft);
|
|
641
|
+
if (paths.some((relPath) => occupiedRuleDriftPaths.has(relPath)))
|
|
642
|
+
continue;
|
|
643
|
+
selectedRuleDrifts.push(draft);
|
|
644
|
+
for (const relPath of paths)
|
|
645
|
+
occupiedRuleDriftPaths.add(relPath);
|
|
646
|
+
}
|
|
647
|
+
const protectedPaths = new Set([
|
|
648
|
+
...managedPaths,
|
|
649
|
+
...contradictionPaths,
|
|
650
|
+
...linkDrafts.flatMap(linkDraftPaths),
|
|
651
|
+
...selectedRuleDrifts.flatMap(ruleDriftPaths),
|
|
652
|
+
]);
|
|
653
|
+
const curationDrafts = result.drafts.filter((draft) => !operationsTouchedByCurateDraft(draft).some((relPath) => protectedPaths.has(relPath)));
|
|
654
|
+
const deferredSlugs = new Set(result.drafts.filter((draft) => !curationDrafts.includes(draft)).map((draft) => draft.slug));
|
|
655
|
+
report.curated = report.curated.map((page) => deferredSlugs.has(page.slug)
|
|
656
|
+
? {
|
|
657
|
+
...page,
|
|
658
|
+
action: 'rejected',
|
|
659
|
+
issues: ['general synthesis deferred until a higher-priority bounded item settles'],
|
|
660
|
+
}
|
|
661
|
+
: page);
|
|
662
|
+
plan = createCurationPlan(ctx, mode, curationDrafts, contradictionDrafts, linkDrafts, managedItemResult.drafts, selectedRuleDrifts, policies);
|
|
663
|
+
}
|
|
664
|
+
if (!plan)
|
|
665
|
+
return null;
|
|
666
|
+
if (mode === 'auto' && !deferAutomaticApply) {
|
|
667
|
+
if (plan.items.some((item) => item.status === 'proposed' && item.policy === 'auto')) {
|
|
668
|
+
plan = await decideDreamPlanWithCurator(ctx, plan.id, telemetry);
|
|
669
|
+
}
|
|
670
|
+
if (plan.items.some((item) => ['approved', 'applying', 'verification_pending'].includes(item.status))) {
|
|
671
|
+
const appliedResult = await applyMaintenancePlan(ctx, plan.id, budget);
|
|
672
|
+
plan = appliedResult.plan;
|
|
673
|
+
applied.push(...appliedResult.files.map((file) => asApplied('curate', file)));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
report.curated = curationReportFromPlan(report.curated, plan);
|
|
677
|
+
report.repaired = repairResultFromPlan(plan, report.repaired?.declined ?? []);
|
|
678
|
+
recordMaintenancePlan(report, plan);
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const result = await curatePages(ctx, { dryRun: true, recordState: false, allowedKinds });
|
|
682
|
+
report.curated = result.pages;
|
|
683
|
+
report.curateChangeId = result.changeId;
|
|
684
|
+
report.warnings.push(...result.warnings);
|
|
685
|
+
report.semanticMerge = addSemanticMergeDiscoveryMetrics(report.semanticMerge, result.semanticMerge);
|
|
686
|
+
for (const degradation of result.degraded) {
|
|
687
|
+
telemetry.degrade('curate', degradation.reason, degradation.failure);
|
|
688
|
+
}
|
|
689
|
+
applied.push(...result.files.map((file) => asApplied('curate', file)));
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
case 'adopt': {
|
|
693
|
+
const adoptPolicy = policyMode(effectiveTransformPolicy(ctx.config, 'adopt', options.mode));
|
|
694
|
+
if (!adoptPolicy)
|
|
695
|
+
return 'adopt policy is off';
|
|
696
|
+
const result = await planOrphanAdoptions(ctx, {
|
|
697
|
+
limit: ctx.config.maintenance.adopt.maxPages,
|
|
698
|
+
});
|
|
699
|
+
report.adopted = result.adopted;
|
|
700
|
+
if (options.dryRun)
|
|
701
|
+
return null;
|
|
702
|
+
const mode = adoptPolicy;
|
|
703
|
+
let plan = findActiveMaintenancePlan(ctx, mode, 'adopt');
|
|
704
|
+
if (plan && !planMatchesPolicies(plan, { adopt: adoptPolicy }))
|
|
705
|
+
plan = null;
|
|
706
|
+
if (!plan && result.drafts.length > 0) {
|
|
707
|
+
plan = createAdoptionPlan(ctx, mode, result.drafts, report.run.snapshot, adoptPolicy);
|
|
708
|
+
}
|
|
709
|
+
if (!plan)
|
|
710
|
+
return null;
|
|
711
|
+
for (const item of plan.items.filter((candidate) => candidate.kind === 'adopt')) {
|
|
712
|
+
if (report.adopted.some((entry) => entry.slug === item.subject))
|
|
713
|
+
continue;
|
|
714
|
+
report.adopted.push({
|
|
715
|
+
slug: item.subject,
|
|
716
|
+
files: item.evidence.flatMap((entry) => entry.type === 'document' && entry.documentRelPath ? [entry.documentRelPath] : []),
|
|
717
|
+
action: 'planned',
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (mode === 'auto' && !deferAutomaticApply) {
|
|
721
|
+
if (plan.items.some((item) => item.status === 'proposed' && item.policy === 'auto')) {
|
|
722
|
+
plan = await decideDreamPlanWithCurator(ctx, plan.id, telemetry);
|
|
723
|
+
}
|
|
724
|
+
if (plan.items.some((item) => ['approved', 'applying', 'verification_pending'].includes(item.status))) {
|
|
725
|
+
const appliedResult = await applyMaintenancePlan(ctx, plan.id, budget);
|
|
726
|
+
plan = appliedResult.plan;
|
|
727
|
+
applied.push(...appliedResult.files.map((file) => asApplied('adopt', file)));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
report.adopted = adoptionReportFromPlan(report.adopted, plan);
|
|
731
|
+
const adoptionChanges = plan.items
|
|
732
|
+
.map((item) => item.changeId)
|
|
733
|
+
.filter((id) => id !== null);
|
|
734
|
+
// Compatibility for callers that adopted one orphan before adoption gained per-item plans.
|
|
735
|
+
report.adoptChangeId = adoptionChanges.length === 1 ? adoptionChanges[0] : null;
|
|
736
|
+
recordMaintenancePlan(report, plan);
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
case 'conflicts': {
|
|
740
|
+
if (!ctx.config.maintenance.conflicts.enabled)
|
|
741
|
+
return 'disabled in config';
|
|
742
|
+
await inspectConflicts(ctx, report, telemetry);
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
case 'repair': {
|
|
746
|
+
if (!ctx.config.maintenance.repair.enabled)
|
|
747
|
+
return 'disabled in config';
|
|
748
|
+
return repairPhase(ctx, report);
|
|
749
|
+
}
|
|
750
|
+
case 'housekeeping': {
|
|
751
|
+
report.housekeeping = await housekeeping(ctx);
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
async function inspectConflicts(ctx, report, telemetry) {
|
|
757
|
+
const candidates = findCrossPageConflicts(ctx, ctx.config.maintenance.conflicts.maxPairs);
|
|
758
|
+
if (!ctx.config.maintenance.conflicts.verify) {
|
|
759
|
+
report.conflicts = candidates;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (candidates.length > 0 && !ctx.models.derive.available) {
|
|
763
|
+
telemetry.degrade('conflicts', ctx.models.derive.degradedReason({}), 'unavailable');
|
|
764
|
+
}
|
|
765
|
+
const verified = await verifyConflicts(ctx, candidates);
|
|
766
|
+
report.conflicts = verified.conflicts;
|
|
767
|
+
report.warnings.push(...verified.warnings);
|
|
768
|
+
// Conflict verdicts are content-addressed graph eligibility. Refresh immediately after
|
|
769
|
+
// caching them so an audit-only cycle does not leave relationship edges one night behind.
|
|
770
|
+
if (ctx.writable) {
|
|
771
|
+
rebuildEvidenceGraph(ctx.store, {
|
|
772
|
+
conflictModelId: ctx.models.derive.modelId,
|
|
773
|
+
contextualModelId: ctx.config.graph.contextualResolution.enabled ? ctx.models.derive.modelId : null,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function operationsTouchedByCurateDraft(draft) {
|
|
778
|
+
return [
|
|
779
|
+
draft.relPath,
|
|
780
|
+
...draft.children.map((child) => child.relPath),
|
|
781
|
+
...draft.extractions.map((extraction) => extraction.relPath),
|
|
782
|
+
...(draft.merge?.linkUpdates.map((update) => update.relPath) ?? []),
|
|
783
|
+
...(draft.merge ? [draft.merge.sourceRelPath] : []),
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
function linkDraftPaths(draft) {
|
|
787
|
+
return [
|
|
788
|
+
...draft.operations.map((operation) => operation.relPath),
|
|
789
|
+
...draft.targets.map((target) => target.relPath),
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
function isGeneralCurationItem(item) {
|
|
793
|
+
return (item.kind !== 'observe' &&
|
|
794
|
+
item.kind !== 'reflect' &&
|
|
795
|
+
item.kind !== 'contradiction' &&
|
|
796
|
+
item.kind !== 'managed_item' &&
|
|
797
|
+
item.kind !== 'broken_link' &&
|
|
798
|
+
item.kind !== 'rule_drift' &&
|
|
799
|
+
item.kind !== 'adopt');
|
|
800
|
+
}
|
|
801
|
+
function repairResultFromPlan(plan, declined = []) {
|
|
802
|
+
const links = plan.items.flatMap((item) => {
|
|
803
|
+
if (item.kind !== 'broken_link')
|
|
804
|
+
return [];
|
|
805
|
+
const action = item.status === 'applied'
|
|
806
|
+
? 'applied'
|
|
807
|
+
: ['rejected', 'blocked', 'stale', 'verification_failed'].includes(item.status)
|
|
808
|
+
? 'rejected'
|
|
809
|
+
: 'planned';
|
|
810
|
+
return item.evidence.flatMap((entry) => entry.type === 'link' && entry.brokenTarget && entry.newTarget && entry.signal
|
|
811
|
+
? [
|
|
812
|
+
{
|
|
813
|
+
from: entry.source,
|
|
814
|
+
brokenTarget: entry.brokenTarget,
|
|
815
|
+
newTarget: entry.newTarget,
|
|
816
|
+
signal: entry.signal,
|
|
817
|
+
action,
|
|
818
|
+
},
|
|
819
|
+
]
|
|
820
|
+
: []);
|
|
821
|
+
});
|
|
822
|
+
return { links, claims: [], declined };
|
|
823
|
+
}
|
|
824
|
+
function maintenancePlanForReport(plan) {
|
|
825
|
+
const { items, ...summary } = plan;
|
|
826
|
+
return {
|
|
827
|
+
...summary,
|
|
828
|
+
items: items.map((item) => ({
|
|
829
|
+
id: item.id,
|
|
830
|
+
kind: item.kind,
|
|
831
|
+
policy: item.policy,
|
|
832
|
+
risk: item.risk,
|
|
833
|
+
componentCount: item.componentCount,
|
|
834
|
+
subject: item.subject,
|
|
835
|
+
status: item.status,
|
|
836
|
+
statusCode: item.statusCode,
|
|
837
|
+
decision: item.decision,
|
|
838
|
+
statusReason: item.statusReason,
|
|
839
|
+
changeId: item.changeId,
|
|
840
|
+
verification: item.verification,
|
|
841
|
+
})),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function recordMaintenancePlan(report, plan) {
|
|
845
|
+
const summary = maintenancePlanForReport(plan);
|
|
846
|
+
const existing = report.maintenancePlans.findIndex((candidate) => candidate.id === plan.id);
|
|
847
|
+
if (existing === -1)
|
|
848
|
+
report.maintenancePlans.push(summary);
|
|
849
|
+
else
|
|
850
|
+
report.maintenancePlans[existing] = summary;
|
|
851
|
+
// Kept for older clients and single-phase callers. Multi-phase clients should use the array.
|
|
852
|
+
report.maintenancePlan = summary;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* A full policy-backed cycle reaches this barrier only after every writable planner has sealed
|
|
856
|
+
* its current phase plan. Curator calls and knowledge-base writes therefore cannot influence a
|
|
857
|
+
* later planner in the same invocation. Single-phase commands deliberately keep their immediate
|
|
858
|
+
* decide/apply behavior.
|
|
859
|
+
*/
|
|
860
|
+
async function decideDreamPlanWithCurator(ctx, planId, telemetry) {
|
|
861
|
+
const curatorContext = {
|
|
862
|
+
...ctx,
|
|
863
|
+
models: {
|
|
864
|
+
...ctx.models,
|
|
865
|
+
derive: ctx.models.derive.withOutcomeObserver((observation) => telemetry.observe('curator', observation)),
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
return decideMaintenancePlanWithCurator(curatorContext, planId);
|
|
869
|
+
}
|
|
870
|
+
async function decideAndApplyPlannedPhases(ctx, report, applied, budget, telemetry) {
|
|
871
|
+
const planIds = report.maintenancePlans.map((plan) => plan.id);
|
|
872
|
+
const pagePaths = new Map(ctx.store.db.prepare('SELECT slug, rel_path FROM pages').all().map((row) => [row.slug, row.rel_path]));
|
|
873
|
+
const schedule = maintenanceItemApplySchedule(planIds.map((planId) => getMaintenancePlan(ctx, planId)), pagePaths);
|
|
874
|
+
await deferStaleMaintenanceItems(ctx, planIds);
|
|
875
|
+
blockMaintenanceDependencies(ctx, planIds);
|
|
876
|
+
// Decide every sealed item before applying any of them. A target creator may move ahead of an
|
|
877
|
+
// earlier-phase referencer, but its bytes must never influence that referencer's curator prompt.
|
|
878
|
+
for (const planId of planIds) {
|
|
879
|
+
let plan = getMaintenancePlan(ctx, planId);
|
|
880
|
+
if (plan.mode === 'auto') {
|
|
881
|
+
if (plan.items.some((item) => item.status === 'proposed' && item.policy === 'auto')) {
|
|
882
|
+
plan = await decideDreamPlanWithCurator(ctx, plan.id, telemetry);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
for (const step of schedule) {
|
|
887
|
+
let plan = getMaintenancePlan(ctx, step.planId);
|
|
888
|
+
const item = plan.items.find((candidate) => candidate.id === step.itemId);
|
|
889
|
+
if (!item || !['approved', 'applying', 'verification_pending'].includes(item.status))
|
|
890
|
+
continue;
|
|
891
|
+
const prerequisiteMissing = step.dependsOn.some((dependency) => {
|
|
892
|
+
const prerequisite = getMaintenancePlan(ctx, dependency.planId).items.find((candidate) => candidate.id === dependency.itemId);
|
|
893
|
+
return prerequisite?.status !== 'applied';
|
|
894
|
+
});
|
|
895
|
+
if (prerequisiteMissing) {
|
|
896
|
+
deferUnmetMaintenanceDependency(ctx, plan.id, item.id);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const result = await applyMaintenancePlan(ctx, plan.id, budget, {
|
|
900
|
+
onlyItemIds: new Set([item.id]),
|
|
901
|
+
});
|
|
902
|
+
plan = result.plan;
|
|
903
|
+
applied.push(...result.files.map((file) => asApplied(plan.phase, file)));
|
|
904
|
+
}
|
|
905
|
+
for (const planId of planIds) {
|
|
906
|
+
const plan = getMaintenancePlan(ctx, planId);
|
|
907
|
+
refreshReportFromPlan(report, plan);
|
|
908
|
+
recordMaintenancePlan(report, plan);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* One bounded post-apply planning wave closes the common autonomous case where an earlier item
|
|
913
|
+
* safely changed a later item's sealed evidence. Every affected phase replans from the newly
|
|
914
|
+
* indexed state before any retry curator call, then all retry plans share one final barrier and
|
|
915
|
+
* the original run budget. A dependency created inside this wave waits for the next cycle; this
|
|
916
|
+
* function is deliberately not recursive.
|
|
917
|
+
*/
|
|
918
|
+
async function retryDependencyDeferredPhases(ctx, options, report, applied, budget, telemetry, setModelStage) {
|
|
919
|
+
const deferred = report.maintenancePlans.filter((plan) => plan.status === 'failed' && plan.items.some((item) => item.statusCode === 'dependency_conflict'));
|
|
920
|
+
if (deferred.length === 0)
|
|
921
|
+
return;
|
|
922
|
+
const retryPhases = new Set(deferred.map((plan) => plan.phase));
|
|
923
|
+
const retryReport = {
|
|
924
|
+
...report,
|
|
925
|
+
phases: [],
|
|
926
|
+
observations: [],
|
|
927
|
+
curated: [],
|
|
928
|
+
rejected: [],
|
|
929
|
+
adopted: [],
|
|
930
|
+
repaired: null,
|
|
931
|
+
repairChangeId: null,
|
|
932
|
+
housekeeping: null,
|
|
933
|
+
changeId: null,
|
|
934
|
+
adoptChangeId: null,
|
|
935
|
+
curateChangeId: null,
|
|
936
|
+
maintenancePlan: null,
|
|
937
|
+
maintenancePlans: [],
|
|
938
|
+
budget: maintenanceBudgetReceipt(budget),
|
|
939
|
+
warnings: [],
|
|
940
|
+
durationMs: 0,
|
|
941
|
+
};
|
|
942
|
+
for (const plan of deferred) {
|
|
943
|
+
recordMaintenancePlan(report, supersedeDependencyMaintenancePlan(ctx, plan.id));
|
|
944
|
+
}
|
|
945
|
+
for (const phase of DREAM_PHASES) {
|
|
946
|
+
if (!retryPhases.has(phase))
|
|
947
|
+
continue;
|
|
948
|
+
setModelStage(phase);
|
|
949
|
+
await runPhase(ctx, phase, options, retryReport, applied, budget, true, telemetry);
|
|
950
|
+
}
|
|
951
|
+
await decideAndApplyPlannedPhases(ctx, retryReport, applied, budget, telemetry);
|
|
952
|
+
mergeRetryReport(report, retryReport);
|
|
953
|
+
}
|
|
954
|
+
function mergeRetryReport(report, retry) {
|
|
955
|
+
report.observations = mergeByKey(report.observations, retry.observations, (entry) => `${entry.slug}\0${entry.pattern}`);
|
|
956
|
+
report.curated = mergeByKey(report.curated, retry.curated, (entry) => entry.slug);
|
|
957
|
+
report.rejected = mergeByKey(report.rejected, retry.rejected, (entry) => `${entry.pattern}\0${entry.reason}`);
|
|
958
|
+
report.adopted = mergeByKey(report.adopted, retry.adopted, (entry) => entry.slug);
|
|
959
|
+
report.repaired = mergeRepairResults(report.repaired, retry.repaired);
|
|
960
|
+
if (retry.changeId !== null)
|
|
961
|
+
report.changeId = retry.changeId;
|
|
962
|
+
if (retry.adoptChangeId !== null)
|
|
963
|
+
report.adoptChangeId = retry.adoptChangeId;
|
|
964
|
+
if (retry.curateChangeId !== null)
|
|
965
|
+
report.curateChangeId = retry.curateChangeId;
|
|
966
|
+
report.warnings.push(...retry.warnings);
|
|
967
|
+
for (const plan of retry.maintenancePlans) {
|
|
968
|
+
const existing = report.maintenancePlans.findIndex((candidate) => candidate.id === plan.id);
|
|
969
|
+
if (existing === -1)
|
|
970
|
+
report.maintenancePlans.push(plan);
|
|
971
|
+
else
|
|
972
|
+
report.maintenancePlans[existing] = plan;
|
|
973
|
+
}
|
|
974
|
+
if (retry.maintenancePlan)
|
|
975
|
+
report.maintenancePlan = retry.maintenancePlan;
|
|
976
|
+
}
|
|
977
|
+
function mergeRepairResults(current, retry) {
|
|
978
|
+
if (!current)
|
|
979
|
+
return retry;
|
|
980
|
+
if (!retry)
|
|
981
|
+
return current;
|
|
982
|
+
return {
|
|
983
|
+
links: mergeByKey(current.links, retry.links, (entry) => `${entry.from}\0${entry.brokenTarget}`),
|
|
984
|
+
claims: mergeByKey(current.claims, retry.claims, (entry) => `${entry.slug}\0${entry.line}\0${entry.before}`),
|
|
985
|
+
declined: mergeByKey(current.declined, retry.declined, (entry) => `${entry.what}\0${entry.reason}`),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function mergeByKey(current, retry, key) {
|
|
989
|
+
const merged = new Map(current.map((entry) => [key(entry), entry]));
|
|
990
|
+
for (const entry of retry)
|
|
991
|
+
merged.set(key(entry), entry);
|
|
992
|
+
return [...merged.values()];
|
|
993
|
+
}
|
|
994
|
+
function refreshReportFromPlan(report, plan) {
|
|
995
|
+
if (plan.phase === 'observe' || plan.phase === 'reflect') {
|
|
996
|
+
for (const item of plan.items.filter((candidate) => candidate.kind === 'observe' || candidate.kind === 'reflect')) {
|
|
997
|
+
const next = observationFromPlanItem(item);
|
|
998
|
+
const existing = report.observations.findIndex((entry) => entry.slug === next.slug && entry.pattern === next.pattern);
|
|
999
|
+
if (existing === -1)
|
|
1000
|
+
report.observations.push(next);
|
|
1001
|
+
else
|
|
1002
|
+
report.observations[existing] = next;
|
|
1003
|
+
}
|
|
1004
|
+
const changes = plan.items.map((item) => item.changeId).filter((id) => id !== null);
|
|
1005
|
+
report.changeId = changes.length === 1 ? changes[0] : null;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (plan.phase === 'curate') {
|
|
1009
|
+
report.curated = curationReportFromPlan(report.curated, plan);
|
|
1010
|
+
report.repaired = repairResultFromPlan(plan, report.repaired?.declined ?? []);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
report.adopted = adoptionReportFromPlan(report.adopted, plan);
|
|
1014
|
+
const changes = plan.items.map((item) => item.changeId).filter((id) => id !== null);
|
|
1015
|
+
report.adoptChangeId = changes.length === 1 ? changes[0] : null;
|
|
1016
|
+
}
|
|
1017
|
+
function curationReportFromPlan(pages, plan) {
|
|
1018
|
+
return pages.map((page) => {
|
|
1019
|
+
const item = plan.items.find((candidate) => isGeneralCurationItem(candidate) && maintenanceItemCoversSlug(candidate, page.slug));
|
|
1020
|
+
if (!item)
|
|
1021
|
+
return page;
|
|
1022
|
+
if (item.status === 'applied')
|
|
1023
|
+
return { ...page, action: 'updated', issues: [] };
|
|
1024
|
+
if (item.status === 'verification_pending') {
|
|
1025
|
+
return {
|
|
1026
|
+
...page,
|
|
1027
|
+
action: 'updated',
|
|
1028
|
+
issues: [item.verification?.detail ?? 'post-write verification is pending'],
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (['rejected', 'blocked', 'stale', 'verification_failed'].includes(item.status)) {
|
|
1032
|
+
return {
|
|
1033
|
+
...page,
|
|
1034
|
+
action: 'rejected',
|
|
1035
|
+
issues: [
|
|
1036
|
+
item.verification?.detail ??
|
|
1037
|
+
item.decision?.reason ??
|
|
1038
|
+
item.statusReason ??
|
|
1039
|
+
`maintenance item is ${item.status}`,
|
|
1040
|
+
],
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return page;
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function maintenanceItemCoversSlug(item, slug) {
|
|
1047
|
+
return (item.subject === slug ||
|
|
1048
|
+
item.evidence.some((entry) => entry.type === 'component' && entry.source === slug));
|
|
1049
|
+
}
|
|
1050
|
+
function adoptionReportFromPlan(entries, plan) {
|
|
1051
|
+
return entries.map((entry) => {
|
|
1052
|
+
if (entry.action !== 'planned')
|
|
1053
|
+
return entry;
|
|
1054
|
+
const item = plan.items.find((candidate) => candidate.subject === entry.slug);
|
|
1055
|
+
if (!item)
|
|
1056
|
+
return entry;
|
|
1057
|
+
if (item.status === 'applied')
|
|
1058
|
+
return { ...entry, action: 'created' };
|
|
1059
|
+
if (['rejected', 'blocked', 'stale', 'verification_failed'].includes(item.status)) {
|
|
1060
|
+
return {
|
|
1061
|
+
...entry,
|
|
1062
|
+
action: 'rejected',
|
|
1063
|
+
reason: item.verification?.detail ??
|
|
1064
|
+
item.decision?.reason ??
|
|
1065
|
+
item.statusReason ??
|
|
1066
|
+
`maintenance item is ${item.status}`,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return entry;
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* The second tier, and the phase that writes the most.
|
|
1074
|
+
*
|
|
1075
|
+
* The guards that need the knowledge base rather than the text are applied here, before the
|
|
1076
|
+
* model sees anything:
|
|
1077
|
+
*
|
|
1078
|
+
* - **Knowledge pages only.** A source page is somebody else's words. "Maria prefers X"
|
|
1079
|
+
* inferred from a marketing email she was cc'd on is exactly the failure this must not have.
|
|
1080
|
+
* - **Never self-feeding.** An observation is not admissible evidence for another observation.
|
|
1081
|
+
* No inference cascades.
|
|
1082
|
+
*/
|
|
1083
|
+
async function repairPhase(ctx, report) {
|
|
1084
|
+
if (!report.repaired) {
|
|
1085
|
+
const result = ctx.config.maintenance.repair.links
|
|
1086
|
+
? await planBrokenLinks(ctx, ctx.config.maintenance.repair.maxChanges)
|
|
1087
|
+
: { report: { links: [], claims: [], declined: [] } };
|
|
1088
|
+
report.repaired = result.report;
|
|
1089
|
+
}
|
|
1090
|
+
report.warnings.push('the legacy repair phase is report-only; use the curate phase in audit, review, or auto mode to persist guarded link fixes');
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
async function observePhase(ctx, options, report, applied, budget, deferAutomaticApply, telemetry) {
|
|
1094
|
+
if (options.dryRun) {
|
|
1095
|
+
await previewObservePhase(ctx, report);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const observePolicy = policyMode(effectiveTransformPolicy(ctx.config, 'observe', options.mode));
|
|
1099
|
+
if (!observePolicy)
|
|
1100
|
+
return;
|
|
1101
|
+
let plan = findActiveMaintenancePlan(ctx, observePolicy, 'observe');
|
|
1102
|
+
if (plan && !planMatchesPolicies(plan, { observe: observePolicy }))
|
|
1103
|
+
plan = null;
|
|
1104
|
+
const previouslyRejected = [];
|
|
1105
|
+
if (!plan) {
|
|
1106
|
+
const prepared = await collectPreparedObservations(ctx, report);
|
|
1107
|
+
const drafts = [];
|
|
1108
|
+
for (const entry of prepared) {
|
|
1109
|
+
if (!entry.draft)
|
|
1110
|
+
continue;
|
|
1111
|
+
if (inferenceWasRejected(ctx, 'observe', entry.draft)) {
|
|
1112
|
+
previouslyRejected.push({ ...entry.written, action: 'rejected' });
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
drafts.push(entry.draft);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
plan = createObservationPlan(ctx, observePolicy, drafts, observePolicy);
|
|
1119
|
+
}
|
|
1120
|
+
if (!plan) {
|
|
1121
|
+
report.observations.push(...previouslyRejected);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (observePolicy === 'auto' && !deferAutomaticApply) {
|
|
1125
|
+
if (plan.items.some((item) => item.status === 'proposed' && item.policy === 'auto')) {
|
|
1126
|
+
plan = await decideDreamPlanWithCurator(ctx, plan.id, telemetry);
|
|
1127
|
+
}
|
|
1128
|
+
if (plan.items.some((item) => ['approved', 'applying', 'verification_pending'].includes(item.status))) {
|
|
1129
|
+
const result = await applyMaintenancePlan(ctx, plan.id, budget);
|
|
1130
|
+
plan = result.plan;
|
|
1131
|
+
applied.push(...result.files.map((file) => asApplied('observe', file)));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
report.observations.push(...plan.items.map(observationFromPlanItem), ...previouslyRejected);
|
|
1135
|
+
const changeIds = plan.items
|
|
1136
|
+
.map((item) => item.changeId)
|
|
1137
|
+
.filter((changeId) => changeId !== null);
|
|
1138
|
+
// Compatibility for callers from before observations gained per-item journal changes.
|
|
1139
|
+
report.changeId = changeIds.length === 1 ? changeIds[0] : null;
|
|
1140
|
+
recordMaintenancePlan(report, plan);
|
|
1141
|
+
}
|
|
1142
|
+
async function previewObservePhase(ctx, report) {
|
|
1143
|
+
const prepared = await collectPreparedObservations(ctx, report);
|
|
1144
|
+
for (const entry of prepared) {
|
|
1145
|
+
report.observations.push(asPreviewObservation(entry.written));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function asPreviewObservation(entry) {
|
|
1149
|
+
if (entry.action === 'created')
|
|
1150
|
+
return { ...entry, action: 'would-create' };
|
|
1151
|
+
if (entry.action === 'refined')
|
|
1152
|
+
return { ...entry, action: 'would-refine' };
|
|
1153
|
+
return entry;
|
|
1154
|
+
}
|
|
1155
|
+
async function collectPreparedObservations(ctx, report) {
|
|
1156
|
+
const groups = subjectGroups(ctx, ctx.config.maintenance.observe.maxSubjects, report.conflicts);
|
|
1157
|
+
if (groups.length === 0)
|
|
1158
|
+
return [];
|
|
1159
|
+
const prepared = [];
|
|
1160
|
+
// Gathered once for the whole phase, not per group: fifteen groups reading the same two things
|
|
1161
|
+
// fifteen times is the same answer at fifteen times the cost.
|
|
1162
|
+
const knownFacts = liveFactClaims(ctx, report.conflicts);
|
|
1163
|
+
const observationsBySlug = await allObservations(ctx);
|
|
1164
|
+
for (const group of groups) {
|
|
1165
|
+
const result = await runObserveMission({
|
|
1166
|
+
// The folder travels with the subject so the model knows what kind of thing it is
|
|
1167
|
+
// looking at: "price, in shopping" is a question; "price" on its own is a word.
|
|
1168
|
+
subject: group.folder === '.' ? group.subject : `${group.subject}, in ${group.folder}`,
|
|
1169
|
+
facts: group.facts,
|
|
1170
|
+
model: ctx.models.derive,
|
|
1171
|
+
mission: ctx.config.maintenance.observe.mission,
|
|
1172
|
+
minEvidence: ctx.config.maintenance.observe.minEvidence,
|
|
1173
|
+
// What last night already concluded about this subject. The facts rarely change between one
|
|
1174
|
+
// night and the next, so without this the same insight comes back reworded every night and
|
|
1175
|
+
// the page accumulates paraphrases of one sentence.
|
|
1176
|
+
existing: observationsBySlug.get(observationSlug(ctx, group.slug)) ?? [],
|
|
1177
|
+
// Everything already observed elsewhere, plus anything written earlier in this same run —
|
|
1178
|
+
// two groups can reach one conclusion from overlapping facts, and each would otherwise write
|
|
1179
|
+
// it to its own page where neither looks like a duplicate.
|
|
1180
|
+
otherObservations: [...observationsBySlug].flatMap(([slug, lines]) => slug === observationSlug(ctx, group.slug) ? [] : lines),
|
|
1181
|
+
knownFacts,
|
|
1182
|
+
});
|
|
1183
|
+
if (result.error) {
|
|
1184
|
+
report.warnings.push(`observe (${group.subject}): ${result.error}`);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
report.rejected.push(...result.rejected);
|
|
1188
|
+
for (const observation of result.observations) {
|
|
1189
|
+
const outcome = await prepareObservation(ctx, { title: group.subject, slug: group.slug }, observation);
|
|
1190
|
+
prepared.push(outcome);
|
|
1191
|
+
if (outcome.rejectionReason) {
|
|
1192
|
+
report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
|
|
1193
|
+
}
|
|
1194
|
+
// So a later group in this same run sees it. Without this, cross-page duplicates are caught
|
|
1195
|
+
// only from the second night onwards — the night they are created, they both go through.
|
|
1196
|
+
const slug = observationSlug(ctx, group.slug);
|
|
1197
|
+
observationsBySlug.set(slug, [...(observationsBySlug.get(slug) ?? []), observation.pattern]);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return prepared;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Live facts from knowledge, non-observation pages, grouped by **folder and subject**.
|
|
1204
|
+
*
|
|
1205
|
+
* Not by subject alone, which is what the first version did and what a real knowledge base
|
|
1206
|
+
* immediately exposed. A small deriver writes the *attribute* into `subject` — "price",
|
|
1207
|
+
* "address", "location", "opening hours" — so grouping on it joined a bag with a drum kit
|
|
1208
|
+
* ("Price is $79 during sales"), and a Roman church with a person's page ("Address is central
|
|
1209
|
+
* and near a major landmark"). Given unrelated facts under one heading, the model does what it
|
|
1210
|
+
* is asked and finds a pattern across them; the input was the bug.
|
|
1211
|
+
*
|
|
1212
|
+
* A folder is the coarsest thing in a knowledge base that means "these are about the same kind
|
|
1213
|
+
* of thing", and it is the user's own division rather than one Akno invented. Two pages in
|
|
1214
|
+
* `shopping/` sharing a subject are at least comparable. Ordered by how recently the pages were
|
|
1215
|
+
* touched, so a capped run looks at what is moving.
|
|
1216
|
+
*/
|
|
1217
|
+
function subjectGroups(ctx, maxSubjects, conflicts) {
|
|
1218
|
+
const observations = ctx.config.paths.observations;
|
|
1219
|
+
const ineligible = ineligibleConflictClaims(conflicts);
|
|
1220
|
+
const rows = ctx.store.db
|
|
1221
|
+
.prepare(`SELECT f.subject, f.claim, f.line_start, p.slug, p.updated_at
|
|
1222
|
+
FROM facts f JOIN pages p ON p.id = f.page_id
|
|
1223
|
+
WHERE f.valid_to IS NULL
|
|
1224
|
+
AND f.subject IS NOT NULL
|
|
1225
|
+
AND f.confidence >= 0.5
|
|
1226
|
+
-- Only canonical claims. A source page is evidence someone else wrote.
|
|
1227
|
+
AND p.role = 'knowledge'
|
|
1228
|
+
AND p.derived_hash = p.body_hash
|
|
1229
|
+
-- Never self-feeding. An observation is not evidence for another observation.
|
|
1230
|
+
AND p.slug != ?
|
|
1231
|
+
AND p.slug NOT LIKE ?
|
|
1232
|
+
ORDER BY p.updated_at DESC`)
|
|
1233
|
+
.all(observations, `${observations}/%`);
|
|
1234
|
+
const groups = new Map();
|
|
1235
|
+
for (const row of rows) {
|
|
1236
|
+
if (ineligible.has(claimKey(row.slug, row.line_start)))
|
|
1237
|
+
continue;
|
|
1238
|
+
const subject = row.subject.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
1239
|
+
if (subject.length === 0)
|
|
1240
|
+
continue;
|
|
1241
|
+
const folder = row.slug.includes('/') ? row.slug.slice(0, row.slug.indexOf('/')) : '.';
|
|
1242
|
+
const key = `${folder}|${subject}`;
|
|
1243
|
+
const existing = groups.get(key);
|
|
1244
|
+
if (existing) {
|
|
1245
|
+
if (existing.facts.length < 30)
|
|
1246
|
+
existing.facts.push({ claim: row.claim, slug: row.slug });
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
groups.set(key, {
|
|
1250
|
+
subject: row.subject,
|
|
1251
|
+
folder,
|
|
1252
|
+
slug: folder === '.' ? slugify(subject) : `${slugify(folder)}-${slugify(subject)}`,
|
|
1253
|
+
facts: [{ claim: row.claim, slug: row.slug }],
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return [...groups.values()]
|
|
1258
|
+
.filter((group) => new Set(group.facts.map((fact) => fact.slug)).size >= 2)
|
|
1259
|
+
.slice(0, maxSubjects);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* **Refine, never overwrite** — a changed pattern gets a new dated line. **Add and
|
|
1263
|
+
* refine, never delete** — a curator that can delete loses things nobody watched it delete.
|
|
1264
|
+
*
|
|
1265
|
+
* So the page is only ever appended to, and an observation already on it is left exactly as it
|
|
1266
|
+
* is. That is also what makes the phase safe to re-run: a second pass over unchanged facts
|
|
1267
|
+
* reports `unchanged` and writes nothing.
|
|
1268
|
+
*/
|
|
1269
|
+
/** Where a subject's observations live. */
|
|
1270
|
+
function observationSlug(ctx, pageSlug) {
|
|
1271
|
+
return normalizeSlug(`${ctx.config.paths.observations}/${pageSlug}`);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Every observation already written, by page.
|
|
1275
|
+
*
|
|
1276
|
+
* Read from the files rather than the index because the index is a reading of the files and this
|
|
1277
|
+
* runs inside the same cycle that writes them.
|
|
1278
|
+
*/
|
|
1279
|
+
async function allObservations(ctx) {
|
|
1280
|
+
const root = path.join(ctx.config.aknoPath, ctx.config.paths.observations);
|
|
1281
|
+
const bySlug = new Map();
|
|
1282
|
+
const entries = await fsp.readdir(root, { withFileTypes: true, recursive: true }).catch(() => []);
|
|
1283
|
+
for (const entry of entries) {
|
|
1284
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
1285
|
+
continue;
|
|
1286
|
+
const abs = path.join(entry.parentPath ?? root, entry.name);
|
|
1287
|
+
// Skipped for the same reason `scanTree` and the watcher skip them: a dot path is
|
|
1288
|
+
// sync-client or editor state, not an observation. Checked on the whole relative
|
|
1289
|
+
// path rather than `entry.name`, because a recursive readdir reports a file inside
|
|
1290
|
+
// `.trash/` with a perfectly ordinary basename. It also has to happen before
|
|
1291
|
+
// `normalizeSlug` below, which now refuses a dot segment — an unfiltered walk would
|
|
1292
|
+
// turn one stray `.backup.md` into a throw inside the nightly cycle.
|
|
1293
|
+
if (path
|
|
1294
|
+
.relative(root, abs)
|
|
1295
|
+
.split(path.sep)
|
|
1296
|
+
.some((segment) => segment.startsWith('.'))) {
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const body = await fsp.readFile(abs, 'utf8').catch(() => null);
|
|
1300
|
+
if (body === null)
|
|
1301
|
+
continue;
|
|
1302
|
+
const lines = body
|
|
1303
|
+
.split('\n')
|
|
1304
|
+
.filter((line) => /^- \d{4}-\d{2}-\d{2} — /.test(line))
|
|
1305
|
+
.map((line) => line
|
|
1306
|
+
.replace(/^- \d{4}-\d{2}-\d{2} — /, '')
|
|
1307
|
+
.replace(/\s*\[\[[^\]]*\]\]/g, '')
|
|
1308
|
+
.trim())
|
|
1309
|
+
.filter(Boolean);
|
|
1310
|
+
if (lines.length === 0)
|
|
1311
|
+
continue;
|
|
1312
|
+
const relative = path.relative(ctx.config.aknoPath, abs).replace(/\.md$/, '');
|
|
1313
|
+
bySlug.set(normalizeSlug(relative), lines);
|
|
1314
|
+
}
|
|
1315
|
+
return bySlug;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Every live claim in the knowledge base, so an "observation" cannot be one of them handed back.
|
|
1319
|
+
*
|
|
1320
|
+
* Superseded claims are left out: their sentence has already been replaced, and an observation is
|
|
1321
|
+
* not a repeat of something the knowledge base no longer says.
|
|
1322
|
+
*/
|
|
1323
|
+
function liveFactClaims(ctx, conflicts = []) {
|
|
1324
|
+
const ineligible = ineligibleConflictClaims(conflicts);
|
|
1325
|
+
const rows = ctx.store.db
|
|
1326
|
+
.prepare(`SELECT f.claim, f.line_start, p.slug
|
|
1327
|
+
FROM facts f JOIN pages p ON p.id = f.page_id
|
|
1328
|
+
WHERE f.valid_to IS NULL AND p.derived_hash = p.body_hash`)
|
|
1329
|
+
.all();
|
|
1330
|
+
return rows.filter((row) => !ineligible.has(claimKey(row.slug, row.line_start))).map((row) => row.claim);
|
|
1331
|
+
}
|
|
1332
|
+
async function prepareObservation(ctx, page, observation, evidenceRole = 'knowledge') {
|
|
1333
|
+
const slug = normalizeSlug(`${ctx.config.paths.observations}/${page.slug}`);
|
|
1334
|
+
const relPath = `${slug}.md`;
|
|
1335
|
+
const absPath = path.join(ctx.config.aknoPath, relPath);
|
|
1336
|
+
const existing = await fsp.readFile(absPath, 'utf8').catch(() => null);
|
|
1337
|
+
// A page is never evidence for itself, whichever caller got here. `reflect` reads the folder it
|
|
1338
|
+
// writes into, so it found `principles` among its own sources and cited it — and a claim offered
|
|
1339
|
+
// as its own support reads, later, as a claim with support.
|
|
1340
|
+
const evidence = observation.evidence.filter((cited) => normalizeSlug(cited) !== slug);
|
|
1341
|
+
if (evidence.length === 0) {
|
|
1342
|
+
return {
|
|
1343
|
+
written: { slug, pattern: observation.pattern, evidence: [], action: 'unchanged' },
|
|
1344
|
+
draft: null,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
observation = { ...observation, evidence };
|
|
1348
|
+
if (existing !== null && existing.includes(observation.pattern)) {
|
|
1349
|
+
return {
|
|
1350
|
+
written: { slug, pattern: observation.pattern, evidence: observation.evidence, action: 'unchanged' },
|
|
1351
|
+
draft: null,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1355
|
+
// Deliberately not `- **YYYY-MM-DD** |`, which is read as a timeline event anywhere it
|
|
1356
|
+
// appears. An inferred pattern is not something that happened on a date.
|
|
1357
|
+
const line = `- ${today} — ${observation.pattern} ${citation(observation.evidence)}`;
|
|
1358
|
+
const next = existing === null
|
|
1359
|
+
? newObservationPage(page.title, observation, today)
|
|
1360
|
+
: appendObservation(existing, line, observation.evidence);
|
|
1361
|
+
const written = {
|
|
1362
|
+
slug,
|
|
1363
|
+
pattern: observation.pattern,
|
|
1364
|
+
evidence: observation.evidence,
|
|
1365
|
+
action: existing === null ? 'created' : 'refined',
|
|
1366
|
+
};
|
|
1367
|
+
const sealedEvidence = await observationEvidence(ctx, observation.evidence, evidenceRole);
|
|
1368
|
+
if (sealedEvidence.length !== observation.evidence.length) {
|
|
1369
|
+
return {
|
|
1370
|
+
written: { ...written, action: 'rejected' },
|
|
1371
|
+
draft: null,
|
|
1372
|
+
rejectionReason: 'evidence changed or stopped being live before the observation was sealed',
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const inputHash = sha256(JSON.stringify({
|
|
1376
|
+
slug,
|
|
1377
|
+
beforeHash: existing === null ? null : sha256(existing),
|
|
1378
|
+
pattern: observation.pattern,
|
|
1379
|
+
evidence: sealedEvidence,
|
|
1380
|
+
}));
|
|
1381
|
+
return {
|
|
1382
|
+
written,
|
|
1383
|
+
draft: { slug, relPath, inputHash, before: existing, after: next, evidence: sealedEvidence },
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
async function observationEvidence(ctx, slugs, role) {
|
|
1387
|
+
const out = [];
|
|
1388
|
+
for (const slug of slugs) {
|
|
1389
|
+
const row = ctx.store.db
|
|
1390
|
+
.prepare('SELECT rel_path FROM pages WHERE slug = ? AND role = ?')
|
|
1391
|
+
.get(slug, role);
|
|
1392
|
+
if (!row)
|
|
1393
|
+
continue;
|
|
1394
|
+
const content = await fsp
|
|
1395
|
+
.readFile(path.join(ctx.config.aknoPath, row.rel_path), 'utf8')
|
|
1396
|
+
.catch(() => null);
|
|
1397
|
+
if (content === null)
|
|
1398
|
+
continue;
|
|
1399
|
+
out.push({ slug, contentHash: sha256(content) });
|
|
1400
|
+
}
|
|
1401
|
+
return out;
|
|
1402
|
+
}
|
|
1403
|
+
function inferenceWasRejected(ctx, kind, draft) {
|
|
1404
|
+
const row = ctx.store.db
|
|
1405
|
+
.prepare(`SELECT 1 FROM maintenance_items
|
|
1406
|
+
WHERE kind = ? AND status = 'rejected' AND subject = ? AND input_hash = ?
|
|
1407
|
+
LIMIT 1`)
|
|
1408
|
+
.get(kind, draft.slug, draft.inputHash);
|
|
1409
|
+
return row !== undefined;
|
|
1410
|
+
}
|
|
1411
|
+
function observationFromPlanItem(item) {
|
|
1412
|
+
const operation = item.operations[0];
|
|
1413
|
+
if (operation.type === 'move')
|
|
1414
|
+
throw new Error('an inference item cannot move a document');
|
|
1415
|
+
const after = operation.type === 'delete' ? operation.before : operation.after;
|
|
1416
|
+
const lastLine = after.trimEnd().split('\n').at(-1) ?? '';
|
|
1417
|
+
const pattern = /^- \d{4}-\d{2}-\d{2} — (.+?)(?:\s+\[\[[^\]]+\]\])+$/.exec(lastLine)?.[1] ?? '';
|
|
1418
|
+
const proposed = operation.type === 'create' ? 'would-create' : 'would-refine';
|
|
1419
|
+
const action = item.status === 'applied'
|
|
1420
|
+
? operation.type === 'create'
|
|
1421
|
+
? 'created'
|
|
1422
|
+
: 'refined'
|
|
1423
|
+
: ['rejected', 'blocked', 'stale', 'verification_failed'].includes(item.status)
|
|
1424
|
+
? 'rejected'
|
|
1425
|
+
: proposed;
|
|
1426
|
+
return {
|
|
1427
|
+
slug: item.subject,
|
|
1428
|
+
pattern,
|
|
1429
|
+
evidence: item.evidence.filter((entry) => entry.type === 'page').map((entry) => entry.source),
|
|
1430
|
+
action,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* `derived` and `evidence` are the two keys Akno writes on `observations/` pages it
|
|
1435
|
+
* authors. They are what makes an inference identifiable as one after the fact — by a reader,
|
|
1436
|
+
* by recall's ranking, and by the guard that refuses to feed observations back in.
|
|
1437
|
+
*/
|
|
1438
|
+
function newObservationPage(subject, observation, today) {
|
|
1439
|
+
const title = subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
1440
|
+
return (`---\ntitle: ${title}\nderived: true\nevidence:\n${observation.evidence
|
|
1441
|
+
.map((slug) => ` - ${slug}`)
|
|
1442
|
+
.join('\n')}\n---\n\n` +
|
|
1443
|
+
`# ${title}\n\n` +
|
|
1444
|
+
`Patterns Akno inferred from pages listed as evidence. Not authored claims.\n\n` +
|
|
1445
|
+
`- ${today} — ${observation.pattern} ${citation(observation.evidence)}\n`);
|
|
1446
|
+
}
|
|
1447
|
+
/** Appends the new line and unions the evidence, leaving every existing line alone. */
|
|
1448
|
+
function appendObservation(current, line, evidence) {
|
|
1449
|
+
const merged = mergeEvidence(current, evidence);
|
|
1450
|
+
return `${merged.replace(/\s+$/, '')}\n${line}\n`;
|
|
1451
|
+
}
|
|
1452
|
+
function mergeEvidence(current, evidence) {
|
|
1453
|
+
const match = /^---\n([\s\S]*?)\n---\n/.exec(current);
|
|
1454
|
+
if (!match)
|
|
1455
|
+
return current;
|
|
1456
|
+
const front = match[1];
|
|
1457
|
+
const listed = new Set([...front.matchAll(/^\s*-\s*(\S+)\s*$/gm)].map((entry) => entry[1]));
|
|
1458
|
+
const missing = evidence.filter((slug) => !listed.has(slug));
|
|
1459
|
+
if (missing.length === 0)
|
|
1460
|
+
return current;
|
|
1461
|
+
// Appended under the existing `evidence:` key, or added as one. Every other frontmatter key
|
|
1462
|
+
// is left byte for byte: that promise holds for pages Akno authors too.
|
|
1463
|
+
const added = missing.map((slug) => ` - ${slug}`).join('\n');
|
|
1464
|
+
const nextFront = /^evidence:/m.test(front)
|
|
1465
|
+
? front.replace(/^(evidence:(?:\n\s*-\s*\S+)*)/m, `$1\n${added}`)
|
|
1466
|
+
: `${front}\nevidence:\n${added}`;
|
|
1467
|
+
return current.replace(match[0], `---\n${nextFront}\n---\n`);
|
|
1468
|
+
}
|
|
1469
|
+
function citation(evidence) {
|
|
1470
|
+
return evidence.map((slug) => `[[${slug}]]`).join(' ');
|
|
1471
|
+
}
|
|
1472
|
+
function slugify(subject) {
|
|
1473
|
+
return (subject
|
|
1474
|
+
.toLowerCase()
|
|
1475
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1476
|
+
.replace(/^-+|-+$/g, '')
|
|
1477
|
+
.slice(0, 60) || 'pattern');
|
|
1478
|
+
}
|
|
1479
|
+
// ─── Reflect ────────────────────────────────────────────────────────────────
|
|
1480
|
+
/**
|
|
1481
|
+
* The third tier — **off by default**, and shipped as an extension point rather than a
|
|
1482
|
+
* finished tier because at a few hundred pages a "pattern" is one coincidence away from noise.
|
|
1483
|
+
*
|
|
1484
|
+
* The plumbing is real: enabling it runs the observe writer over the tier above, so principles
|
|
1485
|
+
* are built from observations and the pages behind them. What it deliberately does not do is
|
|
1486
|
+
* invent a second set of guardrails — the same evidence floor, the same refusal to hedge, the
|
|
1487
|
+
* same append-only writing.
|
|
1488
|
+
*/
|
|
1489
|
+
/** The one page `reflect` writes, and the one page it must not read. */
|
|
1490
|
+
const PRINCIPLES_SLUG = 'principles';
|
|
1491
|
+
async function reflectPhase(ctx, options, report, applied, budget, deferAutomaticApply, telemetry) {
|
|
1492
|
+
if (options.dryRun) {
|
|
1493
|
+
await previewReflectPhase(ctx, report);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const reflectPolicy = policyMode(effectiveTransformPolicy(ctx.config, 'reflect', options.mode));
|
|
1497
|
+
if (!reflectPolicy)
|
|
1498
|
+
return;
|
|
1499
|
+
let plan = findActiveMaintenancePlan(ctx, reflectPolicy, 'reflect');
|
|
1500
|
+
if (plan && !planMatchesPolicies(plan, { reflect: reflectPolicy }))
|
|
1501
|
+
plan = null;
|
|
1502
|
+
const previouslyRejected = [];
|
|
1503
|
+
if (!plan) {
|
|
1504
|
+
const prepared = await collectPreparedReflections(ctx, report);
|
|
1505
|
+
const drafts = [];
|
|
1506
|
+
for (const entry of prepared) {
|
|
1507
|
+
if (!entry.draft)
|
|
1508
|
+
continue;
|
|
1509
|
+
if (inferenceWasRejected(ctx, 'reflect', entry.draft)) {
|
|
1510
|
+
previouslyRejected.push({ ...entry.written, action: 'rejected' });
|
|
1511
|
+
}
|
|
1512
|
+
else {
|
|
1513
|
+
drafts.push(entry.draft);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
plan = createReflectionPlan(ctx, reflectPolicy, drafts, reflectPolicy);
|
|
1517
|
+
}
|
|
1518
|
+
if (!plan) {
|
|
1519
|
+
report.observations.push(...previouslyRejected);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (reflectPolicy === 'auto' && !deferAutomaticApply) {
|
|
1523
|
+
if (plan.items.some((item) => item.status === 'proposed' && item.policy === 'auto')) {
|
|
1524
|
+
plan = await decideDreamPlanWithCurator(ctx, plan.id, telemetry);
|
|
1525
|
+
}
|
|
1526
|
+
if (plan.items.some((item) => ['approved', 'applying', 'verification_pending'].includes(item.status))) {
|
|
1527
|
+
const result = await applyMaintenancePlan(ctx, plan.id, budget);
|
|
1528
|
+
plan = result.plan;
|
|
1529
|
+
applied.push(...result.files.map((file) => asApplied('reflect', file)));
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
report.observations.push(...plan.items.map(observationFromPlanItem), ...previouslyRejected);
|
|
1533
|
+
const changeIds = plan.items
|
|
1534
|
+
.map((item) => item.changeId)
|
|
1535
|
+
.filter((changeId) => changeId !== null);
|
|
1536
|
+
report.changeId = changeIds.length === 1 ? changeIds[0] : null;
|
|
1537
|
+
recordMaintenancePlan(report, plan);
|
|
1538
|
+
}
|
|
1539
|
+
async function previewReflectPhase(ctx, report) {
|
|
1540
|
+
const prepared = await collectPreparedReflections(ctx, report);
|
|
1541
|
+
for (const outcome of prepared) {
|
|
1542
|
+
report.observations.push(asPreviewObservation(outcome.written));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async function collectPreparedReflections(ctx, report) {
|
|
1546
|
+
// The page this phase writes to is under `observations/` like everything it reads, so without
|
|
1547
|
+
// excluding it the tier feeds on its own output: from the second night onwards `principles` has a
|
|
1548
|
+
// summary, is selected as a source, and is cited as evidence for the principle it already
|
|
1549
|
+
// contains. A conclusion that is its own evidence is not a conclusion.
|
|
1550
|
+
const target = observationSlug(ctx, PRINCIPLES_SLUG);
|
|
1551
|
+
const observations = await allObservations(ctx);
|
|
1552
|
+
const ineligibleSources = ineligibleConflictSourceSlugs(report.conflicts);
|
|
1553
|
+
const rows = ctx.store.db
|
|
1554
|
+
.prepare(`SELECT slug, summary, frontmatter FROM pages
|
|
1555
|
+
WHERE (slug = ? OR slug LIKE ?) AND slug != ? AND summary IS NOT NULL
|
|
1556
|
+
ORDER BY updated_at DESC LIMIT 40`)
|
|
1557
|
+
.all(ctx.config.paths.observations, `${ctx.config.paths.observations}/%`, target).filter((row) => !frontmatterEvidence(row.frontmatter).some((slug) => ineligibleSources.has(slug)));
|
|
1558
|
+
if (rows.length < 2) {
|
|
1559
|
+
report.warnings.push('reflect had fewer than two observations to build on — nothing was written');
|
|
1560
|
+
return [];
|
|
1561
|
+
}
|
|
1562
|
+
const result = await runObserveMission({
|
|
1563
|
+
subject: 'decision principles',
|
|
1564
|
+
facts: rows.map((row) => ({ claim: row.summary, slug: row.slug })),
|
|
1565
|
+
model: ctx.models.derive,
|
|
1566
|
+
mission: ctx.config.maintenance.reflect.mission ??
|
|
1567
|
+
'State durable decision principles and long-term tendencies, not individual patterns.',
|
|
1568
|
+
// A tier further from the evidence needs more of it.
|
|
1569
|
+
minEvidence: Math.max(3, ctx.config.maintenance.observe.minEvidence),
|
|
1570
|
+
// This tier appends to one page every night from observations that rarely change, so without
|
|
1571
|
+
// its own previous answers it restates them — the same way `observe` did, one tier up.
|
|
1572
|
+
existing: observations.get(target) ?? [],
|
|
1573
|
+
// A principle that is one of the observation lines verbatim is not a tier above them. Its
|
|
1574
|
+
// sources here are page *summaries*, so the lines themselves are not otherwise checked.
|
|
1575
|
+
otherObservations: [...observations].flatMap(([slug, lines]) => (slug === target ? [] : lines)),
|
|
1576
|
+
// Nor a raw claim promoted to a principle. `facts` for this phase is the observations, so
|
|
1577
|
+
// without this the knowledge base's own facts are the one thing it is not compared against.
|
|
1578
|
+
knownFacts: liveFactClaims(ctx, report.conflicts),
|
|
1579
|
+
});
|
|
1580
|
+
if (result.error) {
|
|
1581
|
+
report.warnings.push(`reflect: ${result.error}`);
|
|
1582
|
+
return [];
|
|
1583
|
+
}
|
|
1584
|
+
report.rejected.push(...result.rejected);
|
|
1585
|
+
const prepared = [];
|
|
1586
|
+
for (const observation of result.observations) {
|
|
1587
|
+
const outcome = await prepareObservation(ctx, { title: 'Principles', slug: PRINCIPLES_SLUG }, observation, 'inference');
|
|
1588
|
+
prepared.push(outcome);
|
|
1589
|
+
if (outcome.rejectionReason) {
|
|
1590
|
+
report.rejected.push({ pattern: observation.pattern, reason: outcome.rejectionReason });
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return prepared;
|
|
1594
|
+
}
|
|
1595
|
+
function ineligibleConflictSourceSlugs(conflicts) {
|
|
1596
|
+
const keys = ineligibleConflictClaims(conflicts);
|
|
1597
|
+
const slugs = new Set();
|
|
1598
|
+
for (const conflict of conflicts) {
|
|
1599
|
+
for (const claim of conflict.claims) {
|
|
1600
|
+
if (keys.has(claimKey(claim.slug, claim.line)))
|
|
1601
|
+
slugs.add(claim.slug);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return slugs;
|
|
1605
|
+
}
|
|
1606
|
+
function frontmatterEvidence(frontmatter) {
|
|
1607
|
+
try {
|
|
1608
|
+
const value = JSON.parse(frontmatter);
|
|
1609
|
+
return Array.isArray(value.evidence)
|
|
1610
|
+
? value.evidence.filter((entry) => typeof entry === 'string')
|
|
1611
|
+
: [];
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
return [];
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/** A journal entry, as the log wants it: which phase, and what the write added. */
|
|
1618
|
+
function asApplied(phase, file) {
|
|
1619
|
+
return {
|
|
1620
|
+
phase,
|
|
1621
|
+
relPath: file.relPath,
|
|
1622
|
+
action: file.action,
|
|
1623
|
+
added: file.after === null ? [] : addedLines(file.before, file.after),
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
/** Guard for the CLI: a phase name that is not one. */
|
|
1627
|
+
export function parsePhase(value) {
|
|
1628
|
+
if (DREAM_PHASES.includes(value))
|
|
1629
|
+
return value;
|
|
1630
|
+
throw new AknoError('invalid', `unknown phase '${value}' — expected one of ${DREAM_PHASES.join(', ')}`);
|
|
1631
|
+
}
|
|
1632
|
+
//# sourceMappingURL=dream.js.map
|