fix: disable board audit coverage validation

This commit is contained in:
Codex
2026-05-23 03:16:44 +00:00
parent 7ea2f77b70
commit 2bd36e7e9c
4 changed files with 67 additions and 139 deletions
+22 -19
View File
@@ -654,29 +654,32 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(boardAudit.status === 0, "issue board-audit should succeed as a read-only audit", boardAudit.json ?? { stdout: boardAudit.stdout, stderr: boardAudit.stderr });
const boardAuditData = dataOf(boardAudit.json ?? {});
assertCondition(boardAuditData.command === "issue board-audit" && boardAuditData.dryRun === true && boardAuditData.readOnly === true, "board-audit should be explicit read-only dry-run", boardAuditData);
const boardAuditIssue = boardAuditData.boardIssue as JsonRecord;
assertCondition(typeof boardAuditIssue.bodySha === "string" && String(boardAuditIssue.bodySha).length === 64, "board-audit should expose board body sha", boardAuditIssue);
const boardAuditSummary = boardAuditData.summary as JsonRecord;
assertCondition(boardAuditSummary.openIssues === null && boardAuditSummary.closedIssues === null, "board-audit should not fetch GitHub open/closed issue lists", boardAuditSummary);
assertCondition(boardAuditSummary.openRows === 4 && boardAuditSummary.closedRows === 3 && boardAuditSummary.parsedSections === 2, "board-audit should report parsed board structure", boardAuditSummary);
const boardAuditValidation = boardAuditData.validation as JsonRecord;
const openClosedCoverage = boardAuditValidation.openClosedCoverage as JsonRecord;
assertCondition(openClosedCoverage.enabled === false, "board-audit should disable OPEN/CLOSED coverage validation", boardAuditValidation);
const missingOpenIssues = boardAuditData.missingOpenIssues as JsonRecord[];
assertCondition(Array.isArray(missingOpenIssues) && missingOpenIssues.some((issue) => issue.number === 36), "board-audit should report open issue missing from OPEN table", missingOpenIssues);
assertCondition(!missingOpenIssues.some((issue) => issue.number === 45), "board-audit should use the primary markdown issue link as the Issue-column row key", missingOpenIssues);
assertCondition(!missingOpenIssues.some((issue) => issue.number === 46), "board-audit should not require daily commander briefs in the OPEN table", missingOpenIssues);
assertCondition(Array.isArray(missingOpenIssues) && missingOpenIssues.length === 0, "board-audit should not report missing OPEN rows", missingOpenIssues);
const closedInOpenRows = boardAuditData.closedInOpenRows as JsonRecord[];
assertCondition(Array.isArray(closedInOpenRows) && closedInOpenRows.some((issue) => issue.number === 40), "board-audit should report closed issue still in OPEN table", closedInOpenRows);
assertCondition(Array.isArray(closedInOpenRows) && closedInOpenRows.length === 0, "board-audit should not report closed issues in OPEN rows", closedInOpenRows);
const missingClosedRows = boardAuditData.missingClosedRows as JsonRecord[];
assertCondition(Array.isArray(missingClosedRows) && missingClosedRows.some((issue) => issue.number === 40) && missingClosedRows.some((issue) => issue.number === 41), "board-audit should report closed issues missing from CLOSED table", missingClosedRows);
assertCondition(!missingClosedRows.some((issue) => issue.number === 18), "board-audit should ignore explanatory issue references after the primary Issue-column markdown link", missingClosedRows);
assertCondition(Array.isArray(missingClosedRows) && missingClosedRows.length === 0, "board-audit should not report missing CLOSED rows", missingClosedRows);
const openInClosedRows = boardAuditData.openInClosedRows as JsonRecord[];
assertCondition(Array.isArray(openInClosedRows) && openInClosedRows.some((issue) => issue.number === 36), "board-audit should report open issue placed in CLOSED table", openInClosedRows);
assertCondition(Array.isArray(openInClosedRows) && openInClosedRows.length === 0, "board-audit should not report open issues in CLOSED rows", openInClosedRows);
const rowValidationWarnings = boardAuditData.rowValidationWarnings as JsonRecord[];
assertCondition(Array.isArray(rowValidationWarnings) && rowValidationWarnings.some((warning) => warning.issueNumber === 40 && Array.isArray(warning.missingColumns) && (warning.missingColumns as unknown[]).includes("branch") && (warning.missingColumns as unknown[]).includes("acceptance") && (warning.missingColumns as unknown[]).includes("progress")), "board-audit should report missing required board columns", rowValidationWarnings);
assertCondition(!rowValidationWarnings.some((warning) => warning.kind === "multiple-issue-references"), "board-audit should not treat explanatory issue references outside the Issue column as row-key conflicts", rowValidationWarnings);
assertCondition(!rowValidationWarnings.some((warning) => warning.kind === "multiple-issue-references" && String(warning.rowPreview ?? "").includes("#20 总看板")), "board-audit should not warn when an Issue-column title mentions #20 after primary #45 link", rowValidationWarnings);
assertCondition(!rowValidationWarnings.some((warning) => warning.kind === "multiple-issue-references" && String(warning.rowPreview ?? "").includes("基于 #4")), "board-audit should not warn when an Issue-column title mentions #4 after primary #18 link", rowValidationWarnings);
assertCondition(!rowValidationWarnings.some((warning) => Array.isArray(warning.missingColumns) && (warning.missingColumns as unknown[]).includes("relatedTask")), "board-audit should recognize relatedTask from the current header", rowValidationWarnings);
assertCondition(Array.isArray(rowValidationWarnings) && rowValidationWarnings.length === 0, "board-audit should not report required-column row warnings", rowValidationWarnings);
const parserWarnings = boardAuditData.parserWarnings as JsonRecord[];
assertCondition(Array.isArray(parserWarnings), "board-audit should expose parser warnings separately", boardAuditData);
const ignoredIssues = boardAuditData.ignoredIssues as JsonRecord[];
assertCondition(Array.isArray(ignoredIssues) && ignoredIssues.some((issue) => issue.number === 20 && issue.reason === "known-meta") && ignoredIssues.some((issue) => issue.number === 24 && issue.reason === "known-meta"), "board-audit should ignore configured known meta issues", ignoredIssues);
assertCondition(ignoredIssues.some((issue) => issue.number === 46 && issue.reason === "brief-index-managed"), "board-audit should ignore daily commander briefs managed by the #20 brief index", ignoredIssues);
assertCondition(Array.isArray(ignoredIssues) && ignoredIssues.length === 0, "board-audit should not produce ignored issue coverage output", ignoredIssues);
const recommendedActions = boardAuditData.recommendedActions as JsonRecord[];
assertCondition(Array.isArray(recommendedActions) && recommendedActions.some((action) => action.action === "add-open-row" && action.issueNumber === 36) && recommendedActions.some((action) => action.action === "move-open-row-to-closed" && action.issueNumber === 40), "board-audit should emit machine-readable recommended actions", recommendedActions);
assertCondition(!recommendedActions.some((action) => action.action === "add-open-row" && action.issueNumber === 46), "board-audit should not recommend adding daily commander briefs to the OPEN table", recommendedActions);
assertCondition(Array.isArray(recommendedActions) && recommendedActions.length === 0, "board-audit should not emit OPEN/CLOSED coverage actions", recommendedActions);
const boardAuditGetRequests = mock.requests.slice(boardAuditRequestCountBefore).filter((request) => request.method === "GET");
assertCondition(boardAuditGetRequests.length === 1 && boardAuditGetRequests[0]?.url === "/repos/pikasTech/unidesk/issues/20", "board-audit should only fetch the board issue body", boardAuditGetRequests);
const boardAuditWriteCount = mock.requests.slice(boardAuditRequestCountBefore).filter((request) => request.method === "PATCH" || request.method === "DELETE" || request.method === "POST").length;
assertCondition(boardAuditWriteCount === 0, "board-audit must not write GitHub", { requests: mock.requests.slice(boardAuditRequestCountBefore) });
@@ -684,8 +687,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const legacyBoardAudit = await runCli(["gh", "issue", "board-audit", "--repo", "pikasTech/unidesk", "--board-issue", "60", "--limit", "60", "--dry-run"], env);
assertCondition(legacyBoardAudit.status === 0, "legacy board-audit fixture should succeed", legacyBoardAudit.json ?? { stdout: legacyBoardAudit.stdout, stderr: legacyBoardAudit.stderr });
const legacyBoardAuditData = dataOf(legacyBoardAudit.json ?? {});
const legacyWarnings = legacyBoardAuditData.rowValidationWarnings as JsonRecord[];
assertCondition(Array.isArray(legacyWarnings) && legacyWarnings.some((warning) => warning.kind === "multiple-issue-references" && warning.issueNumber === 101), "legacy board-audit fixture should keep whole-row fallback when no Issue column exists", legacyWarnings);
const legacyWarnings = legacyBoardAuditData.parserWarnings as JsonRecord[];
assertCondition(Array.isArray(legacyWarnings) && legacyWarnings.some((warning) => warning.kind === "multiple-issue-references" && warning.issueNumber === 101), "legacy board-audit fixture should keep parser warnings when no Issue column exists", legacyWarnings);
const legacyBoardWriteCount = mock.requests.slice(legacyBoardAuditRequestCountBefore).filter((request) => request.method === "PATCH" || request.method === "DELETE" || request.method === "POST").length;
assertCondition(legacyBoardWriteCount === 0, "legacy board-audit must not write GitHub", { requests: mock.requests.slice(legacyBoardAuditRequestCountBefore) });
@@ -1326,7 +1329,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"issue list default fields include labels and filter pull requests",
"issue scan-escape classifies pollution, explanatory mentions, and body risks",
"issue cleanup-plan remains dry-run with body/comment cleanup suggestions",
"issue board-audit reports missing open rows, closed/open section mismatches, missing closed rows, meta/brief-index ignores, and row validation warnings without writes",
"issue board-audit returns read-only board structure, disables OPEN/CLOSED coverage validation, and keeps compatibility fields empty without writes",
"issue board-row list/get expose parsed #20 rows without writes",
"issue board-row upsert updates existing rows, adds missing rows, reports operation, preserves table trailers, rejects ambiguous rows, blocks stale body SHA writes, and stays dry-run without concurrency guards",
"issue board-row add/delete without guard stay on dry-run and do not PATCH",
+43 -118
View File
@@ -3893,101 +3893,10 @@ async function issueBoardAudit(repo: string, token: string, options: GitHubOptio
const commandName = "issue board-audit";
const boardIssue = await getIssue(token, repo, options.boardIssue);
if (isGitHubError(boardIssue)) return commandError(commandName, repo, boardIssue, { boardIssue: options.boardIssue });
const [rawOpenIssues, rawClosedIssues] = await Promise.all([
listIssues(token, repo, "open", options.limit),
listIssues(token, repo, "closed", options.limit),
]);
if (isGitHubError(rawOpenIssues)) return commandError(commandName, repo, rawOpenIssues, { phase: "list-open-issues", boardIssue: options.boardIssue, limit: options.limit });
if (isGitHubError(rawClosedIssues)) return commandError(commandName, repo, rawClosedIssues, { phase: "list-closed-issues", boardIssue: options.boardIssue, limit: options.limit });
const openIssues = rawOpenIssues.filter((issue) => issue.pull_request === undefined).slice(0, options.limit).map(boardIssueEntry);
const closedIssues = rawClosedIssues.filter((issue) => issue.pull_request === undefined).slice(0, options.limit).map(boardIssueEntry);
const allListedIssues = [...openIssues, ...closedIssues];
const issueMap = new Map<number, BoardIssueEntry>(allListedIssues.map((issue) => [issue.number, issue]));
const ignoreMap = boardIgnoreMap(options, allListedIssues);
const parsed = parseBoardTables(boardIssue.body ?? "");
const rowValidationWarnings = parsed.warnings.filter((warning) => warning.issueNumber === null || !ignoreMap.has(warning.issueNumber));
const openRows = parsed.sections.filter((section) => section.kind === "open").flatMap((section) => section.rows);
const closedRows = parsed.sections.filter((section) => section.kind === "closed").flatMap((section) => section.rows);
const openRowMap = boardRowsByIssue(openRows);
const closedRowMap = boardRowsByIssue(closedRows);
const openIssueNumbers = new Set(openIssues.map((issue) => issue.number));
const closedIssueNumbers = new Set(closedIssues.map((issue) => issue.number));
const boardIssueNumbers = Array.from(new Set([...openRowMap.keys(), ...closedRowMap.keys()]));
const missingOpenIssues = sortedIssueEntries(openIssues.filter((issue) => !ignoreMap.has(issue.number) && !openRowMap.has(issue.number)));
const closedInOpenRows = sortedIssueEntries(closedIssues.filter((issue) => !ignoreMap.has(issue.number) && openRowMap.has(issue.number))).map((issue) => ({
...issue,
rows: (openRowMap.get(issue.number) ?? []).map((row) => ({ lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
}));
const missingClosedRows = sortedIssueEntries(closedIssues.filter((issue) => !ignoreMap.has(issue.number) && !closedRowMap.has(issue.number)));
const openInClosedRows = sortedIssueEntries(openIssues.filter((issue) => !ignoreMap.has(issue.number) && closedRowMap.has(issue.number))).map((issue) => ({
...issue,
rows: (closedRowMap.get(issue.number) ?? []).map((row) => ({ lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
}));
const staleOpenRows = Array.from(openRowMap.entries())
.filter(([issueNumber]) => !ignoreMap.has(issueNumber) && !openIssueNumbers.has(issueNumber))
.map(([issueNumber, rows]) => ({
issueNumber,
knownState: closedIssueNumbers.has(issueNumber) ? "closed" : "not-in-listed-window",
rows: rows.map((row) => ({ lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
}))
.sort((a, b) => a.issueNumber - b.issueNumber);
const boardOnlyRows = boardIssueNumbers
.filter((issueNumber) => !ignoreMap.has(issueNumber) && !issueMap.has(issueNumber))
.sort((a, b) => a - b)
.map((issueNumber) => ({
issueNumber,
sections: [
...(openRowMap.has(issueNumber) ? ["open"] : []),
...(closedRowMap.has(issueNumber) ? ["closed"] : []),
],
rows: [...(openRowMap.get(issueNumber) ?? []), ...(closedRowMap.get(issueNumber) ?? [])].map((row) => ({
section: row.section,
lineNumber: row.lineNumber,
rowPreview: preview(row.raw),
})),
}));
const ignoredIssues = [
...ignoredIssueList(allListedIssues, ignoreMap),
...ignoredBoardOnlyIssues(boardIssueNumbers, issueMap, ignoreMap),
].sort((a, b) => a.number - b.number);
const recommendedActions = [
...missingOpenIssues.map((issue) => ({
action: "add-open-row",
issueNumber: issue.number,
title: issue.title,
section: "open",
reason: "GitHub issue is open but no #20 OPEN table row was found.",
})),
...closedInOpenRows.map((issue) => ({
action: "move-open-row-to-closed",
issueNumber: issue.number,
title: issue.title,
reason: "GitHub issue is closed but still appears in the #20 OPEN table.",
})),
...missingClosedRows.map((issue) => ({
action: "add-closed-row",
issueNumber: issue.number,
title: issue.title,
section: "closed",
reason: "GitHub issue is closed but no #20 CLOSED table row was found.",
})),
...openInClosedRows.map((issue) => ({
action: "move-closed-row-to-open",
issueNumber: issue.number,
title: issue.title,
reason: "GitHub issue is open but appears in the #20 CLOSED table.",
})),
...rowValidationWarnings.map((warning) => ({
action: "fill-board-row-fields",
issueNumber: warning.issueNumber,
section: warning.section,
lineNumber: warning.lineNumber,
reason: warning.message,
missingColumns: warning.missingColumns ?? [],
})),
];
const body = boardIssue.body ?? "";
return {
ok: true,
@@ -4001,7 +3910,9 @@ async function issueBoardAudit(repo: string, token: string, options: GitHubOptio
title: boardIssue.title,
state: boardIssue.state,
url: boardIssue.html_url,
bodyChars: (boardIssue.body ?? "").length,
bodyChars: body.length,
bodyLines: body.length === 0 ? 0 : normalizeNewlines(body).split("\n").length,
bodySha: bodySha(body),
updatedAt: boardIssue.updated_at ?? null,
},
config: {
@@ -4009,21 +3920,36 @@ async function issueBoardAudit(repo: string, token: string, options: GitHubOptio
knownMetaIssues: mergedKnownMetaIssues(options),
ignoredIssues: options.ignoredIssues,
briefIndexManagedIssuePattern: options.boardIssue === CODE_QUEUE_BOARD_TARGET_ISSUE ? "YYYY-MM-DD 指挥简报(北京时间)" : null,
requiredColumns: BOARD_AUDIT_REQUIRED_COLUMNS.slice(),
openClosedCoverageValidation: false,
requiredColumns: [],
},
summary: {
openIssues: openIssues.length,
closedIssues: closedIssues.length,
openIssues: null,
closedIssues: null,
openRows: openRows.length,
closedRows: closedRows.length,
missingOpenIssues: missingOpenIssues.length,
closedInOpenRows: closedInOpenRows.length,
missingClosedRows: missingClosedRows.length,
openInClosedRows: openInClosedRows.length,
rowValidationWarnings: rowValidationWarnings.length,
ignoredIssues: ignoredIssues.length,
boardOnlyRows: boardOnlyRows.length,
staleOpenRows: staleOpenRows.length,
parsedSections: parsed.sections.length,
parsedRows: parsed.sections.reduce((sum, section) => sum + section.rows.length, 0),
parserWarnings: parsed.warnings.length,
missingOpenIssues: 0,
closedInOpenRows: 0,
missingClosedRows: 0,
openInClosedRows: 0,
rowValidationWarnings: 0,
ignoredIssues: 0,
boardOnlyRows: 0,
staleOpenRows: 0,
recommendedActions: 0,
},
validation: {
openClosedCoverage: {
enabled: false,
reason: "OPEN/CLOSED table coverage validation has been disabled; this command no longer compares GitHub issue state against board rows.",
},
rowRequiredColumns: {
enabled: false,
reason: "board-audit no longer reports missing required row columns; use board-row commands when intentionally maintaining a legacy OPEN/CLOSED table.",
},
},
sections: parsed.sections.map((section) => ({
kind: section.kind,
@@ -4034,25 +3960,24 @@ async function issueBoardAudit(repo: string, token: string, options: GitHubOptio
rows: section.rows.length,
issueNumbers: section.rows.flatMap((row) => row.issueNumbers),
})),
missingOpenIssues,
closedInOpenRows,
missingClosedRows,
openInClosedRows,
staleOpenRows,
boardOnlyRows,
rowValidationWarnings,
ignoredIssues,
recommendedActions,
parserWarnings: parsed.warnings,
missingOpenIssues: [],
closedInOpenRows: [],
missingClosedRows: [],
openInClosedRows: [],
staleOpenRows: [],
boardOnlyRows: [],
rowValidationWarnings: [],
ignoredIssues: [],
recommendedActions: [],
request: {
method: "GET",
paths: [
`/repos/{owner}/{repo}/issues/${options.boardIssue}`,
"/repos/{owner}/{repo}/issues?state=open",
"/repos/{owner}/{repo}/issues?state=closed",
],
query: { per_page: options.limit },
query: {},
},
note: "Read-only board audit; no issue body was edited, no issue was closed, and no comments were written.",
note: "Read-only board structure audit; OPEN/CLOSED coverage validation is disabled, no issue body was edited, no issue was closed, and no comments were written.",
};
}
@@ -4830,7 +4755,7 @@ export function ghHelp(): unknown {
"When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments; GitHub Markdown writes intentionally use --body-file only.",
"issue scan-escape classifies literal \\n findings as suspected-pollution, explanatory-mention, or risk, and emits cleanupSuggestions with body/comment ids plus diff-like previews. cleanup-plan is an alias that remains dry-run/read-only.",
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It compares GitHub open/closed issue lists with the board OPEN/CLOSED tables and reports missingOpenIssues, closedInOpenRows, missingClosedRows, rowValidationWarnings, ignoredIssues, and recommendedActions. When an Issue column exists, row.issueNumber is taken from that column; #20 and #24 are known meta issues by default. Daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间) are managed by #20's brief index and appear in ignoredIssues with reason=brief-index-managed instead of missingOpenIssues.",
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It reads only the board issue body, returns body size/SHA and parsed Markdown board sections, and no longer validates GitHub open/closed issue coverage against OPEN/CLOSED tables. The legacy coverage fields remain present as empty arrays/zero counts for compatibility.",
"issue board-row list/get reuse the board-audit table parser and are read-only. board-row update changes one table cell by issue number, returns old/new row, body SHA, body guard and request plan, and defaults to dry-run unless --expect-updated-at or --expect-body-sha is supplied for the guarded PATCH. Field aliases map status and validation to the 验收状态 column, tasks to 相关 Code Queue 任务, and focus to 当前关注点.",
"issue board-row upsert updates an existing row when the issue is already present, or generates a complete row in --section open|closed when missing. It returns operation=update or operation=add, defaults to dry-run, requires --expect-body-sha or --expect-updated-at before PATCH, and refuses section migration; use board-row move for OPEN/CLOSED migration.",
"issue board-row add/move/delete are row-scoped #20 table mutations. add validates a one-line --row-file against the target table column count, Issue column, and GitHub 状态 column; move refuses duplicate/ambiguous rows and can update GitHub 状态 via --status; delete removes only the matched row. All three default to dry-run and require --expect-body-sha or --expect-updated-at before PATCH. add/move/delete return old/new row, body SHA, and line/section plan details for the parsed table mutation.",