@modusensus/dsh-mneme 0.12.0 → 0.14.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -20
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +47 -6
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +788 -30
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +4627 -1409
- package/lib/config.js +166 -1
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/dream/decisions.js +95 -6
- package/lib/dream/sleep.js +760 -19
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +168 -21
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +22 -11
- package/lib/index.js +123 -3
- package/lib/inject.js +203 -20
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +98 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +268 -69
- package/lib/settings.js +128 -15
- package/lib/store.js +585 -26
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +282 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +145 -32
- package/package.json +39 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +788 -30
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +166 -1
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/dream/decisions.js +95 -6
- package/src/dream/sleep.js +760 -19
- package/src/dream/triage.js +139 -0
- package/src/dream.js +168 -21
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +22 -11
- package/src/index.js +123 -3
- package/src/inject.js +203 -20
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +98 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +268 -69
- package/src/settings.js +128 -15
- package/src/store.js +585 -26
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +282 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +145 -32
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +385 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +1051 -185
- package/test/conflict-freeze.test.js +45 -1
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/continuity.test.js +8 -1
- package/test/contrast.test.js +124 -12
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +7 -7
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +635 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +463 -1
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +128 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-t1.test.js +226 -1
package/lib/wiki/queries.js
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
// 输入——面板与工具传进来的 slug 查不到模块行就返回 null,绝不用它拼路径。
|
|
10
10
|
import { readFileSync, existsSync, realpathSync } from "node:fs";
|
|
11
11
|
import { join, resolve, sep } from "node:path";
|
|
12
|
+
// 过期判定复用既有三件真源(t0 的 gitHead、plan 的指纹、render 的模板版本),
|
|
13
|
+
// 面板侧不另造一套判据——两套判据迟早会对同一仓库给出两个答案。
|
|
14
|
+
import { gitHead } from "./t0.js";
|
|
15
|
+
import { loadPlanFromRepo } from "./plan.js";
|
|
16
|
+
import { TEMPLATE_VERSION, T1_SECTION_START } from "./render.js";
|
|
17
|
+
// claim 失效判定的纯函数(借 openwiki 的 Grounded Claims):解析 diff + 逐条比对
|
|
18
|
+
// 三列证据。放独立模块是因为它零 DB、零 IO(除 diffSince 那一次 git),可单测。
|
|
19
|
+
import { diffSince, summarizeClaims } from "./claims.js";
|
|
12
20
|
|
|
13
21
|
// 页正文去 frontmatter / CHORO 标记行后作为嵌入文本(P40:嵌页级全文)。
|
|
14
22
|
function pageEmbedText(content) {
|
|
@@ -49,12 +57,131 @@ export function createChoroQueries({ store }) {
|
|
|
49
57
|
`SELECT r.id, r.root_path, r.name, r.plan_fingerprint, r.git_baseline, r.updated_at,
|
|
50
58
|
(SELECT COUNT(*) FROM choro_modules m WHERE m.repo_id = r.id) AS module_count,
|
|
51
59
|
(SELECT COUNT(*) FROM choro_pages p JOIN choro_modules m2 ON p.module_id = m2.id
|
|
52
|
-
WHERE m2.repo_id = r.id) AS page_count
|
|
60
|
+
WHERE m2.repo_id = r.id) AS page_count,
|
|
61
|
+
(SELECT COUNT(*) FROM choro_claims c
|
|
62
|
+
JOIN choro_pages p2 ON c.page_id = p2.id
|
|
63
|
+
JOIN choro_modules m3 ON p2.module_id = m3.id
|
|
64
|
+
WHERE m3.repo_id = r.id) AS claim_count
|
|
53
65
|
FROM choro_repos r ORDER BY r.id`
|
|
54
66
|
).all();
|
|
55
67
|
return rows;
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 面板仓库列表(契约 §3.2):在 listRepos 的行上补契约要求的字段。
|
|
72
|
+
*
|
|
73
|
+
* 为什么另开一个函数而不是改 listRepos 的返回形状:listRepos 的
|
|
74
|
+
* module_count / page_count 被工具层与查询层测试锁着(下划线命名),改名就是
|
|
75
|
+
* 破坏既有调用方;而契约要的是 modules / pages 这样的对外字段。两套名字并存
|
|
76
|
+
* 难看,但改名的连带面比难看贵得多——装饰层把它包一层,工具层照旧读原始行。
|
|
77
|
+
*
|
|
78
|
+
* 每个仓库都要 fork 一次 git 判过期:这是用户点开面板才走的路(不是每轮注入的
|
|
79
|
+
* 热路径),一次 rev-parse 几十毫秒在交互里不可感知。
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* 契约字段的仓库对象(stale 由调用方决定是否重算)。
|
|
83
|
+
*
|
|
84
|
+
* 注意区分两个入口:repoFacts 每次都会 fork git 判过期;repoFromRow 是纯行投影,
|
|
85
|
+
* 给「已经知道过期原因、手上有判定结果」的调用方用(例如自动重跑之后要紧着按
|
|
86
|
+
* 新基线说话,不能再拿旧行算一遍)。混用会得到自相矛盾的回执。
|
|
87
|
+
*/
|
|
88
|
+
function repoFromRow(row) {
|
|
89
|
+
const rootPath = String(row.root_path ?? "");
|
|
90
|
+
return {
|
|
91
|
+
id: row.id,
|
|
92
|
+
name: row.name,
|
|
93
|
+
rootPath,
|
|
94
|
+
modules: Number(row.module_count ?? 0),
|
|
95
|
+
pages: Number(row.page_count ?? 0),
|
|
96
|
+
claims: Number(row.claim_count ?? 0),
|
|
97
|
+
// legacy = 这个仓库里有页面是用旧模板(或 M0 的 NULL)渲染的:重跑一次 T0 就
|
|
98
|
+
// 会把它们升级成当前模板,面板据此提示「建议重建」而不是让用户自己比对版本。
|
|
99
|
+
legacy: legacyPages(row.id) > 0,
|
|
100
|
+
// enrichedPages:已经富化过(页面里有 T1 语义段、库里有 claims)的页数。
|
|
101
|
+
// 自动刷新要看它:这类页重跑 T0 会退回骨架,所以那条路必须先问一句。
|
|
102
|
+
enrichedPages: enrichedPageCount(row.id),
|
|
103
|
+
lastRunAt: row.updated_at ?? null
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 已富化的页数。判据取库里的 status(T1 成功后会写 enriched),而不是读盘找
|
|
109
|
+
* T1 标记——这是条给面板列表用的轻查询,几百页也不该逐文件 readFileSync。
|
|
110
|
+
* 两边可能短暂不一致(例如有人手改了 MD),但「要不要提醒」这种提示用途,
|
|
111
|
+
* 库里的账本就是最合适的真源;真正判断「会丢什么」的仍是 T0 的 collectT1Loss(读盘)。
|
|
112
|
+
*/
|
|
113
|
+
function enrichedPageCount(repoId) {
|
|
114
|
+
const hit = db.prepare(
|
|
115
|
+
`SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
|
|
116
|
+
WHERE m.repo_id = ? AND p.status = 'enriched'`
|
|
117
|
+
).get(repoId);
|
|
118
|
+
return Number(hit?.n ?? 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function legacyPages(repoId) {
|
|
122
|
+
const hit = db.prepare(
|
|
123
|
+
`SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
|
|
124
|
+
WHERE m.repo_id = ? AND (p.template_version IS NULL OR p.template_version <> ?)`
|
|
125
|
+
).get(repoId, TEMPLATE_VERSION);
|
|
126
|
+
return Number(hit?.n ?? 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function repoFacts(row) {
|
|
130
|
+
const rootPath = String(row.root_path ?? "");
|
|
131
|
+
// git 优先:代码变了是更硬的「该重跑」信号,plan 变了才退而求其次——两者同时
|
|
132
|
+
// 成立时面板只说一句话,说「git」比说「plan」更贴近用户看到的现象(他刚改了码)。
|
|
133
|
+
const head = rootPath === "" ? null : gitHead(rootPath);
|
|
134
|
+
const baseline = String(row.git_baseline ?? "").trim();
|
|
135
|
+
let staleReason = null;
|
|
136
|
+
if (head && head !== baseline) {
|
|
137
|
+
staleReason = "git";
|
|
138
|
+
} else if (rootPath !== "") {
|
|
139
|
+
// 读不到 git(head 为 null)时不直接判「没过期」:plan.yaml 可能已经被改过,
|
|
140
|
+
// 那条判据不依赖 git,仍能给出答案。
|
|
141
|
+
let planFingerprintNow = null;
|
|
142
|
+
try { planFingerprintNow = loadPlanFromRepo(rootPath).fingerprint; } catch { planFingerprintNow = null; }
|
|
143
|
+
// plan.yaml 校验不过时 loadPlanFromRepo 会抛——那是「现在读不出结论」,不是
|
|
144
|
+
// 「过期了」,所以落 null 保持 stale=false,让重建按钮自己去报那条 YAML 错误。
|
|
145
|
+
if (planFingerprintNow && planFingerprintNow !== String(row.plan_fingerprint ?? "")) {
|
|
146
|
+
staleReason = "plan";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...repoFromRow(row),
|
|
151
|
+
stale: staleReason !== null,
|
|
152
|
+
staleReason
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 单仓的原始 listRepos 行(自动重跑之后要拿推进过的基线,不能再用旧行)。 */
|
|
157
|
+
function listRepoRow(repoId) {
|
|
158
|
+
return listRepos().find((r) => Number(r.id) === Number(repoId)) ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 契约形状的仓库列表:listRepos 的原始行 → 对外字段。 */
|
|
162
|
+
function listRepoFacts() {
|
|
163
|
+
return listRepos().map((row) => repoFacts(row));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 仓库的契约字段(按 id/name,给 /choro/page 这类已知单仓的读路径用)。 */
|
|
167
|
+
function repoFactsById(repoId) {
|
|
168
|
+
const row = listRepoRow(repoId);
|
|
169
|
+
return row ? repoFacts(row) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 按绝对路径找已登记仓库(面板「选目录 → 生成」必需):T0 apply 落库时存的是
|
|
174
|
+
* realpath,所以这里也必须先 realpath 再比,否则 Windows 上盘符大小写/短名/符号
|
|
175
|
+
* 链接会让同一个目录被当成新仓库重复登记一次。
|
|
176
|
+
*/
|
|
177
|
+
function repoByRoot(rootPath) {
|
|
178
|
+
const raw = String(rootPath ?? "").trim();
|
|
179
|
+
if (raw === "") return null;
|
|
180
|
+
let normalized;
|
|
181
|
+
try { normalized = realpathSync(resolve(raw)); } catch { return null; }
|
|
182
|
+
return db.prepare("SELECT * FROM choro_repos WHERE root_path = ?").get(normalized) ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
58
185
|
/** repo 解析:数字 id 或显示名(root_path 是本机私有路径,不作为对外键)。 */
|
|
59
186
|
function resolveRepo(ref) {
|
|
60
187
|
if (ref === undefined || ref === null || ref === "") {
|
|
@@ -82,6 +209,12 @@ export function createChoroQueries({ store }) {
|
|
|
82
209
|
`SELECT from_slug, to_slug, weight FROM choro_edges WHERE repo_id = ?
|
|
83
210
|
ORDER BY from_slug, to_slug`
|
|
84
211
|
).all(repoId);
|
|
212
|
+
// 纯加法补一个 camelCase 的 relPath:面板从地志页导入知识点要拼「根路径 + 页相对
|
|
213
|
+
// 路径」,而这里历史上直出 SQL 行的 snake_case(page_rel_path)。C 线按契约读的是
|
|
214
|
+
// mod.relPath,字段一缺就恒 undefined,用户选完模块点导入只会看到「请先选模块」。
|
|
215
|
+
// 原字段一律保留——src/tools.js 的 render 与 choro-command 都还在吃 file_count /
|
|
216
|
+
// page_status,改形状会波及它们;outline() 里的同名映射是这件事的正确写法。
|
|
217
|
+
for (const m of modules) m.relPath = m.page_rel_path ?? null;
|
|
85
218
|
return { modules, edges };
|
|
86
219
|
}
|
|
87
220
|
|
|
@@ -255,6 +388,152 @@ export function createChoroQueries({ store }) {
|
|
|
255
388
|
};
|
|
256
389
|
}
|
|
257
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Claim 粒度的失效清单(借 openwiki 的 Grounded Claims):把「仓库代码变过就整仓过期」
|
|
393
|
+
* 降成「这 2 页里的 5 条结论被改动碰过」。
|
|
394
|
+
*
|
|
395
|
+
* 为什么放读侧而不写成后台任务:判据是纯机械的(git diff 的 hunk 区间 × claims 的
|
|
396
|
+
* 三列证据),不需要模型,一次 diff 几十毫秒——面板打开时现算,永远比任何落库的
|
|
397
|
+
* 缓存新鲜。写路径一条不加,红线不动。
|
|
398
|
+
*
|
|
399
|
+
* **比对起点是每页自己的 verified_baseline,不是全仓 git_baseline。** claims 的行号
|
|
400
|
+
* 是那页被富化的那一刻记下的;全仓基线是 T0 的时点。若 T1 晚于 T0(常态),用全仓
|
|
401
|
+
* 基线起算就会把「T1 之前就发生、claim 早已算进去」的改动也算进 diff,误报 touched。
|
|
402
|
+
* 所以按 verified_baseline 分组、每组各跑一次 git diff(同一起点只跑一次)。
|
|
403
|
+
*
|
|
404
|
+
* 五种「说不清」要分开说,别混成一个空清单:
|
|
405
|
+
* - repo 不存在 → null(调用方 404)
|
|
406
|
+
* - git 读不到 HEAD → 全仓 anchorStale + reason "git-unreadable"
|
|
407
|
+
* - 某页基线认不出(浅克隆/force-push)→ 该页 anchorStale,**其余页照常判**
|
|
408
|
+
* - 某页没记过验证基线(老数据/拿不到 HEAD)→ 该页 baselineUnknown,一条不判
|
|
409
|
+
* - 起点 == HEAD → 那组真没变,clean
|
|
410
|
+
*/
|
|
411
|
+
function claimDrift(repoId) {
|
|
412
|
+
const repo = db.prepare("SELECT id, root_path, name, git_baseline FROM choro_repos WHERE id = ?").get(repoId);
|
|
413
|
+
if (!repo) return null;
|
|
414
|
+
const rootPath = String(repo.root_path ?? "");
|
|
415
|
+
const rows = db.prepare(
|
|
416
|
+
`SELECT c.id, c.statement, c.kind, c.source_path, c.start_line, c.end_line,
|
|
417
|
+
m.slug AS page_slug, m.title AS page_title, p.rel_path
|
|
418
|
+
FROM choro_claims c
|
|
419
|
+
JOIN choro_pages p ON c.page_id = p.id
|
|
420
|
+
JOIN choro_modules m ON p.module_id = m.id
|
|
421
|
+
WHERE m.repo_id = ? ORDER BY m.slug, c.id`
|
|
422
|
+
).all(repoId);
|
|
423
|
+
// 页面清单单算:hasT1 只能读盘(标记写在正文里,库里没有这一列);验证基线在库里。
|
|
424
|
+
const pageRows = db.prepare(
|
|
425
|
+
`SELECT m.slug, m.title, p.rel_path, p.verified_baseline, p.verified_at
|
|
426
|
+
FROM choro_pages p
|
|
427
|
+
JOIN choro_modules m ON p.module_id = m.id WHERE m.repo_id = ? ORDER BY m.slug`
|
|
428
|
+
).all(repoId);
|
|
429
|
+
const head = rootPath === "" ? null : gitHead(rootPath);
|
|
430
|
+
const baseline = String(repo.git_baseline ?? "").trim();
|
|
431
|
+
// 每组验证基线只跑一次 diff:富化往往成批完成,同一基线的页占多数。
|
|
432
|
+
const diffCache = new Map();
|
|
433
|
+
const diffFor = (verified) => {
|
|
434
|
+
if (head === null) return null;
|
|
435
|
+
const key = String(verified ?? "").trim();
|
|
436
|
+
if (key === "") return null; // 没记过起点 → 判不了,别拿别的页的起点凑
|
|
437
|
+
if (diffCache.has(key)) return diffCache.get(key);
|
|
438
|
+
let value = null;
|
|
439
|
+
if (key === head) value = { ok: true, files: new Map(), deleted: [] };
|
|
440
|
+
else {
|
|
441
|
+
const diff = diffSince(rootPath, key, head);
|
|
442
|
+
value = diff.ok ? diff : { ok: false, reason: diff.reason };
|
|
443
|
+
}
|
|
444
|
+
diffCache.set(key, value);
|
|
445
|
+
return value;
|
|
446
|
+
};
|
|
447
|
+
const pages = pageRows.map((p) => {
|
|
448
|
+
let hasT1 = false;
|
|
449
|
+
try {
|
|
450
|
+
hasT1 = readFileSync(join(rootPath, p.rel_path), "utf8").includes(T1_SECTION_START);
|
|
451
|
+
} catch { hasT1 = false; }
|
|
452
|
+
const verifiedBaseline = String(p.verified_baseline ?? "").trim() || null;
|
|
453
|
+
const verifiedAt = p.verified_at ?? null;
|
|
454
|
+
// 老数据(本功能之前富化的页)只有全仓基线可依。退到它是**保守**方向:它早于
|
|
455
|
+
// 那页被富化的时刻,diff 会把「T1 之前就发生」的改动也算进来,只会多报不会漏报。
|
|
456
|
+
// 记 baselineApprox 是为了面板能说清「这是退而求其次的起点」,不是当真的验证点。
|
|
457
|
+
//
|
|
458
|
+
// 但「验证过、却没记下基线」不是老数据,是**验证没留下凭据**(T1 那次富化写了
|
|
459
|
+
// verified_at 却没写 verified_baseline,比如中途失败)。这种情况退到全仓基线
|
|
460
|
+
// 就会把「富化之后才发生」的改动当成「富化之前就有的」而放过——方向是漏报,
|
|
461
|
+
// 与上面那条恰好相反。所以判成 baselineUnknown:不知道起点,就不装作知道。
|
|
462
|
+
const verifyWithoutBaseline = verifiedBaseline === null && verifiedAt !== null;
|
|
463
|
+
const effective = verifyWithoutBaseline ? null : (verifiedBaseline ?? (baseline || null));
|
|
464
|
+
const computed = diffFor(effective);
|
|
465
|
+
return {
|
|
466
|
+
slug: p.slug,
|
|
467
|
+
title: p.title,
|
|
468
|
+
hasT1,
|
|
469
|
+
verifiedBaseline,
|
|
470
|
+
verifiedAt,
|
|
471
|
+
diffBaseline: effective,
|
|
472
|
+
baselineApprox: !verifyWithoutBaseline && verifiedBaseline === null && effective !== null,
|
|
473
|
+
baselineUnknown: effective === null,
|
|
474
|
+
anchorStale: computed !== null && computed.ok === false,
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
const claims = rows.map((r) => ({
|
|
478
|
+
id: r.id,
|
|
479
|
+
statement: r.statement,
|
|
480
|
+
kind: r.kind,
|
|
481
|
+
sourcePath: r.source_path,
|
|
482
|
+
startLine: r.start_line,
|
|
483
|
+
endLine: r.end_line,
|
|
484
|
+
pageSlug: r.page_slug,
|
|
485
|
+
pageTitle: r.page_title,
|
|
486
|
+
}));
|
|
487
|
+
const base = {
|
|
488
|
+
repo: { id: repo.id, name: repo.name, rootPath, gitBaseline: baseline || null },
|
|
489
|
+
head: head ?? null,
|
|
490
|
+
baseline: baseline || null,
|
|
491
|
+
};
|
|
492
|
+
if (head === null) {
|
|
493
|
+
return { ...base, clean: false, anchorStale: true, reason: "git-unreadable", ...summarizeClaims(claims, null, { pages }) };
|
|
494
|
+
}
|
|
495
|
+
// 仓库级 anchorStale 保留原来的语义:**骨架**(T0 页的文件清单、模块划分)记下的
|
|
496
|
+
// 那次基线还认不认得出。它和逐页 claims 的起点是两件事,面板上也是两句话——
|
|
497
|
+
// 「骨架要重建」不等于「结论都过时」。两条都算,谁也不掩盖谁。
|
|
498
|
+
let repoStale = false;
|
|
499
|
+
let repoStaleReason = null;
|
|
500
|
+
if (baseline !== "" && baseline !== head) {
|
|
501
|
+
const repoDiff = diffSince(rootPath, baseline, head);
|
|
502
|
+
if (!repoDiff.ok) { repoStale = true; repoStaleReason = repoDiff.reason; }
|
|
503
|
+
}
|
|
504
|
+
const diffByPage = (slug) => {
|
|
505
|
+
const page = pages.find((p) => p.slug === slug);
|
|
506
|
+
if (!page) return null;
|
|
507
|
+
const computed = diffFor(page.diffBaseline);
|
|
508
|
+
return computed !== null && computed.ok ? { files: computed.files, deleted: computed.deleted } : null;
|
|
509
|
+
};
|
|
510
|
+
const summary = summarizeClaims(claims, null, { pages, diffByPage });
|
|
511
|
+
// clean 的判据:每条结论都真判过、且一条都没被碰到。
|
|
512
|
+
// 「有结论但起点认不出 / 没记过」不算 clean——那是不知道,不是没事(#607 的病根);
|
|
513
|
+
// 一条结论都没有时也没有「没事」可说(那是「还没富化」,不是「全部干净」)。
|
|
514
|
+
// 仓库基线 == HEAD → 整仓一条提交都没发生,任何 claims 都不可能被碰过。这一条既短路
|
|
515
|
+
// 「逐页基线没记」的老数据(缺列不是判不了),也覆盖「刚跑完 T0、还没有 conclusion」
|
|
516
|
+
// 的情形:没变就是没变,不必等富化完才敢说。
|
|
517
|
+
// 不是这个情形时才逐页看:有哪页判不了就不说 clean,一条结论都没有也不说 clean
|
|
518
|
+
// (那是「还没富化」,不是「全部干净」——面板上这两句不一样)。
|
|
519
|
+
const repoAtHead = baseline !== "" && baseline === head;
|
|
520
|
+
const undecidable = summary.totals.baselineUnknownPages + summary.totals.anchorStalePages > 0;
|
|
521
|
+
// inconsistentPages 不参与 repoAtHead 的短路:它说的不是「起点认不出」,而是
|
|
522
|
+
// **页面有语义正文、库里却一条 claims 都没有**(上次 T1 没写完的残局)。这种页的
|
|
523
|
+
// 证据根本无法核对,与仓库有没有新提交无关;只要还有这种页,就不能说「全部干净」,
|
|
524
|
+
// 否则面板会把一次没落库的富化说成没事。
|
|
525
|
+
const clean = summary.totals.atRiskClaims === 0
|
|
526
|
+
&& summary.totals.inconsistentPages === 0
|
|
527
|
+
&& (repoAtHead || (summary.totals.claims > 0 && !undecidable && !repoStale));
|
|
528
|
+
return {
|
|
529
|
+
...base,
|
|
530
|
+
clean,
|
|
531
|
+
anchorStale: repoStale,
|
|
532
|
+
reason: repoStale ? repoStaleReason : null,
|
|
533
|
+
...summary,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
258
537
|
// --- chorography 向量(收尾档 P37–P39)---------------------------------
|
|
259
538
|
// 惰性物化(P37):只在 host 侧检索路径调用(embedder 由 index.js 注入);
|
|
260
539
|
// CLI 环境没有 embedder → 向量永不物化,检索退关键词。staleness 两维:
|
|
@@ -354,7 +633,8 @@ export function createChoroQueries({ store }) {
|
|
|
354
633
|
}
|
|
355
634
|
|
|
356
635
|
return {
|
|
357
|
-
listRepos,
|
|
636
|
+
listRepos, listRepoFacts, repoFacts, repoFactsById, repoFromRow, listRepoRow, repoByRoot,
|
|
637
|
+
resolveRepo, moduleMap, readPage, search, repoByCwd, outline, claimDrift,
|
|
358
638
|
embedPageVectors, searchSemantic, embedQueryText, semanticTopN,
|
|
359
639
|
};
|
|
360
640
|
}
|
package/lib/wiki/render.js
CHANGED
|
@@ -23,6 +23,80 @@ export const HUMAN_END = "<!-- CHORO:HUMAN:END -->";
|
|
|
23
23
|
export const T1_SECTION_START = "<!-- CHORO:T1:START -->";
|
|
24
24
|
export const T1_SECTION_END = "<!-- CHORO:T1:END -->";
|
|
25
25
|
|
|
26
|
+
// 验证溯源的两个 frontmatter 键(issue #615 之后的 ①:语义段带上「对着哪次提交验证的」)。
|
|
27
|
+
// 只有 T1 写它们,T0 渲染时一个字节都不产出——骨架页没有语义段,也就没有「哪次验证的」
|
|
28
|
+
// 这回事;反过来说,T0 重建整页时这两个键随 T1 正文一起消失,与「T0 丢弃语义段」是
|
|
29
|
+
// 同一件事,不新增语义。放在这里而不是 t1.js:frontmatter 的形状属于页面格式。
|
|
30
|
+
export const FM_VERIFIED_BASELINE = "verifiedBaseline";
|
|
31
|
+
export const FM_VERIFIED_AT = "verifiedAt";
|
|
32
|
+
|
|
33
|
+
/** frontmatter 边界:首行 --- 与下一个 --- 的行号;不存在返回 null。 */
|
|
34
|
+
function frontmatterBounds(lines) {
|
|
35
|
+
if (String(lines[0] ?? "").trim() !== "---") return null;
|
|
36
|
+
for (let i = 1; i < lines.length; i++) {
|
|
37
|
+
if (String(lines[i]).trim() === "---") return { open: 0, close: i };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 取 frontmatter 里某个顶层键的字符串值。
|
|
44
|
+
*
|
|
45
|
+
* 只认顶层键(`key: value`),缩进的列表项(` - x`)不会误命中;值两侧的引号剥掉
|
|
46
|
+
* (yamlScalar 会给含特殊字符的值加引号,读回来要还原成原文)。
|
|
47
|
+
* 没有 frontmatter、键不存在、值为空 → 一律 null:调用方据此说「没记下」,
|
|
48
|
+
* 而不是把空串当成一个合法的提交号。
|
|
49
|
+
*/
|
|
50
|
+
export function readFrontmatterKey(pageText, key) {
|
|
51
|
+
const lines = String(pageText ?? "").split("\n");
|
|
52
|
+
const bounds = frontmatterBounds(lines);
|
|
53
|
+
if (!bounds) return null;
|
|
54
|
+
const re = new RegExp(`^${escapeRegExp(key)}\\s*:\\s*(.*)$`);
|
|
55
|
+
for (let i = bounds.open + 1; i < bounds.close; i++) {
|
|
56
|
+
const m = re.exec(lines[i]);
|
|
57
|
+
if (!m) continue;
|
|
58
|
+
let v = m[1].trim();
|
|
59
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
60
|
+
try { v = JSON.parse(v); } catch { v = v.slice(1, -1); }
|
|
61
|
+
}
|
|
62
|
+
return v === "" ? null : v;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 就地设置 frontmatter 顶层键:有则覆盖、无则追加在收尾 --- 之前(按传入顺序)。
|
|
69
|
+
*
|
|
70
|
+
* value 为 null / undefined / 空串 = **删掉**这个键,不留 `key:` 空行——「这一页是哪次
|
|
71
|
+
* 提交验证的,T1 没记下来」本身要能在文件里查得出来,而不是显示成一片空白。
|
|
72
|
+
* 没有 frontmatter 的正文原样返回:绝不给人写的坏文件凭空造一个块。
|
|
73
|
+
*
|
|
74
|
+
* @param {string} pageText
|
|
75
|
+
* @param {Array<[string, string|null]>} entries
|
|
76
|
+
*/
|
|
77
|
+
export function setFrontmatterKeys(pageText, entries) {
|
|
78
|
+
const lines = String(pageText ?? "").split("\n");
|
|
79
|
+
const bounds = frontmatterBounds(lines);
|
|
80
|
+
if (!bounds) return String(pageText ?? "");
|
|
81
|
+
const pending = new Map(entries.map(([k, v]) => [k, v === undefined || v === null || String(v).trim() === "" ? null : String(v)]));
|
|
82
|
+
const out = lines.slice(0, bounds.open + 1);
|
|
83
|
+
for (let i = bounds.open + 1; i < bounds.close; i++) {
|
|
84
|
+
const m = /^([A-Za-z_][\w-]*)\s*:/.exec(lines[i]);
|
|
85
|
+
if (m && pending.has(m[1])) {
|
|
86
|
+
const v = pending.get(m[1]);
|
|
87
|
+
pending.delete(m[1]);
|
|
88
|
+
if (v !== null) out.push(`${m[1]}: ${v}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
out.push(lines[i]);
|
|
92
|
+
}
|
|
93
|
+
for (const [k, v] of pending) {
|
|
94
|
+
if (v !== null) out.push(`${k}: ${v}`);
|
|
95
|
+
}
|
|
96
|
+
out.push(...lines.slice(bounds.close));
|
|
97
|
+
return out.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
26
100
|
/** 抽出全部保留区。返回 { blocks: string[](含标记本身,原样), count }。 */
|
|
27
101
|
export function extractHumanBlocks(pageText) {
|
|
28
102
|
if (typeof pageText !== "string") return { blocks: [], count: 0 };
|
package/lib/wiki/t0.js
CHANGED
|
@@ -39,9 +39,10 @@ export class ChoroRefusal extends Error {
|
|
|
39
39
|
* @param {string} opts.repoRoot 仓库路径(内部 realpath)
|
|
40
40
|
* @param {object|null} opts.store createStore 产物(apply 时必传;dry-run 可为 null)
|
|
41
41
|
* @param {boolean} [opts.apply=false] false = dry-run(零写入)
|
|
42
|
+
* @param {boolean} [opts.guardT1Loss=false] 会丢掉已有语义段时整轮拒绝(自动刷新用)
|
|
42
43
|
* @param {() => string} [opts.now] ISO 时刻(测试注入假时钟)
|
|
43
44
|
*/
|
|
44
|
-
export function runChoroT0({ repoRoot, store = null, apply = false, now = () => new Date().toISOString() }) {
|
|
45
|
+
export function runChoroT0({ repoRoot, store = null, apply = false, guardT1Loss = false, now = () => new Date().toISOString() }) {
|
|
45
46
|
let rootPath;
|
|
46
47
|
try {
|
|
47
48
|
rootPath = realpathSync(resolve(repoRoot));
|
|
@@ -177,6 +178,36 @@ export function runChoroT0({ repoRoot, store = null, apply = false, now = () =>
|
|
|
177
178
|
+ "要留住语义段就别在这里重跑 T0;需要时再用 --t1 重新富化。"
|
|
178
179
|
);
|
|
179
180
|
}
|
|
181
|
+
if (t1Loss.unreadable.length > 0) {
|
|
182
|
+
warnings.push(
|
|
183
|
+
`${t1Loss.unreadable.length} 页读不出来`
|
|
184
|
+
+ `(${t1Loss.unreadable.slice(0, 3).map((u) => u.relPath).join("、")}${t1Loss.unreadable.length > 3 ? " 等" : ""}),`
|
|
185
|
+
+ "无法确认其中是否有 T1 语义段;自动刷新会因此拒绝,手动重建会按骨架重写这些页。"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// guardT1Loss(自动刷新专用):读路径上「顺手」重跑 T0 会把用户花模型调用换来的
|
|
189
|
+
// 语义段和 claims 一起清掉——那不是顺手,那是在用户只点开一页的时候替他做了一次
|
|
190
|
+
// 破坏性写盘。手动重建不受此限:用户点了按钮,回执里的 warnings 会如实列出丢了什么。
|
|
191
|
+
//
|
|
192
|
+
// 拒绝而不是「跳过有语义段的那几页」:T0 的整仓 replaceChoroGraph 是一次事务,
|
|
193
|
+
// 只保几页会留下「页面有 T1 正文、库里 claims 已被清空」的半截状态,比整体不写更糟。
|
|
194
|
+
//
|
|
195
|
+
// 读不出来的页同样要拒绝:清点靠读盘,读失败就先不知道它有没有语义段。放行等于
|
|
196
|
+
// 赌它没有,赌输就是把它那份语义段和 claims 一起抹掉;拒绝只是这次不刷新。
|
|
197
|
+
if (guardT1Loss && (t1Loss.pages.length > 0 || t1Loss.unreadable.length > 0)) {
|
|
198
|
+
const names = t1Loss.pages.slice(0, 3).join("、");
|
|
199
|
+
const blockedBy = t1Loss.unreadable.length > 0
|
|
200
|
+
? `另有 ${t1Loss.unreadable.length} 页读不出来(${t1Loss.unreadable.slice(0, 3).map((u) => u.relPath).join("、")}`
|
|
201
|
+
+ `${t1Loss.unreadable.length > 3 ? " 等" : ""}),无法确认它们有没有语义段。`
|
|
202
|
+
: "";
|
|
203
|
+
throw new ChoroRefusal(
|
|
204
|
+
"would-drop-t1",
|
|
205
|
+
`这个仓库有 ${t1Loss.pages.length} 页已经富化过(${names}${t1Loss.pages.length > 3 ? " 等" : ""})。`
|
|
206
|
+
+ blockedBy
|
|
207
|
+
+ "自动刷新会退回骨架、清空 claims,所以这次没有动:"
|
|
208
|
+
+ "要保留语义段就先别刷新,确实要重建骨架请在面板里手动点「重建骨架」(回执会列出丢了哪几页)。"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
180
211
|
|
|
181
212
|
if (!apply) {
|
|
182
213
|
return {
|
|
@@ -196,7 +227,7 @@ export function runChoroT0({ repoRoot, store = null, apply = false, now = () =>
|
|
|
196
227
|
skippedFiles: graph.skipped,
|
|
197
228
|
unresolvedCount: unresolvedTotal,
|
|
198
229
|
humanBlocksSeen: countExistingHumanBlocks(rootPath, pageTargets),
|
|
199
|
-
t1Loss: { pages: t1Loss.pages, orphanEvidence: t1Loss.orphanEvidence },
|
|
230
|
+
t1Loss: { pages: t1Loss.pages, unreadable: t1Loss.unreadable, orphanEvidence: t1Loss.orphanEvidence },
|
|
200
231
|
wouldWrite: [...pageTargets.map((p) => ({ path: p.relPath, bytes: Buffer.byteLength(p.content, "utf8") })),
|
|
201
232
|
{ path: indexRelPath, bytes: Buffer.byteLength(indexContent, "utf8") },
|
|
202
233
|
{ path: `${CHORO_DIR}/state.json`, bytes: 0 }],
|
|
@@ -282,7 +313,7 @@ export function runChoroT0({ repoRoot, store = null, apply = false, now = () =>
|
|
|
282
313
|
unresolvedCount: unresolvedTotal,
|
|
283
314
|
humanBlocksPreserved,
|
|
284
315
|
bytes,
|
|
285
|
-
t1Loss: { pages: t1Loss.pages, orphanEvidence: t1Loss.orphanEvidence },
|
|
316
|
+
t1Loss: { pages: t1Loss.pages, unreadable: t1Loss.unreadable, orphanEvidence: t1Loss.orphanEvidence },
|
|
286
317
|
warnings,
|
|
287
318
|
moduleSummary: modules.map((m) => ({
|
|
288
319
|
slug: m.slug, title: m.title, fileCount: m.fileCount,
|
|
@@ -421,13 +452,19 @@ function existingHumanBlockCount(absPath) {
|
|
|
421
452
|
// 引用者。两者都是「会失去什么」的清点,不构成阻止:丢弃是设计意图,静默才是 bug。
|
|
422
453
|
function collectT1Loss(rootPath, pageTargets) {
|
|
423
454
|
const pages = [];
|
|
455
|
+
const unreadable = [];
|
|
424
456
|
for (const target of pageTargets) {
|
|
425
457
|
const abs = join(rootPath, target.relPath);
|
|
426
458
|
if (!existsSync(abs)) continue;
|
|
427
459
|
try {
|
|
428
460
|
if (readFileSync(abs, "utf8").includes(T1_SECTION_START)) pages.push(target.relPath);
|
|
429
|
-
} catch {
|
|
430
|
-
|
|
461
|
+
} catch (err) {
|
|
462
|
+
// **读不了不等于没有**:权限、占用、编码错误都会走到这里,而「有没有 T1 语义段」
|
|
463
|
+
// 恰恰要靠读那一眼才知道。吞成「没有」的直接后果是 guard 放行,然后一整仓
|
|
464
|
+
// replaceChoroGraph 把读不到的页连同它可能有的语义段一起退回骨架、清空 claims。
|
|
465
|
+
// 清点本身不该让 T0 失败(下面 warnings 只如实记一笔),但**守卫**必须把它当成
|
|
466
|
+
// 「有会丢的东西、但我数不清」而拒绝——见 guardT1Loss 分支。
|
|
467
|
+
unreadable.push({ relPath: target.relPath, reason: String(err?.code ?? err?.message ?? err) });
|
|
431
468
|
}
|
|
432
469
|
}
|
|
433
470
|
let orphanEvidence = 0;
|
|
@@ -437,7 +474,7 @@ function collectT1Loss(rootPath, pageTargets) {
|
|
|
437
474
|
} catch {
|
|
438
475
|
orphanEvidence = 0;
|
|
439
476
|
}
|
|
440
|
-
return { pages, orphanEvidence };
|
|
477
|
+
return { pages, unreadable, orphanEvidence };
|
|
441
478
|
}
|
|
442
479
|
|
|
443
480
|
function countExistingHumanBlocks(rootPath, pageTargets) {
|