fix(ticket-management): 采纳复核 —— 段位词表补全 + 评论可执行物提示 + 顺序如实判定

复核回执 R-20260924-103820-a8f2(现状梳理)逐条实跑核对后落地。

【采纳】
- 词表按 #157/#159 真实小节名补齐:`结论`(结论即提案)→ 提案 · `生产实测` → 背景 · `基线` → 效果;
  `取证记录` 进「其他已识别小节」(原报「未归类」是误报)。
- 去掉「更正 / 补充」特例 ⇒ 顺序改按主叙事**如实**判定(复核指出 `## 影响面(实测更正)`
  被错列成「后置小节」);提示语写明「确为更正小节后置时可保留,由人判断」。
- 缺「提案」的措辞给出路:提示「若『结论即提案』或结论为『无需处置』,请显式写一段声明」。
- 评论清单加 `[可执行]` / `[? 非可执行]` 列(启发式:代码围栏或 SQL 关键字)⇒
  「评论只应剩必须可执行物」由人肉核变为机器提示 + 人核;SKILL §8.1/§8.3 同步。
- `selftest` 13 → **20** 条(补 结论即提案 / 基线归效果 / 取证记录 / 可执行物三态 / 方案明细不被「实测」抢走)。

【自查中自己踩的坑(已修 + 加回归用例)】
- 首版把裸词 `实测` 加进背景词表 ⇒ `## 方案明细(…实测代价)` 被抢归「问题背景」,`#158` 误报「顺序 ⚠」。
  改为 `生产实测`,并加 selftest 锁死该回归。

【不采纳(附理由)】
- 「`闸门=不处置` 作为提案的替代」:闸门是断言 4 的受控词表(等口径 / 等取证 / 等部署),
  新增取值属口径变更且会使「不能开工」语义漂移 ⇒ 改用「`## 结论` 可替代提案 + 正文显式声明」,
  **不动闸门词表**;确要加取值请用户拍板。

【验证 · E1 · 本机】
- `node --check` 通过;`selftest` **20/20**;`verify 157 158 159` = **21/21**。
- `assess`:#158 四段全 ✓ · 顺序 OK · 空壳 无 · 两条评论均判 `[可执行]`;
  #157 四段(结果 / 效果未发生 ⇒ 不长空壳)· 顺序 OK;
  #159 ⇒ **顺序 ⚠**(`影响面` 落在 `机制` 之后)—— 如实判定,前移或按更正小节保留由属主定。
