chore: 增强TASKS索引治理门禁

This commit is contained in:
sunlei 2026-07-03 13:53:20 +08:00
parent 71d9ac7e10
commit 366a7a1606
2 changed files with 177 additions and 1 deletions

View File

@ -1149,6 +1149,51 @@ export async function runSelfTest(): Promise<void> {
) {
throw new Error("TASKS recent record limit self-check failed");
}
const taskHardLimitFindings = findTaskRecordGovernanceFindings([
"# TASKS.md",
"",
"## 当前上下文",
"",
"## 维护规则",
"",
"## 最近记录",
"### 2026-07-03记录一",
"- 范围root",
`- 关键词:${"TASKS 治理".repeat(60)}`,
" - 子弹列表塞长细节",
"补充验证:不允许用补充段绕过三字段结构。",
"- 验证global-review",
"## 历史索引",
"",
"### Root Governance / Workflow",
`- 2026-07-03${"历史索引也不能塞长流水。".repeat(20)}`,
...Array.from({ length: 700 }, () => "填充总体大小上限"),
]);
for (const category of [
"tasks-file-size-limit",
"tasks-record-field-too-long",
"tasks-record-freeform-line",
"tasks-history-item-too-long",
]) {
if (!taskHardLimitFindings.some((item) => item.category === category)) {
throw new Error(`TASKS hard limit self-check failed: ${category}`);
}
}
const taskSectionFindings = findTaskRecordGovernanceFindings([
"# TASKS.md",
"",
"## 当前上下文",
"## 最近记录",
"## 维护规则",
"## 历史索引",
]);
if (
!taskSectionFindings.some(
(item) => item.category === "tasks-section-order",
)
) {
throw new Error("TASKS section order self-check failed");
}
const napcatImageFindings = findNapcatImageGovernanceFindings(
["QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest"],

View File

@ -68,6 +68,15 @@ const napcatImageAssignmentPattern =
/^\s*QQBOT_NAPCAT_IMAGE\s*=\s*([^#\s]+)\s*(?:#.*)?$/;
const taskRecordAllowedFields = new Set(["范围", "关键词", "验证"]);
const taskRecordRequiredFields = ["范围", "关键词", "验证"] as const;
const taskRequiredSecondLevelSections = [
"当前上下文",
"维护规则",
"最近记录",
"历史索引",
] as const;
const taskFileMaxBytes = 14 * 1024;
const taskHistoryItemMaxChars = 180;
const taskRecordFieldMaxChars = 360;
const taskRecentRecordLimit = 3;
interface ReviewFinding {
@ -102,7 +111,13 @@ export function findTaskRecordGovernanceFindings(
lines: string[],
): ReviewFinding[] {
const findings: ReviewFinding[] = [];
findings.push(...findTaskFileShapeFindings(lines));
const recentStart = lines.findIndex((line) => line.trim() === "## 最近记录");
const historyStart = lines.findIndex((line) => line.trim() === "## 历史索引");
if (historyStart >= 0) {
findings.push(...findTaskHistoryIndexFindings(lines, historyStart));
}
if (recentStart < 0) return findings;
const nextSectionOffset = lines
@ -164,13 +179,38 @@ export function findTaskRecordGovernanceFindings(
}
if (!currentRecord) continue;
if (!line) continue;
const fieldMatch = line.match(/^-\s*([^:]+)[:]/);
if (!fieldMatch) continue;
if (!fieldMatch) {
findings.push({
category: "tasks-record-freeform-line",
file: "TASKS.md",
line: index + 1,
level: "P2",
message: `TASKS 最近记录「${currentRecord.heading}」包含非三字段正文。`,
project: "root",
suggestion:
"最近记录只允许三条 bullet范围、关键词、验证补充段、子列表、表格、代码块全部迁入 docs 或历史索引。",
});
continue;
}
const field = fieldMatch[1].trim();
if (taskRecordAllowedFields.has(field)) {
currentRecord.fields.add(field);
if (line.length > taskRecordFieldMaxChars) {
findings.push({
category: "tasks-record-field-too-long",
file: "TASKS.md",
line: index + 1,
level: "P2",
message: `TASKS 最近记录字段「${field}」过长,当前 ${line.length} 字,限制 ${taskRecordFieldMaxChars} 字。`,
project: "root",
suggestion:
"把长证据、长复现和长命令移入 docs 或历史索引短关键词,最近记录只保留摘要。",
});
}
continue;
}
@ -190,6 +230,97 @@ export function findTaskRecordGovernanceFindings(
return findings;
}
/**
* Checks the whole task index shape before section-specific recent-record checks.
* @param lines - Current `TASKS.md` content split by line.
* @returns Review findings for total size or required second-level section drift.
*/
function findTaskFileShapeFindings(lines: string[]): ReviewFinding[] {
const findings: ReviewFinding[] = [];
const byteLength = Buffer.byteLength(lines.join("\n"), "utf8");
if (byteLength > taskFileMaxBytes) {
findings.push({
category: "tasks-file-size-limit",
file: "TASKS.md",
level: "P2",
message: `TASKS.md 只能作为压缩索引,当前 ${byteLength} bytes限制 ${taskFileMaxBytes} bytes。`,
project: "root",
suggestion:
"删除流水账、长验证、截图清单和完整命令;必要细节放入 docsTASKS 只保留索引。",
});
}
const sectionHeadings = lines
.map((line, index) => ({ line, lineNumber: index + 1 }))
.filter((item) => /^##\s+/.test(item.line.trim()));
const actualSections = sectionHeadings.map((item) =>
item.line.trim().replace(/^##\s+/, ""),
);
const expectedSections = [...taskRequiredSecondLevelSections];
const hasExactSectionOrder =
actualSections.length === expectedSections.length &&
expectedSections.every((section, index) => actualSections[index] === section);
if (!hasExactSectionOrder) {
findings.push({
category: "tasks-section-order",
file: "TASKS.md",
line: sectionHeadings[0]?.lineNumber ?? 1,
level: "P2",
message: `TASKS.md 二级章节必须固定为 ${expectedSections.join(" -> ")}`,
project: "root",
suggestion:
"不要新增临时章节、补充章节或任务分组;把额外内容压缩进最近记录三字段或历史索引分类 bullet。",
});
}
return findings;
}
/**
* Checks the compact history index so old work cannot become another long-form log sink.
* @param lines - Current `TASKS.md` content split by line.
* @param historyStart - Zero-based line index of `## 历史索引`.
* @returns Review findings for long or free-form history index content.
*/
function findTaskHistoryIndexFindings(
lines: string[],
historyStart: number,
): ReviewFinding[] {
const findings: ReviewFinding[] = [];
for (let index = historyStart + 1; index < lines.length; index += 1) {
const line = lines[index].trim();
if (!line) continue;
if (line.startsWith("### ")) continue;
if (!line.startsWith("- ")) {
findings.push({
category: "tasks-history-freeform-line",
file: "TASKS.md",
line: index + 1,
level: "P2",
message: "TASKS 历史索引只能使用分类标题和单行关键词 bullet。",
project: "root",
suggestion:
"删除段落、代码块、表格和补充说明;历史索引只保留可检索关键词。",
});
continue;
}
if (line.length <= taskHistoryItemMaxChars) continue;
findings.push({
category: "tasks-history-item-too-long",
file: "TASKS.md",
line: index + 1,
level: "P2",
message: `TASKS 历史索引条目过长,当前 ${line.length} 字,限制 ${taskHistoryItemMaxChars} 字。`,
project: "root",
suggestion:
"把旧任务压缩成日期、主题和关键词;长背景移入 docs 或删除。",
});
}
return findings;
}
export function findNapcatImageGovernanceFindings(
lines: string[],
context: {