diff --git a/.ua/backlog.json b/.ua/backlog.json index 7bdf164..44cec25 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1188,6 +1188,14 @@ "acceptance": "有筛选词时显示「没有符合「关键词」的行」并给「清除筛选」按钮(一次性清空关键字信号与内核筛选、回第 1 页);无筛选词时显示「暂无数据」且不出现按钮;关键字用受控信号持有,与筛选框同源;每行文件仍 ≤ 200 行;npm run check 全绿", "notes": "自补:可用性;空状态行抽到 tableEmpty.ts,dataTable 顺手把 api 类型别名下沉到 types.ts 才没破 200 行", "status": "done" + }, + { + "id": "g104", + "title": "留痕校验回查提交是否真实存在", + "motivation": "iterations.log 目前只校验字段格式:commit 随便写个哈希、或复制粘贴时抄错一位,门禁照样全绿。留痕是「这条迭代真的落过」的唯一凭证,凭证本身不可验证等于没校验", + "acceptance": "ua lint-iterations 额外做双向核对:每行 commit 必须能在 git 历史里按前缀命中,且该提交主题以 iter() 开头(兼容早期无 scope 的 iter: );伪造 / 抄错的哈希报违规并退出码 1;非 git 环境(打包分发)自动跳过该维度;ITERATION.md 留痕章节写明该校验;npm run check 全绿", + "notes": "自补:门禁工具链;一次 git log 建索引,150 行留痕不额外起进程", + "status": "open" } ] } diff --git a/ITERATION.md b/ITERATION.md index 7752eb6..d1e0027 100644 --- a/ITERATION.md +++ b/ITERATION.md @@ -47,7 +47,7 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main 4. **门禁**:跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify)。 - **通过**:提交到 `qa/probe`,提交信息按 `COMMITTING.md` 规范(`iter(<条目id>): <中文祈使句>`,元数据变更用 `chore(backlog): 记录 完成`;commit-msg 钩子自动校验),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。 - **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。 -5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。 +5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。`ua lint-iterations` 会把每行 `commit` 拿到 git 历史里回查:哈希必须真实存在、主题必须指向同一条目(`iter()`,早期无 scope 的 `iter: ` 也认),伪造或抄错哈希直接判违规。 6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。 ## 选题规则(自主补点子时) diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index 7af2a74..2c1f1e2 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -1017,6 +1017,18 @@ function cmdCommitLint(args) { console.log(`commit-lint OK (${type}${scope ? `(${scope})` : ''})`); } +/*** 提交主题索引:一次 git log 换「短哈希 → 主题」表,供留痕回查,避免逐行起进程 ***/ +function commitSubjects() { + const r = spawnSync('git', ['log', '--all', '--pretty=%h|%s'], { cwd: ROOT, encoding: 'utf8' }); + if (r.status !== 0) return null; // 非 git 环境(打包分发等)跳过该维度,不让整条门禁失效 + const pairs = []; + for (const line of String(r.stdout ?? '').split('\n')) { + const cut = line.indexOf('|'); + if (cut > 0) pairs.push([line.slice(0, cut), line.slice(cut + 1)]); + } + return pairs; +} + /*** 迭代留痕校验:.ua/iterations.log 每行 JSON 字段与格式必须符合 ITERATION.md 规范 ***/ function cmdLintIterations() { const file = path.join(INDEX_DIR, 'iterations.log'); @@ -1041,6 +1053,23 @@ function cmdLintIterations() { if (o.result === 'aborted' && commit && !/^[0-9a-f]{7,40}$/.test(commit)) issues.push(`${tag} aborted 条目 commit 须为空或哈希`); if (!String(o.summary ?? '').trim()) issues.push(`${tag} summary 不能为空`); }); + // —— 留痕 ↔ 提交双向核对:哈希必须真实存在,且主题要指向同一条目(防伪造 / 防抄错哈希)—— + const pairs = commitSubjects(); + if (pairs) { + lines.forEach((line, i) => { + const tag = `iterations.log:${i + 1}`; + let o; + try { o = JSON.parse(line); } catch { return; } // 行格式问题已在上轮报过 + const hash = String(o.commit ?? ''); + if (!hash) return; // aborted 允许留空 + const hit = pairs.find(([h]) => h.startsWith(hash)); + if (!hit) { issues.push(`${tag} commit ${hash} 不在仓库历史中`); return; } + const legacy = `iter: ${o.id}`; // 早期提交用「iter: 描述」,无 scope + if (!hit[1].startsWith(`iter(${o.id})`) && !hit[1].startsWith(legacy)) { + issues.push(`${tag} commit ${hash} 的主题「${hit[1]}」没有指向条目 ${o.id}`); + } + }); + } if (issues.length) { for (const it of issues) console.error('lint-iterations: ' + it); console.error(`${issues.length} violation(s)`);