This commit is contained in:
2026-09-24 10:44:07 +08:00
parent 79a9c13c91
commit feeafbd70d
2 changed files with 46 additions and 24 deletions
+37 -18
View File
@@ -14,12 +14,13 @@ const BAD = ["codes/", "分析/数据/", "分析/D-", "分析/E-", "分析/A-",
// §8.1「任务全貌」四段:正文骨架词表(数组顺序 = 规范顺序)
const SECTIONS = [
{ key: "问题背景", words: ["现象", "根因", "影响面", "问题背景", "背景"], need: "必备" },
{ key: "处置提案", words: ["处置提案", "方案", "机制", "提案", "路线"], need: "必备" },
{ key: "问题背景", words: ["现象", "根因", "影响面", "问题背景", "背景", "生产实测"], need: "必备" },
{ key: "处置提案", words: ["处置提案", "方案", "机制", "提案", "路线", "结论"], need: "必备" },
{ key: "处置结果", words: ["处置结果", "执行回执", "当前状态", "回执", "结果"], need: "按事实" },
{ key: "效果对比", words: ["效果对比", "收益验证", "收益", "对照", "效果"], need: "按事实" }
{ key: "效果对比", words: ["效果对比", "收益验证", "收益", "对照", "效果", "基线"], need: "按事实" }
];
const OPTIONAL_SECTIONS = ["验收判据", "反证条件", "遗留"];
// 四段之外「已识别的小节」:不计入四段,但也不该被报成「未归类」
const OPTIONAL_SECTIONS = ["验收判据", "反证条件", "遗留", "取证记录"];
// 取值型选项:`--repo=o/r` 与 `--repo o/r` 两种写法都收(等价;布尔开关不吃后一个参数)
const VALUE_OPTS = ["repo", "title", "body-file", "label", "milestone", "token-file", "host", "state", "limit", "domain", "gate", "dump"];
@@ -100,7 +101,6 @@ function sectionIndex(text) {
}
return best;
}
function isPostNote(text) { return /更正|订正|勘误|补充|追加/.test(text); }
function isOptional(text) { return OPTIONAL_SECTIONS.some(function (w) { return text.indexOf(w) >= 0; }); }
function structureOf(body) {
const lines = humanPart(body).split(NL);
@@ -119,16 +119,15 @@ function structureOf(body) {
if (!inner) empty.push(lines[heads[k].pos].trim());
}
// 归类只看 ## 级;「更正 / 补充」小节允许后置,不计入顺序
// 归类只看 ## 级;顺序按「主叙事」如实判定(「更正 / 补充」小节若后置会如实报出,由人判断)
const tops = heads.filter(function (h) { return h.level === 2; })
.map(function (h) { return { text: h.text, idx: sectionIndex(h.text), post: isPostNote(h.text) }; });
const main = tops.filter(function (t) { return !t.post && t.idx >= 0; });
const order = main.map(function (t) { return t.idx; });
const ordered = order.every(function (v, i) { return i === 0 || order[i - 1] <= v; });
.map(function (h) { return { text: h.text, idx: sectionIndex(h.text) }; });
const hits = tops.filter(function (t) { return t.idx >= 0; });
const postNotes = tops.filter(function (t) { return t.post; }).map(function (t) { return t.text; });
const unclassified = tops.filter(function (t) { return t.idx < 0 && !t.post && !isOptional(t.text); }).map(function (t) { return t.text; });
const order = hits.map(function (t) { return t.idx; });
const ordered = order.every(function (v, i) { return i === 0 || order[i - 1] <= v; });
const unclassified = tops.filter(function (t) { return t.idx < 0 && !isOptional(t.text); }).map(function (t) { return t.text; });
const optional = OPTIONAL_SECTIONS.filter(function (w) { return heads.some(function (h) { return h.text.indexOf(w) >= 0; }); });
return { hits: hits, ordered: ordered, empty: empty, optional: optional, postNotes: postNotes, unclassified: unclassified };
return { hits: hits, ordered: ordered, empty: empty, optional: optional, unclassified: unclassified };
}
function printStructure(body) {
const st = structureOf(body);
@@ -136,15 +135,21 @@ function printStructure(body) {
for (let i = 0; i < SECTIONS.length; i++) {
const own = st.hits.filter(function (x) { return x.idx === i; }).map(function (x) { return "## " + x.text; });
const tail = own.length ? " " + own.join(" · ")
: (SECTIONS[i].need === "必备" ? " ← 未识别到(必备段:确认是否用了同义小标题,或确实缺)" : " ← 未识别到(未发生 / 未测就不写,不要空壳)");
: (SECTIONS[i].need === "必备" ? " ← 未识别到(必备段:确认是否用了同义小标题;若「结论即提案」或结论为「无需处置」,请显式写一段声明)" : " ← 未识别到(未发生 / 未测就不写,不要空壳)");
console.log(" " + (own.length ? "[✓]" : "[ ]") + " " + SECTIONS[i].key + "(" + SECTIONS[i].need + ")" + tail);
}
console.log(" 顺序:" + (st.ordered ? "OK" : "⚠ 主叙事未按 背景→提案→结果→效果 排列 ⇒ 调整(更正 / 补充小节不计)"));
if (st.postNotes.length) console.log(" 后置小节(不计顺序):" + st.postNotes.map(function (t) { return "## " + t; }).join(" · "));
console.log(" 顺序:" + (st.ordered ? "OK" : "⚠ 主叙事未按 背景→提案→结果→效果 排列 ⇒ 调整(确为「更正 / 补充」后置小节时可保留,由人判断)"));
if (st.unclassified.length) console.log(" 未归类 ## 小节(人工判断归属):" + st.unclassified.map(function (t) { return "## " + t; }).join(" · "));
console.log(" 空壳段:" + (st.empty.length ? "✗ " + st.empty.join(" · ") + "(删掉或补齐)" : "无"));
console.log(" 可选段:" + (st.optional.length ? st.optional.join(" · ") : "无"));
console.log(" 其他已识别小节(不计入四段):" + (st.optional.length ? st.optional.join(" · ") : "无"));
}
// 「必须可执行物」的启发式判据(§8.1 第 2 条):含代码围栏或 SQL 关键字 —— 只作提示,仍需人核
const FENCE = String.fromCharCode(96, 96, 96);
function isExecutable(body) {
const b = String(body || "");
return b.indexOf(FENCE) >= 0 || /\b(SELECT|INSERT|UPDATE|DELETE|ALTER|CREATE|DROP|SHOW|EXPLAIN|ANALYZE|DESCRIBE|SET)\b/i.test(b);
}
// 自检(不联网、不需要令牌):node bin/ticket.mjs selftest
function cmdSelftest() {
let pass = 0, fail = 0;
@@ -177,6 +182,18 @@ function cmdSelftest() {
t("空壳段可被识别", sEmpty.empty.length === 1);
const sOrder = structureOf("域=平台-性能-索引" + NL + NL + "## 效果对比" + NL + "a" + NL + NL + "## 现象" + NL + "b");
t("乱序可被识别", sOrder.ordered === false);
// —— 复核采纳:结论即提案 / 基线归效果 / 取证记录不再误报 / 可执行物启发式 ——
const sConclusion = structureOf("域=平台-性能-分区" + NL + NL + "## 现象" + NL + "坏。" + NL + NL + "## 结论:与索引的分工" + NL + "分工。");
t("「结论即提案」可被识别为提案", sConclusion.hits.some(function (x) { return x.idx === 1; }) === true);
const sBaseline = structureOf("域=平台-性能-索引" + NL + NL + "## 现象" + NL + "x" + NL + NL + "## 生产基线 T0" + NL + "y");
t("「基线」可被识别为效果对比", sBaseline.hits.some(function (x) { return x.idx === 3; }) === true);
const sEvid = structureOf("域=平台-性能-索引" + NL + NL + "## 现象" + NL + "x" + NL + NL + "## 取证记录" + NL + "y");
t("「取证记录」不再报成未归类", sEvid.unclassified.length === 0 && sEvid.optional.indexOf("取证记录") >= 0);
const sDetail = structureOf("域=平台-性能-索引" + NL + NL + "## 现象" + NL + "x" + NL + NL + "## 方案明细(… 实测代价)" + NL + "y");
t("「方案明细(…实测代价)」归提案(不被『实测』抢走)", sDetail.hits.some(function (x) { return x.idx === 1; }) === true);
t("可执行物启发式:代码围栏", isExecutable(FENCE + "bash" + NL + "echo hi" + NL + FENCE) === true);
t("可执行物启发式:SQL 关键字", isExecutable("跑这条 SELECT 即可") === true);
t("可执行物启发式:过程叙述判非可执行", isExecutable("今天讨论了一下,暂时没有结论") === false);
console.log("自检 " + pass + "/" + (pass + fail));
return fail === 0 ? 0 : 1;
}
@@ -243,8 +260,9 @@ async function cmdAssess(api, opt, n) {
console.log("评论清单(" + comments.length + ")—— 只应剩「必须可执行物」(§8.1 第 2 条):");
for (const c of comments) {
const b = String(c.body || "");
console.log(" " + c.id + " " + b.length + " 字 首行: " + (b.split(NL)[0] || "").slice(0, 90));
console.log(" " + c.id + " " + b.length + " 字 " + (isExecutable(b) ? "[可执行]" : "[? 非可执行]") + " 首行: " + (b.split(NL)[0] || "").slice(0, 90));
}
console.log(" ([可执行] 为**启发式**:含代码围栏或 SQL 关键字 ⇒ 仍需人核)");
if (comments.length > 1) console.log(" ⚠ 多条评论 ⇒ 逐条过 §8.2;删前先备份:comments " + n + " --dump=<文件>(§8.1 第 5 条)");
}
console.log("下一步:把本轮候选信息逐条填进 §8.0 的落点表 ⇒ 再 create / update / comment-add。");
@@ -329,8 +347,9 @@ async function cmdComments(api, opt, n) {
console.log("#" + n + " 评论 " + arr.length + " 条(只应剩「必须可执行物」):");
for (const c of arr) {
const b = String(c.body || "");
console.log(" " + c.id + " " + b.length + " 字 首行: " + (b.split(NL)[0] || "").slice(0, 90));
console.log(" " + c.id + " " + b.length + " 字 " + (isExecutable(b) ? "[可执行]" : "[? 非可执行]") + " 首行: " + (b.split(NL)[0] || "").slice(0, 90));
}
console.log("([可执行] 为**启发式**:含代码围栏或 SQL 关键字 ⇒ 仍需人核)");
return 0;
}
async function cmdCommentAdd(api, opt, n) {