diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index a7a5a9ae..465a0812 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +- **wiki 面板三处「给人看」的口径修正:机器标记不再露、文件 status 与库同步、树的缩进看真父子(#582)**:chorography 的价值主要在「人看」,但面板把三件「给程序看的东西」也摆了出来。**① `` 等机器标记原样显示**——`renderWikiMarkdown` 没有 HTML 注释分支,t0/t1 用来剥段与回填的段落边界成了一行行裸文本;现在渲染层跳过「整行以 `` 注释行会被下面的隐藏规则 + // 删掉;四反引号块内部出现三反引号时旧写法也会提前退出代码态。 + // CommonMark:结束围栏必须与开启同字符、长度不小于开启、其后只有空白。 + let fence = null; let codeLines = []; // --- 表格(v0.11.1 美化):连续 `|` 行解析成真 table,不再按等宽行画竖线 --- const splitRow = (line) => String(line).trim().replace(/^\|/, "").replace(/\|$/, "") @@ -11627,14 +11632,31 @@ window.__ModuleLoader__.load({ }; for (; i < lines.length; i++) { const line = lines[i]; - if (line.trim().startsWith("```")) { - if (inCode) { + const fenceOpen = /^(`{3,}|~{3,})(.*)$/.exec(line.trim()); + if (!inCode && fenceOpen) { + flushList(); flushTable(); + inCode = true; + fence = { ch: fenceOpen[1][0], len: fenceOpen[1].length }; + codeLines = []; + continue; + } + if (inCode) { + if (fenceOpen && fenceOpen[1][0] === fence.ch && fenceOpen[1].length >= fence.len + && fenceOpen[2].trim() === "") { out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n"))); - codeLines = []; inCode = false; - } else { flushList(); flushTable(); inCode = true; } + codeLines = []; inCode = false; fence = null; + } else { + codeLines.push(line); + } continue; } - if (inCode) { codeLines.push(line); continue; } + // 机器标记行不进正文(#582):`` 与 + // `` 是 t0/t1 剥段与回填用的段落边界, + // 给程序看的;排版里它们是一行裸文本噪音。只隐藏「整行以 "; export const FM_VERIFIED_BASELINE = "verifiedBaseline"; export const FM_VERIFIED_AT = "verifiedAt"; +// 页面生命周期档(#582):status 也是 frontmatter 键,但它 T0 / T1 都写——T0 +// 起骨架时写 skeleton,T1 成功时随 verified 两键一起推进 enriched。定义在这 +// 与另两个键同住:frontmatter 的形状属于页面格式。 +export const FM_STATUS = "status"; + /** frontmatter 边界:首行 --- 与下一个 --- 的行号;不存在返回 null。 */ function frontmatterBounds(lines) { if (String(lines[0] ?? "").trim() !== "---") return null; diff --git a/dsh-mneme/lib/wiki/t1.js b/dsh-mneme/lib/wiki/t1.js index d7161caa..fffab37d 100644 --- a/dsh-mneme/lib/wiki/t1.js +++ b/dsh-mneme/lib/wiki/t1.js @@ -25,7 +25,7 @@ import { tmpdir } from "node:os"; import { realpathSync } from "node:fs"; import { extractHumanBlocks, injectHumanBlocks, T1_SECTION_START, T1_SECTION_END, - FM_VERIFIED_AT, FM_VERIFIED_BASELINE, setFrontmatterKeys, + FM_VERIFIED_AT, FM_VERIFIED_BASELINE, FM_STATUS, setFrontmatterKeys, } from "./render.js"; import { gitHead } from "./t0.js"; // issue #607:拒绝文案里的 CLI 命令要给绝对路径入口(profile 装的插件没有全局 @@ -566,6 +566,13 @@ function enrichPageFile(pageFile, sections, { verifiedBaseline = null, verifiedA const finalContent = setFrontmatterKeys(withHuman, [ [FM_VERIFIED_BASELINE, verifiedBaseline], [FM_VERIFIED_AT, verifiedAt], + // 文件里的 status 与库里的 choro_pages.status 同步推进(#582):此前 T1 + // 成功只改库(markChoroPageEnriched),文件那行永远停在 T0 写的 skeleton + // ——页面文件自身与数据库状态不一致,看文件的人会以为这页从没富化过。 + // 失败路径不写它:文件停在 skeleton 与「首次失败不标 failed」的口径一致 + // (recordChoroPageFailure 改的是库)。T0 重跑整页重写、status 回 skeleton, + // 与「T0 丢弃语义段」同一件事。 + [FM_STATUS, "enriched"], ]); writeFileSync(pageFile, finalContent, "utf8"); } diff --git a/dsh-mneme/src/wiki/render.js b/dsh-mneme/src/wiki/render.js index 9b70fd67..5affcf05 100644 --- a/dsh-mneme/src/wiki/render.js +++ b/dsh-mneme/src/wiki/render.js @@ -30,6 +30,11 @@ export const T1_SECTION_END = ""; export const FM_VERIFIED_BASELINE = "verifiedBaseline"; export const FM_VERIFIED_AT = "verifiedAt"; +// 页面生命周期档(#582):status 也是 frontmatter 键,但它 T0 / T1 都写——T0 +// 起骨架时写 skeleton,T1 成功时随 verified 两键一起推进 enriched。定义在这 +// 与另两个键同住:frontmatter 的形状属于页面格式。 +export const FM_STATUS = "status"; + /** frontmatter 边界:首行 --- 与下一个 --- 的行号;不存在返回 null。 */ function frontmatterBounds(lines) { if (String(lines[0] ?? "").trim() !== "---") return null; diff --git a/dsh-mneme/src/wiki/t1.js b/dsh-mneme/src/wiki/t1.js index d7161caa..fffab37d 100644 --- a/dsh-mneme/src/wiki/t1.js +++ b/dsh-mneme/src/wiki/t1.js @@ -25,7 +25,7 @@ import { tmpdir } from "node:os"; import { realpathSync } from "node:fs"; import { extractHumanBlocks, injectHumanBlocks, T1_SECTION_START, T1_SECTION_END, - FM_VERIFIED_AT, FM_VERIFIED_BASELINE, setFrontmatterKeys, + FM_VERIFIED_AT, FM_VERIFIED_BASELINE, FM_STATUS, setFrontmatterKeys, } from "./render.js"; import { gitHead } from "./t0.js"; // issue #607:拒绝文案里的 CLI 命令要给绝对路径入口(profile 装的插件没有全局 @@ -566,6 +566,13 @@ function enrichPageFile(pageFile, sections, { verifiedBaseline = null, verifiedA const finalContent = setFrontmatterKeys(withHuman, [ [FM_VERIFIED_BASELINE, verifiedBaseline], [FM_VERIFIED_AT, verifiedAt], + // 文件里的 status 与库里的 choro_pages.status 同步推进(#582):此前 T1 + // 成功只改库(markChoroPageEnriched),文件那行永远停在 T0 写的 skeleton + // ——页面文件自身与数据库状态不一致,看文件的人会以为这页从没富化过。 + // 失败路径不写它:文件停在 skeleton 与「首次失败不标 failed」的口径一致 + // (recordChoroPageFailure 改的是库)。T0 重跑整页重写、status 回 skeleton, + // 与「T0 丢弃语义段」同一件事。 + [FM_STATUS, "enriched"], ]); writeFileSync(pageFile, finalContent, "utf8"); } diff --git a/dsh-mneme/test/client.test.js b/dsh-mneme/test/client.test.js index 1da13c28..c728e64d 100644 --- a/dsh-mneme/test/client.test.js +++ b/dsh-mneme/test/client.test.js @@ -4681,3 +4681,44 @@ test("conflict ledger mode warns when freeze overrides the chosen tier (#661)", assert.match(joined, /\u77db\u76fe\u51bb\u7ed3|conflict freeze/i, "the notice must name the freeze switch so the user knows what to turn off"); }); + +// ---- #582 wiki 渲染三处 ---- + +// 机器标记行(CHORO:T1 / CHORO:HUMAN 的 HTML 注释包装)是 t0/t1 剥段与回填用 +// 的段落边界,给人看的排版里是裸文本噪音。跳过判定必须挂在围栏分支**之后**: +// 代码块里的 `\n人写的批注\n\n"; @@ -124,6 +127,9 @@ test("快乐路径:富化 + claims 双落 + 保留区照旧页尾回填", asyn assert.ok(rePage.includes("人写的批注"), "重富化不抹保留区"); assert.ok(rePage.trimEnd().endsWith(""), "保留区照旧页尾回填"); assert.equal(rePage.match(new RegExp(T1_SECTION_START.replace(/[/*]/g, (c) => "\\" + c), "g")).length, 1, "旧 T1 段被剥掉、只有一份新段"); + // #582:重富化走同一写入口,status 幂等覆写——不能重复插行,也不能退回。 + assert.ok(/^status: enriched$/m.test(rePage), "重富化后 status 仍是 enriched"); + assert.equal((rePage.match(/^status: /gm) || []).length, 1, "frontmatter 里 status 只有一行"); } finally { store.close(); rmTempBestEffort(root); @@ -142,6 +148,10 @@ test("输出契约:坏 JSON → 失败计数;连续两次 → status=failed assert.equal(r2.failed[0].marked, true, "连续两次到阈值"); const st = store.db.prepare("SELECT p.status FROM choro_pages p JOIN choro_modules m ON p.module_id=m.id WHERE m.slug='src'").get(); assert.equal(st.status, "failed"); + // #582 的另一面:失败路径不动文件——status 停在 T0 写的 skeleton,与 + // 「首次失败不标 failed(骨架仍是有效产物)」的库侧口径一致。 + assert.ok(!/^status: enriched/m.test(readFileSync(join(root, ".chorography", "modules", "src.md"), "utf8")), + "失败路径不能把文件 frontmatter 的 status 推到 enriched"); respond = GOOD_OUTPUT; const r3 = await t1.runT1({ repoRoot: root, moduleSlug: "src", retryFailed: true });