From feeafbd70df1ab80d7e22f279e70687acf9b24f1 Mon Sep 17 00:00:00 2001 From: XuXiang Date: Thu, 24 Sep 2026 10:44:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(ticket-management):=20=E9=87=87=E7=BA=B3?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=20=E2=80=94=E2=80=94=20=E6=AE=B5=E4=BD=8D?= =?UTF-8?q?=E8=AF=8D=E8=A1=A8=E8=A1=A5=E5=85=A8=20+=20=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=8F=AF=E6=89=A7=E8=A1=8C=E7=89=A9=E6=8F=90=E7=A4=BA=20+=20?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E5=A6=82=E5=AE=9E=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复核回执 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 ⇒ **顺序 ⚠**(`影响面` 落在 `机制` 之后)—— 如实判定,前移或按更正小节保留由属主定。 --- ticket-management/SKILL.md | 15 +++++---- ticket-management/bin/ticket.mjs | 55 +++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/ticket-management/SKILL.md b/ticket-management/SKILL.md index 973bcd9..cf2bad2 100644 --- a/ticket-management/SKILL.md +++ b/ticket-management/SKILL.md @@ -170,16 +170,19 @@ node bin/ticket.mjs rename 142 --repo=xbc/xbc --domain=平台 [--gate=等口径] | 段 | 回答的问题 | 典型小标题 | |---|---|---| - | **① 问题背景**(必备) | 谁 / 什么场景 / 什么问题 · 影响面 · 根因(含证据) | `## 现象` `## 根因` `## 影响面` | - | **② 处置提案**(必备) | 打算怎么做 / 已选哪条路线 / 为什么 | `## 方案` `## 机制` `## 两条修复路线` | - | **③ 处置结果**(按事实) | 实际做了什么 · 当前状态(已执行 / 挂起 / 未动) | `## 执行回执` `## 当前状态` | - | **④ 效果对比**(按事实) | 改前 vs 改后:**指标 + 口径 + 时点**同表对照 | `## 效果对比` `## T0 / T1 对照` | + | **① 问题背景**(必备) | 谁 / 什么场景 / 什么问题 · 影响面 · 根因(含证据 / 生产实测) | `## 现象` `## 根因` `## 影响面` `## 生产实测阶梯` | + | **② 处置提案**(必备) | 打算怎么做 / 已选哪条路线 / 为什么 | `## 方案` `## 机制` `## 两条修复路线` `## 结论` | + | **③ 处置结果**(按事实) | 实际做了什么 · 当前状态(已执行 / 挂起 / 未动) | `## 执行回执` `## 当前状态` `## 处置结果` | + | **④ 效果对比**(按事实) | 改前 vs 改后:**指标 + 口径 + 时点**同表对照 | `## 效果对比` `## 生产基线 T0` `## T0 / T1 对照` | - ③④ **只在真的做了 / 真的测了之后**才写;**未发生就整段不写**(不留空壳 —— 空壳等于噪音)。 - - **可选补段**(一律放四段之后):`## 验收判据` · `## 反证条件` · `## 遗留`。 + - **同一段可以有多个并列小节**:用 `## <段名> · <子题>`(实测:`## 效果对比 · 生产基线 T0` + `## 效果对比 · T1/T2 判据与实测`)—— `assess` 会**合并计为一节**。 + - **没有「提案」时不要硬造**:`## 结论`(**结论即提案**)可替代;若结论是「**无需处置**」或「**与 #n 不是同一根因**」,就在正文**显式写一段结论声明**。 + - **其他已识别小节**(不计入四段):`## 验收判据` · `## 反证条件` · `## 遗留` · `## 取证记录`。 - 正文必须**自包含**(§1 规则 3)—— 只读正文就能回答:**现在什么状态 / 要做什么 / 怎么验收 / 效果如何**。 2. **评论只保留「必须可执行物」**:可执行 SQL / 命令 / 待跑清单。**其余过程性评论一律删除**(不是「标注过时后留着」)。 ⚠ **可执行物不得因「正文已列名字」而删**(2026-09-24 实测事故):DDL / SQL 这类东西,**判定可删的唯一判据是「其全文已被正文或另一条评论完整承载」**;只列了对象名(如索引名清单)**不等于**承载了可执行语句 —— 一旦删除,工单就**没法照着做**了。 + **工具辅助**:`assess` / `comments` 会给每条评论标 `[可执行]` / `[? 非可执行]`(**启发式**:含代码围栏或 SQL 关键字)—— 只作提示,**仍需人核**。 ⚠ **工单里的 SQL 必须逐条原样**(2026-09-24 实测事故):**禁止**对语句做「拼接 / 去尾 / 换分隔符」式字符串变换(实测把 `, ALGORITHM=INPLACE, LOCK=NONE;` 换成逗号 ⇒ 18 条语句连成一段**无分号的废码**);**回退脚本不得留占位符**(列序 / 唯一性要填实)—— 否则「可执行物」实际**不可执行**。**投递前必须实跑验证**(结构影子库零数据逐条执行,正反向都要过)。 3. **过程流水与逐条取证进外部记录**(分析文档 / 台账底册),工单里**只在末尾引用块登记**(§3.3);人读部分**不得**出现外部路径与编号。 4. **口径更新必须同步正文与标题**:凡结论/数量/清单发生变化(如「24 条」→「23 条」、「删 5」→「删 4」),必须**同一次**改掉**标题 + 正文 + 摘要评论**(`update` 支持标题与正文同提交);**旧口径若仍在被引用(如执行清单),必须显式标注「已废止」并写明正确值**。 @@ -216,7 +219,7 @@ node bin/ticket.mjs rename 142 --repo=xbc/xbc --domain=平台 [--gate=等口径] - [ ] 四段**顺序正确**,**没有空壳段**(标题下没内容 ⇒ 删); - [ ] 正文**没有**「旧版 / 已废止」并列堆叠 —— 旧值已删,或压成一句「已废止,正确值 X」; - [ ] 标题 / 正文 / 评论里的**数量与范围一致**(「23 条」不能只改一处); -- [ ] 评论**只剩可执行物**,且**可执行物完整可跑**(是全文,不是对象名); +- [ ] 评论**只剩可执行物**,且**可执行物完整可跑**(是全文,不是对象名);工具标 `[? 非可执行]` 的**逐条核过**; - [ ] 同一条信息**没有两处详写**(一处详写,其余 `#n` 交叉引用); - [ ] 首行机读行仍是 ` | ` 形态、字段顺序未被打乱。 diff --git a/ticket-management/bin/ticket.mjs b/ticket-management/bin/ticket.mjs index 5e3ab78..a153880 100644 --- a/ticket-management/bin/ticket.mjs +++ b/ticket-management/bin/ticket.mjs @@ -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) {