@modusensus/dsh-mneme 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/bin/dsh-mneme-choro.mjs +254 -0
- package/docs/CONFIGURATION.md +8 -1
- package/lib/api-standalone.js +17 -21
- package/lib/api.js +247 -11
- package/lib/bootstrap-http.js +105 -0
- package/lib/bootstrap.js +5 -1
- package/lib/choro-command.js +104 -0
- package/lib/client.js +1028 -102
- package/lib/config-keys.js +144 -0
- package/lib/config.js +28 -0
- package/lib/index.js +50 -4
- package/lib/inject.js +15 -3
- package/lib/lang.js +54 -0
- package/lib/quality-filter.js +48 -12
- package/lib/service.js +3 -0
- package/lib/settings.js +8 -1
- package/lib/store.js +502 -0
- package/lib/tools.js +127 -1
- package/lib/wiki/inject.js +164 -0
- package/lib/wiki/partition.js +200 -0
- package/lib/wiki/plan.js +214 -0
- package/lib/wiki/queries.js +220 -0
- package/lib/wiki/render.js +187 -0
- package/lib/wiki/scan.js +371 -0
- package/lib/wiki/t0.js +463 -0
- package/lib/wiki/t1.js +496 -0
- package/lib/write-admission.js +5 -3
- package/package.json +4 -2
- package/src/api-standalone.js +17 -21
- package/src/api.js +247 -11
- package/src/bootstrap-http.js +105 -0
- package/src/bootstrap.js +5 -1
- package/src/choro-command.js +104 -0
- package/src/config-keys.js +144 -0
- package/src/config.js +28 -0
- package/src/index.js +50 -4
- package/src/inject.js +15 -3
- package/src/lang.js +54 -0
- package/src/quality-filter.js +48 -12
- package/src/service.js +3 -0
- package/src/settings.js +8 -1
- package/src/store.js +502 -0
- package/src/tools.js +127 -1
- package/src/wiki/inject.js +164 -0
- package/src/wiki/partition.js +200 -0
- package/src/wiki/plan.js +214 -0
- package/src/wiki/queries.js +220 -0
- package/src/wiki/render.js +187 -0
- package/src/wiki/scan.js +371 -0
- package/src/wiki/t0.js +463 -0
- package/src/wiki/t1.js +496 -0
- package/src/write-admission.js +5 -3
- package/test/api-routes-gaps.test.js +131 -1
- package/test/api.test.js +7 -1
- package/test/bootstrap.test.js +8 -0
- package/test/choro-command.test.js +218 -0
- package/test/client.test.js +655 -19
- package/test/config-keys-routes.test.js +233 -0
- package/test/contrast.test.js +14 -7
- package/test/inject.test.js +51 -0
- package/test/llm-audit.test.js +29 -1
- package/test/quality-backfill.test.js +290 -0
- package/test/quality-filter.test.js +113 -4
- package/test/service.test.js +1 -1
- package/test/wiki-api.test.js +153 -0
- package/test/wiki-inject.test.js +265 -0
- package/test/wiki-partition.test.js +112 -0
- package/test/wiki-plan.test.js +80 -0
- package/test/wiki-queries.test.js +187 -0
- package/test/wiki-scan.test.js +208 -0
- package/test/wiki-store.test.js +190 -0
- package/test/wiki-t0.test.js +380 -0
- package/test/wiki-t1.test.js +235 -0
- package/test/wiki-tools.test.js +121 -0
package/lib/store.js
CHANGED
|
@@ -2,6 +2,10 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { statSync } from "node:fs";
|
|
4
4
|
import { contentHashOf } from "./content-hash.js";
|
|
5
|
+
// 质量分回填要的判据与扣分表。只取常量与纯函数:依赖方向 store → quality-filter 单向
|
|
6
|
+
// (quality-filter.js 不 import 任何本包模块,是纯函数模块),不构成环,也不会让存储层
|
|
7
|
+
// 反向依赖服务层。判据只许有一份——回填另写一个 0.7 就会与写入路径悄悄分叉。
|
|
8
|
+
import { REPETITION_THRESHOLD, SIGNAL_DEDUCTIONS, repetitionRatio } from "./quality-filter.js";
|
|
5
9
|
// 政策轴(issue #471)的归一化函数。刻意只取归一化,不取判定:判定(buildPausedIndex /
|
|
6
10
|
// matchPausedScopes / createScopeStateGate)留在 scope-state.js,由 service 侧消费,
|
|
7
11
|
// 存储层只管「一格状态怎么落」。依赖方向 store → scope-state → scope 是单向的
|
|
@@ -49,6 +53,20 @@ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
|
49
53
|
-- (5k 行实测 all() 231ms → 135ms)。IF NOT EXISTS 幂等,存量库启动即建。
|
|
50
54
|
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at);
|
|
51
55
|
|
|
56
|
+
-- 一次性数据迁移的登记表:一条迁移一行,跑过就记下。看起来多余(本文件其余迁移都
|
|
57
|
+
-- 靠「条件式 SQL 恒等幂等」),但存量回填那类迁移的条件在跑完之后**恒不命中、却仍要
|
|
58
|
+
-- 全表扫一遍才能证明不命中**——质量分回填的探测在本机 12,827 行上是 36.7 ms,而
|
|
59
|
+
-- createStore 稳态才 3.3 ms,等于每次打开都白付十倍的开销。登记表把这一步变成一次
|
|
60
|
+
-- 主键 seek。
|
|
61
|
+
-- 为什么不是 PRAGMA user_version:实测它在 BEGIN 里设值**不随 ROLLBACK 回退**,与回填
|
|
62
|
+
-- 放不进同一个事务(先置标记再回填中途失败 = 永久漏跑);而且它全库只有一个整数,被
|
|
63
|
+
-- 单条迁移占掉之后,日后真需要 schema 版本号就没地方放了。普通表既能跟着事务回退,
|
|
64
|
+
-- 也能一条迁移一行。
|
|
65
|
+
CREATE TABLE IF NOT EXISTS applied_migrations (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
applied_at TEXT NOT NULL
|
|
68
|
+
);
|
|
69
|
+
|
|
52
70
|
-- autoDream audit trail: one row per consolidation run, capturing the exact
|
|
53
71
|
-- input snapshot digest + the LLM decision list + per-id outcome + a compact
|
|
54
72
|
-- receipt. This makes every decision replayable so silent consolidation errors
|
|
@@ -516,6 +534,98 @@ CREATE TABLE IF NOT EXISTS continuity_proposals (
|
|
|
516
534
|
);
|
|
517
535
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_continuity_session_kind ON continuity_proposals(session_id, kind);
|
|
518
536
|
CREATE INDEX IF NOT EXISTS idx_continuity_status ON continuity_proposals(status, updated_at);
|
|
537
|
+
|
|
538
|
+
-- ============================================================
|
|
539
|
+
-- chorography(仓库地志,spec: docs/specs/2026-11-04-chorography-design.md)。
|
|
540
|
+
-- 与记忆行物理隔离的硬边界落在这组表上:wiki 的行永不进 memories /
|
|
541
|
+
-- saveWithDedupe / dream / 质量过滤的任何池子(spec §3)——autoDream 的
|
|
542
|
+
-- 「对核心知识库长期参考价值较低」归档理由一旦够得着 wiki 行,机器生成页就会
|
|
543
|
+
-- 被巩固流程自己归档掉(dream-archive-guardrail 测的正是这个语义)。
|
|
544
|
+
-- M0 只有 T0 确定性骨架(零模型);choro_claims / choro_vectors 此时是空壳,
|
|
545
|
+
-- 分别在 M2(T1 生成轮出 claims)与 M1/M2(embedder 接入)开始灌数据。
|
|
546
|
+
--
|
|
547
|
+
-- 仓库登记(实现计划 P2 补口):spec §5 的 choro_modules.repo_id 指向这里;
|
|
548
|
+
-- 消费面「按工作目录匹配已登记仓库」(spec §7)也要求登记行。root_path 存
|
|
549
|
+
-- realpath——Windows 上大小写不同的两条路径指向同一目录,登记表按 realpath
|
|
550
|
+
-- 收敛成一行。唯一性照本仓惯例走独立索引,不进 CREATE TABLE。
|
|
551
|
+
CREATE TABLE IF NOT EXISTS choro_repos (
|
|
552
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
553
|
+
root_path TEXT NOT NULL,
|
|
554
|
+
name TEXT NOT NULL,
|
|
555
|
+
plan_fingerprint TEXT, -- 上次成功 run 的 plan.yaml 内容 sha256
|
|
556
|
+
git_baseline TEXT, -- 上次成功 run 的 HEAD
|
|
557
|
+
created_at TEXT NOT NULL,
|
|
558
|
+
updated_at TEXT NOT NULL
|
|
559
|
+
);
|
|
560
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_repos_root ON choro_repos(root_path);
|
|
561
|
+
|
|
562
|
+
-- 模块划分与依赖边。fingerprint = 模块文件集 + 边集的内容 sha256:幂等语义靠
|
|
563
|
+
-- replaceChoroGraph 的整仓替换,不靠逐行 diff,所以本表没有「上次见过的划分」
|
|
564
|
+
-- 之外的账要记。
|
|
565
|
+
CREATE TABLE IF NOT EXISTS choro_modules (
|
|
566
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
567
|
+
repo_id INTEGER NOT NULL,
|
|
568
|
+
slug TEXT NOT NULL,
|
|
569
|
+
title TEXT NOT NULL,
|
|
570
|
+
parent_id INTEGER, -- 最近祖先模块(目录包含关系),无则 NULL
|
|
571
|
+
file_count INTEGER NOT NULL DEFAULT 0,
|
|
572
|
+
fingerprint TEXT,
|
|
573
|
+
updated_at TEXT NOT NULL
|
|
574
|
+
);
|
|
575
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_modules_repo_slug ON choro_modules(repo_id, slug);
|
|
576
|
+
|
|
577
|
+
-- 页面 ↔ baseline ↔ 状态。rel_path 是仓库相对路径(.chorography/modules/<slug>.md):
|
|
578
|
+
-- 真源在仓库磁盘的 MD 里(spec §4),本表只做索引与状态账,删了可以整表重算。
|
|
579
|
+
CREATE TABLE IF NOT EXISTS choro_pages (
|
|
580
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
581
|
+
module_id INTEGER NOT NULL,
|
|
582
|
+
rel_path TEXT NOT NULL,
|
|
583
|
+
status TEXT NOT NULL DEFAULT 'skeleton', -- skeleton | enriched | stale | failed
|
|
584
|
+
git_baseline TEXT,
|
|
585
|
+
template_version TEXT,
|
|
586
|
+
generated_at TEXT,
|
|
587
|
+
human_block_count INTEGER NOT NULL DEFAULT 0
|
|
588
|
+
);
|
|
589
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_pages_module ON choro_pages(module_id);
|
|
590
|
+
|
|
591
|
+
-- 溯源知识卡(M2 起写):陈述 + 出处锚点(相对路径#Lx-Ly)。kind 限定五类
|
|
592
|
+
-- (spec §5),不在别处发明第六类。
|
|
593
|
+
CREATE TABLE IF NOT EXISTS choro_claims (
|
|
594
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
595
|
+
page_id INTEGER NOT NULL,
|
|
596
|
+
statement TEXT NOT NULL,
|
|
597
|
+
kind TEXT NOT NULL, -- architecture | convention | tech-stack | setup | unknown
|
|
598
|
+
source_path TEXT,
|
|
599
|
+
start_line INTEGER,
|
|
600
|
+
end_line INTEGER,
|
|
601
|
+
confidence TEXT,
|
|
602
|
+
created_at TEXT NOT NULL
|
|
603
|
+
);
|
|
604
|
+
CREATE INDEX IF NOT EXISTS idx_choro_claims_page ON choro_claims(page_id);
|
|
605
|
+
|
|
606
|
+
-- 向量壳(M1/M2 接 embedder 后灌):owner_id 指向 choro_claims 或 choro_pages
|
|
607
|
+
-- (spec §5 的多态指向)。向量与记忆向量不共表、不共查询——两边换模型版本的
|
|
608
|
+
-- 节奏不同(V4 待实测项),混表会让其中一边被另一边的模型版本绑死。
|
|
609
|
+
CREATE TABLE IF NOT EXISTS choro_vectors (
|
|
610
|
+
owner_id INTEGER NOT NULL,
|
|
611
|
+
dim INTEGER NOT NULL,
|
|
612
|
+
model TEXT NOT NULL,
|
|
613
|
+
created_at TEXT NOT NULL
|
|
614
|
+
);
|
|
615
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_vectors_owner_model ON choro_vectors(owner_id, model);
|
|
616
|
+
|
|
617
|
+
-- 模块间依赖边(M1 补口,实现计划 P8):spec §7 的 choro_module_map 要画依赖图,
|
|
618
|
+
-- M0 schema 漏了这张。只记跨模块边(from_slug ≠ to_slug),模块内部边不进库——
|
|
619
|
+
-- 模块页正文里已经有完整文件级清单,库只存图需要的那一层。
|
|
620
|
+
CREATE TABLE IF NOT EXISTS choro_edges (
|
|
621
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
622
|
+
repo_id INTEGER NOT NULL,
|
|
623
|
+
from_slug TEXT NOT NULL,
|
|
624
|
+
to_slug TEXT NOT NULL,
|
|
625
|
+
weight INTEGER NOT NULL DEFAULT 1
|
|
626
|
+
);
|
|
627
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_edges_triple ON choro_edges(repo_id, from_slug, to_slug);
|
|
628
|
+
CREATE INDEX IF NOT EXISTS idx_choro_edges_to ON choro_edges(repo_id, to_slug);
|
|
519
629
|
`;
|
|
520
630
|
|
|
521
631
|
// Exported for API-layer type validation (standalone API POST /memories and
|
|
@@ -1028,6 +1138,10 @@ export function createStore(path) {
|
|
|
1028
1138
|
addColumn("memories", "content_history", "ALTER TABLE memories ADD COLUMN content_history TEXT");
|
|
1029
1139
|
addColumn("memories", "quality_score", "ALTER TABLE memories ADD COLUMN quality_score REAL");
|
|
1030
1140
|
|
|
1141
|
+
// chorography(M2,实现计划 P17):T1 生成轮的页级失败计数。T0 建的表没有它
|
|
1142
|
+
// (M0 只写 skeleton),存量库经 PRAGMA 检查后补列,幂等。
|
|
1143
|
+
addColumn("choro_pages", "fail_count", "ALTER TABLE choro_pages ADD COLUMN fail_count INTEGER NOT NULL DEFAULT 0");
|
|
1144
|
+
|
|
1031
1145
|
// v0.8.0 A1(issue #17)scope 隔离存储层:四列全部可空、不带 DEFAULT——存量行
|
|
1032
1146
|
// 零重写,读侧把 NULL 视为未标注(= 全局可见)。是否落值由 scopeEnabled 门控
|
|
1033
1147
|
// (service/tools 层),存储层只负责归一化(normalizeScopeText/normalizeOccurredAt)。
|
|
@@ -1113,6 +1227,141 @@ export function createStore(path) {
|
|
|
1113
1227
|
}
|
|
1114
1228
|
}
|
|
1115
1229
|
|
|
1230
|
+
// 一次性数据迁移 #1:撤回旧重复度度量的误判(#560 的收尾)。
|
|
1231
|
+
//
|
|
1232
|
+
// v0.7.12 起 repetitive 用的是「唯一字符数 ÷ 总长度 < 0.3」,这个比率随长度单调下降,
|
|
1233
|
+
// 于是长中文记忆被系统性判成重复:本机 12,827 条里 800–2000 字的 94.3%、2000 字以上的
|
|
1234
|
+
// 100% 命中。0.11.0 把度量换成 trigram 重复率(> 0.7),那只管住了**新写入**——
|
|
1235
|
+
// evaluateMemoryQuality 全仓只有 service 的写路径一处调用,更新路径不重算 quality_score,
|
|
1236
|
+
// 所以库里那批标签与扣分原封不动。
|
|
1237
|
+
//
|
|
1238
|
+
// 口径是「手术式撤销」,不是「整列重算」:整列重算会顺手重新裁一遍别人的分——实测会
|
|
1239
|
+
// 新命中 90 条 meta 与 98 条 self_referential(判据没变、但评分当时没跑到这些行),
|
|
1240
|
+
// 那是双向改写,越过了「只撤除被修掉的那个误判」的范围。这里只动带 repetitive 的行,
|
|
1241
|
+
// 且必须**用新度量复算仍然不重复**才动;新度量下依旧 > 0.7 的是真重复,标签有效,保持原样。
|
|
1242
|
+
//
|
|
1243
|
+
// 分数据「只升不降」:重建值来自存活标签的扣分表,取 max(存量, 重建) 而不是直接取重建——
|
|
1244
|
+
// 存量分不是纯标签函数(实测有 3 行与任何标签组合都对不上,历史上有信号被改路径抹掉过),
|
|
1245
|
+
// 直接用重建值会把这批行往下压。这次迁移的理由是「它被误扣了 50」,只有加回去才对得上。
|
|
1246
|
+
//
|
|
1247
|
+
// 不碰 archived / archived_at / updated_at / content_hash / embedding,也不 bump
|
|
1248
|
+
// generation:归档位是另一个决定(撤销归档 = 把 300 条重新放进注入池),不夹带在
|
|
1249
|
+
// 「修分数」里;不刷 updated_at 则代表镜像不重排——代价是镜像文件里这些行的 tags 行
|
|
1250
|
+
// 会显示已删掉的 repetitive,而 parseHumanEdits 不解析 tags,陈旧只停留在显示层、不回灌。
|
|
1251
|
+
// 这层陈旧还会自己收敛:镜像每次 syncMirror 都是全量重渲染(store.list limit:null),
|
|
1252
|
+
// 所以启动后第一次业务写就把这批行刷对了——为这几分钟专门接一条「打开库即全量重绘」
|
|
1253
|
+
// 的路径,代价(新导出 syncMirror + createStore 回传标记 + 两个启动点各改一行)大于收益。
|
|
1254
|
+
//
|
|
1255
|
+
// 用 applied_migrations 登记而不是条件式恒等幂等:这批条件在跑完之后**恒不命中**,
|
|
1256
|
+
// 但要全表扫一遍才能证明不命中(本机实测 36.7 ms,而 createStore 稳态只有 3.3 ms)——
|
|
1257
|
+
// 每次打开都白付十倍开销。登记表把它变成一次主键 seek(PRAGMA user_version 做不到:
|
|
1258
|
+
// 实测它在 BEGIN 里设值不随 ROLLBACK 回退,先置标记再回填会永久漏跑)。
|
|
1259
|
+
//
|
|
1260
|
+
// 回填分批提交、登记行最后单独提交,不做「一个大事务包住全部」:逐批提交才不会把
|
|
1261
|
+
// 「候选行数 × 全文长度」攒在内存与一次 WAL 里;而这样拆之所以仍然安全,是因为
|
|
1262
|
+
// **修过的行就不再是候选**(repetitive 已撤),中途崩掉重跑只会从断点接着走,不会
|
|
1263
|
+
// 重复加回分数。登记行只认「全部批次都跑完、且没有一条被 CAS 挡下」,半截进度不落地。
|
|
1264
|
+
{
|
|
1265
|
+
const MIGRATION_ID = "2026-11-quality-repetitive-backfill";
|
|
1266
|
+
const applied = db.prepare("SELECT 1 FROM applied_migrations WHERE id = ?").get(MIGRATION_ID);
|
|
1267
|
+
if (!applied) {
|
|
1268
|
+
// LIKE 只负责候选裁剪,真正的判据是下一段的 tags.includes("repetitive")——把
|
|
1269
|
+
// LIKE 放松成 %repetitive% 也筛不出错误结果(变异自证里它是等价变异)。用带引号
|
|
1270
|
+
// 的形态是为了缩小候选集、少解析几条将来可能出现的坏 JSON,不是为了防误伤。
|
|
1271
|
+
// 按 rowid 键集翻页,每批独立提交:候选行带全文,一次 .all() 会把「行数 × 正文
|
|
1272
|
+
// 长度」全攒在内存里(本机 1,476 条合计 726 KB,大库不是这个量级)。分批之所以
|
|
1273
|
+
// 仍然安全,是因为候选集自己会收缩——修过的行撤掉 repetitive 就不再命中;
|
|
1274
|
+
// 中途崩掉重跑只会从断点接着走,不会把已经加回去的分再加一遍。
|
|
1275
|
+
const BATCH = 200;
|
|
1276
|
+
const selectBatch = db.prepare(
|
|
1277
|
+
`SELECT rowid AS rid, id, content, _full_content, tags, quality_score
|
|
1278
|
+
FROM memories
|
|
1279
|
+
WHERE tags LIKE '%"repetitive"%' AND rowid > ?
|
|
1280
|
+
ORDER BY rowid
|
|
1281
|
+
LIMIT ?`
|
|
1282
|
+
);
|
|
1283
|
+
// CAS:带上读到的旧 tags、旧 quality_score 与两列正文作为条件。多宿主共用一个库是
|
|
1284
|
+
// 支持形态(busy_timeout + WAL),SELECT 到 UPDATE 之间别的进程可能刚改过这一行——
|
|
1285
|
+
// 那就以它为准、跳过这一条,不拿这次读到的旧判断覆盖它。正文两列也要带:判据读的是
|
|
1286
|
+
// 正文,只校验 tags/quality_score 时,正文被换成重复文本而另两列未变仍会 CAS 通过,
|
|
1287
|
+
// 于是把本该保留的 repetitive 撤掉。
|
|
1288
|
+
const stmt = db.prepare(
|
|
1289
|
+
`UPDATE memories SET tags = ?, quality_score = ?
|
|
1290
|
+
WHERE id = ? AND tags = ? AND quality_score IS ?
|
|
1291
|
+
AND content IS ? AND _full_content IS ?
|
|
1292
|
+
AND tags LIKE '%"repetitive"%'`
|
|
1293
|
+
);
|
|
1294
|
+
// CAS 落空的行这一趟没修到,它们仍是候选——那就别登记「已完成」,下次打开整条重跑
|
|
1295
|
+
// (修过的行已不是候选,重跑天然幂等)。否则一条被别处同时改写而保留 repetitive 的行
|
|
1296
|
+
// 会带着旧分与旧标签一直留在库里。
|
|
1297
|
+
let casMisses = 0;
|
|
1298
|
+
let cursor = 0;
|
|
1299
|
+
for (;;) {
|
|
1300
|
+
const rows = selectBatch.all(cursor, BATCH);
|
|
1301
|
+
if (rows.length === 0) break;
|
|
1302
|
+
cursor = rows[rows.length - 1].rid;
|
|
1303
|
+
const updates = [];
|
|
1304
|
+
for (const row of rows) {
|
|
1305
|
+
let tags;
|
|
1306
|
+
try {
|
|
1307
|
+
tags = JSON.parse(row.tags);
|
|
1308
|
+
} catch {
|
|
1309
|
+
continue; // 坏 tags 不动:这条迁移不该顺手重写一个解析不了的值
|
|
1310
|
+
}
|
|
1311
|
+
if (!Array.isArray(tags) || !tags.includes("repetitive")) continue;
|
|
1312
|
+
// 含非字符串元素的行整条放过:下面 Object.hasOwn 对对象元素会先做属性键转换,
|
|
1313
|
+
// 抛 TypeError(JSON.parse('{"toString":1}') 这类值可达),而这里没有任何 catch,
|
|
1314
|
+
// 异常会直接让 createStore 中止、迁移永远登不上记,下次打开原地再撞同一行。
|
|
1315
|
+
if (!tags.every((t) => typeof t === "string")) continue;
|
|
1316
|
+
// 判据落在**原文**上:已降级的行 content 只剩前 120 字摘要(demoteToSummary
|
|
1317
|
+
// 把正文挪进 _full_content,且它不重算 quality_score),拿摘要判重复会把
|
|
1318
|
+
// 「前缀不重复、后段大量重复」的原文放过去——而那恰好是旧度量最容易误判的
|
|
1319
|
+
// 那一类。未降级的行 _full_content 为 NULL,退回 content,与原判据一致。
|
|
1320
|
+
const body = String(row._full_content ?? row.content ?? "");
|
|
1321
|
+
// 新度量下仍然重复 → 标签是对的,保持原样(不重算分数:那会变成整列重算)
|
|
1322
|
+
if (repetitionRatio(body.trim()) > REPETITION_THRESHOLD) continue;
|
|
1323
|
+
const kept = tags.filter((t) => t !== "repetitive");
|
|
1324
|
+
let rebuilt = 100;
|
|
1325
|
+
// 只减自有键:SIGNAL_DEDUCTIONS 是普通对象字面量,查「constructor」命中的
|
|
1326
|
+
// 是原型链上的函数,`?? 0` 挡不住,会把分数算成 NaN 再写进列里。
|
|
1327
|
+
for (const tag of kept) {
|
|
1328
|
+
if (Object.hasOwn(SIGNAL_DEDUCTIONS, tag)) rebuilt -= SIGNAL_DEDUCTIONS[tag];
|
|
1329
|
+
}
|
|
1330
|
+
rebuilt = Math.max(0, Math.min(100, Math.round(rebuilt)));
|
|
1331
|
+
const stored = Number.isFinite(row.quality_score) ? row.quality_score : null;
|
|
1332
|
+
const score = stored === null ? rebuilt : Math.max(stored, rebuilt);
|
|
1333
|
+
// low_quality 是分数的派生态(< 30 必须有、≥ 30 必须没有),分数动了它就跟着动。
|
|
1334
|
+
// 显式同步而不是留给下次写入:不重算的路径不会自己把它修好。
|
|
1335
|
+
const next = kept.filter((t) => t !== "low_quality");
|
|
1336
|
+
if (score < 30) next.push("low_quality");
|
|
1337
|
+
updates.push([
|
|
1338
|
+
JSON.stringify(next),
|
|
1339
|
+
score,
|
|
1340
|
+
row.id,
|
|
1341
|
+
row.tags,
|
|
1342
|
+
row.quality_score,
|
|
1343
|
+
row.content,
|
|
1344
|
+
row._full_content
|
|
1345
|
+
]);
|
|
1346
|
+
}
|
|
1347
|
+
if (updates.length === 0) continue;
|
|
1348
|
+
const batch = updates;
|
|
1349
|
+
// 一批一个事务:逐条自动提交是每行一次 WAL 提交(与 content_hash 回填同一个教训)。
|
|
1350
|
+
runAtomically(() => {
|
|
1351
|
+
for (const u of batch) {
|
|
1352
|
+
if (stmt.run(u[0], u[1], u[2], u[3], u[4], u[5], u[6]).changes === 0) casMisses += 1;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
// 登记行单独提交,且只在**所有批次都跑完、且没有一条 CAS 落空**之后:它是「这条迁移
|
|
1357
|
+
// 已完成」的证据,半截进度不能落地,否则重跑会被自己挡住、剩下的行再也修不回来。
|
|
1358
|
+
const mark = db.prepare("INSERT OR IGNORE INTO applied_migrations (id, applied_at) VALUES (?, ?)");
|
|
1359
|
+
// 时间戳这里直接用 new Date():nowIso 的单调守卫是给 memories.updated_at 用的,
|
|
1360
|
+
// 而且它依赖的 lastTs 此刻还没初始化(TDZ),调它会在打开库时直接抛。
|
|
1361
|
+
if (casMisses === 0) runAtomically(() => mark.run(MIGRATION_ID, new Date().toISOString()));
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1116
1365
|
// Legacy dream_runs without policy_epoch → backfill with the default epoch.
|
|
1117
1366
|
addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
|
|
1118
1367
|
addColumn("dream_runs", "run_type", "ALTER TABLE dream_runs ADD COLUMN run_type TEXT NOT NULL DEFAULT 'auto'");
|
|
@@ -2857,6 +3106,84 @@ export function createStore(path) {
|
|
|
2857
3106
|
};
|
|
2858
3107
|
}
|
|
2859
3108
|
|
|
3109
|
+
/**
|
|
3110
|
+
* 沉淀曲线(面板「8 周沉淀」图):按天给出「进来的」与「真沉淀的」两条序列,
|
|
3111
|
+
* 用来一眼看出库到底在巩固还是在膨胀——只看新增会以为一直在长,只看总量
|
|
3112
|
+
* 看不出巩固停摆(2026-10-25~10-31 那段新增照旧、合并归零,就是这张图要
|
|
3113
|
+
* 暴露的形态)。
|
|
3114
|
+
*
|
|
3115
|
+
* added = 当天新建记忆。排除外来导入行(本库不为别人导入的原文记账,
|
|
3116
|
+
* 见 memory-source.js);`source IS NULL OR` 那一支是必填——
|
|
3117
|
+
* SQL 三值逻辑让 `NULL <> 'imported'` 求值为 NULL 而非 true,
|
|
3118
|
+
* 只写后者的版本会让本方行(source 恒为 NULL)整批消失。
|
|
3119
|
+
* merged = 当天 receipt_chain 里 kind∈(merge,update) 且 verdict='live' 的
|
|
3120
|
+
* **去重 record_id 数**,不是行数。同一条记录被多轮巩固重复记账
|
|
3121
|
+
* 是常态(活库实测 2412 行只对应 1796 个记录,见 #559 期间的
|
|
3122
|
+
* 核对),按行数会把「又记了一遍」算成「又沉淀了一条」。口径与
|
|
3123
|
+
* store 里「已巩固」的既有子查询同源(kind IN ('merge','update')
|
|
3124
|
+
* AND verdict='live'),不另立一套。
|
|
3125
|
+
* archived = 当天进入归档面的条数(archived_at 幂等,见其注释:已归档再
|
|
3126
|
+
* 归档不刷新,取消归档置 NULL),所以日聚合 = 当天新归档条数。
|
|
3127
|
+
*
|
|
3128
|
+
* 三列都按 UTC 日期切分,与 getLlmAuditStats 的 by_day 同一套日期口径,
|
|
3129
|
+
* 前端补零/对齐逻辑因此可以复用。
|
|
3130
|
+
*
|
|
3131
|
+
* 只返回有数据的日期(GROUP BY 不补零),与 by_day 同形——补零是展示层的事,
|
|
3132
|
+
* 后端补零会让「未知」和「已知为空」在响应里长得一样。
|
|
3133
|
+
*/
|
|
3134
|
+
function getConsolidationStats({ days = 56 } = {}) {
|
|
3135
|
+
const span = Math.max(1, Math.min(365, Math.floor(Number(days)) || 56));
|
|
3136
|
+
const since = new Date(Date.now() - span * 86400000).toISOString();
|
|
3137
|
+
const added = db.prepare(
|
|
3138
|
+
`SELECT date(created_at) AS day, count(*) AS c
|
|
3139
|
+
FROM memories
|
|
3140
|
+
WHERE created_at >= ? AND (source IS NULL OR source <> ?)
|
|
3141
|
+
GROUP BY date(created_at) ORDER BY day ASC`
|
|
3142
|
+
).all(since, IMPORTED_SOURCE);
|
|
3143
|
+
const merged = db.prepare(
|
|
3144
|
+
`SELECT day, count(*) AS c FROM (
|
|
3145
|
+
SELECT date(created_at) AS day, record_id
|
|
3146
|
+
FROM receipt_chain
|
|
3147
|
+
WHERE created_at >= ? AND kind IN ('merge', 'update') AND verdict = 'live'
|
|
3148
|
+
GROUP BY day, record_id
|
|
3149
|
+
) GROUP BY day ORDER BY day ASC`
|
|
3150
|
+
).all(since);
|
|
3151
|
+
const archived = db.prepare(
|
|
3152
|
+
`SELECT date(archived_at) AS day, count(*) AS c
|
|
3153
|
+
FROM memories
|
|
3154
|
+
WHERE archived_at IS NOT NULL AND archived_at >= ? AND (source IS NULL OR source <> ?)
|
|
3155
|
+
GROUP BY date(archived_at) ORDER BY day ASC`
|
|
3156
|
+
).all(since, IMPORTED_SOURCE);
|
|
3157
|
+
// 三串按天并进一张表:三条 SQL 各扫各的索引,比一次 UNION + 外层聚合好读,
|
|
3158
|
+
// 也让某一段全 0 时不会因为缺行而丢掉这一天(缺的是 0,不是不存在)。
|
|
3159
|
+
const byDay = new Map();
|
|
3160
|
+
const bump = (rows, key) => {
|
|
3161
|
+
for (const r of rows) {
|
|
3162
|
+
const day = String(r.day ?? "");
|
|
3163
|
+
if (!day) continue;
|
|
3164
|
+
const cur = byDay.get(day) ?? { day, added: 0, merged: 0, archived: 0 };
|
|
3165
|
+
cur[key] = Number(r.c) || 0;
|
|
3166
|
+
byDay.set(day, cur);
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
bump(added, "added");
|
|
3170
|
+
bump(merged, "merged");
|
|
3171
|
+
bump(archived, "archived");
|
|
3172
|
+
const list = [...byDay.values()].sort((a, b) => (a.day < b.day ? -1 : a.day > b.day ? 1 : 0));
|
|
3173
|
+
const sum = list.reduce(
|
|
3174
|
+
(acc, r) => ({ added: acc.added + r.added, merged: acc.merged + r.merged, archived: acc.archived + r.archived }),
|
|
3175
|
+
{ added: 0, merged: 0, archived: 0 }
|
|
3176
|
+
);
|
|
3177
|
+
return {
|
|
3178
|
+
days: span,
|
|
3179
|
+
since,
|
|
3180
|
+
by_day: list,
|
|
3181
|
+
total_added: sum.added,
|
|
3182
|
+
total_merged: sum.merged,
|
|
3183
|
+
total_archived: sum.archived
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
|
|
2860
3187
|
/** Delete audit rows older than `before` (ISO string). Returns count removed. */
|
|
2861
3188
|
function deleteOldLlmAudits(before) {
|
|
2862
3189
|
return db.prepare("DELETE FROM llm_audit_logs WHERE timestamp < ?").run(before).changes;
|
|
@@ -3926,6 +4253,16 @@ export function createStore(path) {
|
|
|
3926
4253
|
return rows.map(toEntity);
|
|
3927
4254
|
}
|
|
3928
4255
|
|
|
4256
|
+
/**
|
|
4257
|
+
* 实体总行数(全库,不受 limit 影响)。目录路由的 `total` 必须走这里:
|
|
4258
|
+
* 它此前填的是 `entities.length`(本次切片长度),于是库里一万六千个实体
|
|
4259
|
+
* 在面板上永远显示 500 —— 数字看着像"实体太少",其实是被上限截断了。
|
|
4260
|
+
* 与 listEntities 同源同表,只差一个 COUNT。
|
|
4261
|
+
*/
|
|
4262
|
+
function countEntities() {
|
|
4263
|
+
return db.prepare("SELECT COUNT(*) AS n FROM entities").get().n;
|
|
4264
|
+
}
|
|
4265
|
+
|
|
3929
4266
|
// --- 图召回轴(issue #219)-------------------------------------------------
|
|
3930
4267
|
|
|
3931
4268
|
/**
|
|
@@ -4215,6 +4552,161 @@ export function createStore(path) {
|
|
|
4215
4552
|
});
|
|
4216
4553
|
}
|
|
4217
4554
|
|
|
4555
|
+
/* --------------------- chorography(仓库地志,M0 只写不读) --------------------- */
|
|
4556
|
+
// 消费面(检索 / 注入)在 M1 才接,这里只落写入口。时间戳由调用方传:一次 run
|
|
4557
|
+
// 的所有行共用同一个时刻(生成的页正文里也引用它),由 t0.js 的单一时钟保证,
|
|
4558
|
+
// store 不自取 now()——两处时钟会把「同一 run」拆成两个版本。
|
|
4559
|
+
|
|
4560
|
+
function choroRepoByRoot(rootPath) {
|
|
4561
|
+
return db.prepare("SELECT * FROM choro_repos WHERE root_path = ?").get(rootPath) ?? undefined;
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
// 登记或刷新仓库行。created_at 只在首次插入时落(ON CONFLICT 不碰它);
|
|
4565
|
+
// plan_fingerprint / git_baseline 永远反映上次成功 run——失败 run 不写这两格
|
|
4566
|
+
// (红线 5「失败不推进 state」,库侧与 state.json 同一口径)。
|
|
4567
|
+
function upsertChoroRepo(rootPath, name, planFingerprint, gitBaseline, updatedAt) {
|
|
4568
|
+
db.prepare(
|
|
4569
|
+
`INSERT INTO choro_repos (root_path, name, plan_fingerprint, git_baseline, created_at, updated_at)
|
|
4570
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
4571
|
+
ON CONFLICT(root_path) DO UPDATE SET
|
|
4572
|
+
name = excluded.name,
|
|
4573
|
+
plan_fingerprint = excluded.plan_fingerprint,
|
|
4574
|
+
git_baseline = excluded.git_baseline,
|
|
4575
|
+
updated_at = excluded.updated_at`
|
|
4576
|
+
).run(rootPath, name, planFingerprint, gitBaseline, updatedAt, updatedAt);
|
|
4577
|
+
return choroRepoByRoot(rootPath);
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
// 整仓替换(实现计划 P7):一次成功 run 的模块/页面/claims 全量重写。幂等语义
|
|
4581
|
+
// 在这里成型:重跑同一划分 = 删了重插同样内容,行不翻倍;划分变了也不会留孤儿行
|
|
4582
|
+
// (旧模块的页面与 claims 随模块一起删)。事务包住删除与插入,半截失败整体回滚,
|
|
4583
|
+
// 库里保持上一次成功 run 的完整图。
|
|
4584
|
+
// 父链两段式落(先全插再补 parent_id):父模块的行号在插入前不存在,按 slug 排序
|
|
4585
|
+
// 的单遍插入会遇到「父在后」的顺序问题,两段式与顺序无关。
|
|
4586
|
+
function replaceChoroGraph(repoId, snapshot) {
|
|
4587
|
+
return runAtomically(() => {
|
|
4588
|
+
db.prepare(
|
|
4589
|
+
`DELETE FROM choro_claims WHERE page_id IN (
|
|
4590
|
+
SELECT p.id FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
|
|
4591
|
+
WHERE m.repo_id = ?)`
|
|
4592
|
+
).run(repoId);
|
|
4593
|
+
db.prepare(
|
|
4594
|
+
"DELETE FROM choro_pages WHERE module_id IN (SELECT id FROM choro_modules WHERE repo_id = ?)"
|
|
4595
|
+
).run(repoId);
|
|
4596
|
+
db.prepare("DELETE FROM choro_edges WHERE repo_id = ?").run(repoId);
|
|
4597
|
+
db.prepare("DELETE FROM choro_modules WHERE repo_id = ?").run(repoId);
|
|
4598
|
+
|
|
4599
|
+
const insModule = db.prepare(
|
|
4600
|
+
`INSERT INTO choro_modules (repo_id, slug, title, parent_id, file_count, fingerprint, updated_at)
|
|
4601
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?)`
|
|
4602
|
+
);
|
|
4603
|
+
const ids = new Map();
|
|
4604
|
+
for (const mod of snapshot.modules) {
|
|
4605
|
+
const info = insModule.run(
|
|
4606
|
+
repoId, mod.slug, mod.title, mod.fileCount, mod.fingerprint, snapshot.updatedAt
|
|
4607
|
+
);
|
|
4608
|
+
ids.set(mod.slug, Number(info.lastInsertRowid));
|
|
4609
|
+
}
|
|
4610
|
+
const setParent = db.prepare("UPDATE choro_modules SET parent_id = ? WHERE id = ?");
|
|
4611
|
+
for (const mod of snapshot.modules) {
|
|
4612
|
+
if (mod.parentSlug == null) continue;
|
|
4613
|
+
const parentId = ids.get(mod.parentSlug);
|
|
4614
|
+
if (parentId != null) setParent.run(parentId, ids.get(mod.slug));
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
const insPage = db.prepare(
|
|
4618
|
+
`INSERT INTO choro_pages
|
|
4619
|
+
(module_id, rel_path, status, git_baseline, template_version, generated_at, human_block_count)
|
|
4620
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
4621
|
+
);
|
|
4622
|
+
const insClaim = db.prepare(
|
|
4623
|
+
`INSERT INTO choro_claims
|
|
4624
|
+
(page_id, statement, kind, source_path, start_line, end_line, confidence, created_at)
|
|
4625
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
4626
|
+
);
|
|
4627
|
+
for (const page of snapshot.pages) {
|
|
4628
|
+
const moduleId = ids.get(page.moduleSlug);
|
|
4629
|
+
if (moduleId == null) continue; // 划分与页面同源生成,走到这里说明调用方组装错了
|
|
4630
|
+
const pageInfo = insPage.run(
|
|
4631
|
+
moduleId, page.relPath, page.status, page.gitBaseline,
|
|
4632
|
+
page.templateVersion, page.generatedAt, page.humanBlockCount
|
|
4633
|
+
);
|
|
4634
|
+
for (const claim of page.claims ?? []) {
|
|
4635
|
+
insClaim.run(
|
|
4636
|
+
Number(pageInfo.lastInsertRowid), claim.statement, claim.kind,
|
|
4637
|
+
claim.sourcePath ?? null, claim.startLine ?? null, claim.endLine ?? null,
|
|
4638
|
+
claim.confidence ?? null, snapshot.updatedAt
|
|
4639
|
+
);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
// 跨模块边(P8):from/to 都是模块 slug,只存 from≠to 的;同向多条文件级
|
|
4643
|
+
// 边聚合为一条带 weight 的模块级边。
|
|
4644
|
+
const insEdge = db.prepare(
|
|
4645
|
+
"INSERT INTO choro_edges (repo_id, from_slug, to_slug, weight) VALUES (?, ?, ?, ?)"
|
|
4646
|
+
);
|
|
4647
|
+
let edgeCount = 0;
|
|
4648
|
+
for (const edge of snapshot.edges ?? []) {
|
|
4649
|
+
if (edge.fromSlug === edge.toSlug) continue;
|
|
4650
|
+
insEdge.run(repoId, edge.fromSlug, edge.toSlug, edge.weight ?? 1);
|
|
4651
|
+
edgeCount++;
|
|
4652
|
+
}
|
|
4653
|
+
return { modules: snapshot.modules.length, pages: snapshot.pages.length, edges: edgeCount };
|
|
4654
|
+
});
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
// T1 的页级读写(实现计划 P16–P18):与 replaceChoroGraph 的整仓替换并存——
|
|
4658
|
+
// T1 是逐模块的(一轮只动一页),整仓替换会把其他页的富化状态抹掉。
|
|
4659
|
+
function choroPageBySlug(repoId, slug) {
|
|
4660
|
+
return db.prepare(
|
|
4661
|
+
`SELECT p.id, p.module_id, p.rel_path, p.status, p.git_baseline, p.template_version,
|
|
4662
|
+
p.generated_at, p.human_block_count, p.fail_count, m.slug
|
|
4663
|
+
FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
|
|
4664
|
+
WHERE m.repo_id = ? AND m.slug = ?`
|
|
4665
|
+
).get(repoId, String(slug));
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
// 单页失败记账(spec 步 12「失败两次即记 failed」):首次失败只计数、状态不动
|
|
4669
|
+
// (骨架仍是当前有效产物),第二次到阈值才把 status 推成 failed。generatedAt
|
|
4670
|
+
// 不动——失败不推进 state(红线 5)的页级口径。
|
|
4671
|
+
function recordChoroPageFailure(pageId, failThreshold = 2) {
|
|
4672
|
+
db.prepare("UPDATE choro_pages SET fail_count = fail_count + 1 WHERE id = ?").run(pageId);
|
|
4673
|
+
const row = db.prepare("SELECT fail_count, status FROM choro_pages WHERE id = ?").get(pageId);
|
|
4674
|
+
if (row.fail_count >= failThreshold) {
|
|
4675
|
+
db.prepare("UPDATE choro_pages SET status = 'failed' WHERE id = ?").run(pageId);
|
|
4676
|
+
row.status = "failed";
|
|
4677
|
+
}
|
|
4678
|
+
return row;
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
// 富化成功:skeleton → enriched,generatedAt 推进、失败计数清零(成功重置的是
|
|
4682
|
+
// 「连续失败」的连续性,不是历史)。
|
|
4683
|
+
function markChoroPageEnriched(pageId, generatedAt) {
|
|
4684
|
+
db.prepare(
|
|
4685
|
+
"UPDATE choro_pages SET status = 'enriched', generated_at = ?, fail_count = 0 WHERE id = ?"
|
|
4686
|
+
).run(generatedAt, pageId);
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// 单页 claims 整页替换(P20 的表侧):T1 一轮产出的 claims 删旧插新;evidence/
|
|
4690
|
+
// JSON 由 t1.js 同步写盘(两处同源同写)。
|
|
4691
|
+
function saveChoroPageClaims(pageId, claims) {
|
|
4692
|
+
return runAtomically(() => {
|
|
4693
|
+
db.prepare("DELETE FROM choro_claims WHERE page_id = ?").run(pageId);
|
|
4694
|
+
const ins = db.prepare(
|
|
4695
|
+
`INSERT INTO choro_claims
|
|
4696
|
+
(page_id, statement, kind, source_path, start_line, end_line, confidence, created_at)
|
|
4697
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
4698
|
+
);
|
|
4699
|
+
for (const claim of claims) {
|
|
4700
|
+
ins.run(
|
|
4701
|
+
pageId, claim.statement, claim.kind,
|
|
4702
|
+
claim.sourcePath ?? null, claim.startLine ?? null, claim.endLine ?? null,
|
|
4703
|
+
claim.confidence ?? null, claim.createdAt
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
return claims.length;
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4218
4710
|
return {
|
|
4219
4711
|
db,
|
|
4220
4712
|
count,
|
|
@@ -4294,6 +4786,7 @@ export function createStore(path) {
|
|
|
4294
4786
|
listLlmAudits,
|
|
4295
4787
|
countLlmAudits,
|
|
4296
4788
|
getLlmAuditStats,
|
|
4789
|
+
getConsolidationStats,
|
|
4297
4790
|
deleteOldLlmAudits,
|
|
4298
4791
|
// 蒸馏原始记录与记忆级溯源(#399):窗口凭证的写入/统计/裁剪,链接的读写。
|
|
4299
4792
|
saveDistillRecords,
|
|
@@ -4344,6 +4837,7 @@ export function createStore(path) {
|
|
|
4344
4837
|
findEntityByName,
|
|
4345
4838
|
findEntityById,
|
|
4346
4839
|
listEntities,
|
|
4840
|
+
countEntities,
|
|
4347
4841
|
updateEntity,
|
|
4348
4842
|
saveAttr,
|
|
4349
4843
|
invalidateOldAttr,
|
|
@@ -4378,6 +4872,14 @@ export function createStore(path) {
|
|
|
4378
4872
|
getContinuityProposal,
|
|
4379
4873
|
listContinuityProposals,
|
|
4380
4874
|
countContinuityProposals,
|
|
4875
|
+
// chorography(仓库地志):M0 只写不读,消费面 M1 接(见上面注释块)。
|
|
4876
|
+
choroRepoByRoot,
|
|
4877
|
+
upsertChoroRepo,
|
|
4878
|
+
replaceChoroGraph,
|
|
4879
|
+
choroPageBySlug,
|
|
4880
|
+
recordChoroPageFailure,
|
|
4881
|
+
markChoroPageEnriched,
|
|
4882
|
+
saveChoroPageClaims,
|
|
4381
4883
|
close() {
|
|
4382
4884
|
db.close();
|
|
4383
4885
|
}
|