edc437cdd8
Co-authored-by: Codex <codex@noreply.local>
230 lines
10 KiB
TypeScript
230 lines
10 KiB
TypeScript
// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
|
||
// Moved mechanically from scripts/src/gh.ts:1669-1889.
|
||
|
||
import { bodySafetySignals, preview, previewLines } from "./auth-and-safety";
|
||
import { isMarkdownTableSeparator } from "./board-parser";
|
||
import { CODE_QUEUE_BOARD_TARGET_ISSUE, COMMANDER_BRIEF_UPDATE_HEADING_PATTERNS } from "./types";
|
||
import type { CommanderBriefDiff, CommanderBriefSection } from "./types";
|
||
|
||
export function dryRunBody(repo: string, title: string | undefined, body: string): Record<string, unknown> {
|
||
return {
|
||
repo,
|
||
...(title === undefined ? {} : { title }),
|
||
bodyPreview: preview(body),
|
||
bodyPreviewLines: previewLines(body),
|
||
...bodySafetySignals(body),
|
||
};
|
||
}
|
||
|
||
export function normalizeNewlines(text: string): string {
|
||
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||
}
|
||
|
||
export function isTimelineHeading(line: string): boolean {
|
||
return /^##\s+更新\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}\s+北京时间\s*$/u.test(line.trimEnd());
|
||
}
|
||
|
||
export function isCommanderBriefUpdateHeading(line: string): boolean {
|
||
const trimmed = line.trimEnd();
|
||
return COMMANDER_BRIEF_UPDATE_HEADING_PATTERNS.some((pattern) => pattern.test(trimmed));
|
||
}
|
||
|
||
export function commanderBriefUpdateHeadings(body: string): string[] {
|
||
return normalizeNewlines(body)
|
||
.split("\n")
|
||
.map((line) => line.trimEnd())
|
||
.filter((line) => isCommanderBriefUpdateHeading(line));
|
||
}
|
||
|
||
export function codeQueueBoardHwlabProductRoutingFindings(body: string | undefined): Array<Record<string, unknown>> {
|
||
if (body === undefined) return [];
|
||
const findings: Array<Record<string, unknown>> = [];
|
||
const directIssuePatterns = [
|
||
{ kind: "hwlab-repo-issue-url", pattern: /(?:https?:\/\/github\.com\/)?pikasTech\/HWLAB\/issues\/\d+/iu },
|
||
{ kind: "hwlab-repo-issue-short-ref", pattern: /\bpikasTech\/HWLAB#\d+\b/iu },
|
||
{ kind: "hwlab-short-issue-ref", pattern: /\bHWLAB#\d+\b/iu },
|
||
];
|
||
const hwlabContextPattern = /\bHWLAB\b|pikasTech\/HWLAB|hwlab-/iu;
|
||
const strongProductPattern = /hwlab-cloud-web|Cloud Workbench|DEV-LIVE|res_boxsimu|hwlab-patch-panel|patch panel|M3\s*(?:虚拟硬件|可信闭环)/iu;
|
||
const genericProductPattern = /用户反馈|user feedback/iu;
|
||
const governanceContextPattern = /\b(?:commander|Code Queue|CLI|infra|governance|guard|guardrail|routing|misfile|board)\b|治理|调度|基础设施|守卫|边界|分流|看板/iu;
|
||
const lines = normalizeNewlines(body).split("\n");
|
||
for (let index = 0; index < lines.length; index += 1) {
|
||
const line = lines[index];
|
||
const isTableRow = line.trim().startsWith("|") && !isMarkdownTableSeparator(line);
|
||
const strongProductSignal = strongProductPattern.test(line);
|
||
const genericProductSignal = genericProductPattern.test(line);
|
||
const governanceContext = governanceContextPattern.test(line);
|
||
for (const definition of directIssuePatterns) {
|
||
const match = definition.pattern.exec(line);
|
||
if (match !== null && !governanceContext && (isTableRow || strongProductSignal || (genericProductSignal && hwlabContextPattern.test(line)))) {
|
||
findings.push({
|
||
kind: definition.kind,
|
||
lineNumber: index + 1,
|
||
match: match[0],
|
||
snippet: preview(line.trim()),
|
||
});
|
||
}
|
||
}
|
||
if (isTableRow && !governanceContext && (strongProductSignal || (genericProductSignal && hwlabContextPattern.test(line)))) {
|
||
const match = strongProductSignal ? strongProductPattern.exec(line) : genericProductPattern.exec(line);
|
||
findings.push({
|
||
kind: "hwlab-product-signal",
|
||
lineNumber: index + 1,
|
||
match: match?.[0] ?? "HWLAB product signal",
|
||
snippet: preview(line.trim()),
|
||
});
|
||
}
|
||
if (findings.length >= 12) break;
|
||
}
|
||
return findings;
|
||
}
|
||
|
||
export function codeQueueBoardHwlabProductRoutingHint(boardIssueNumber: number, body?: string): Record<string, unknown> | null {
|
||
if (boardIssueNumber !== CODE_QUEUE_BOARD_TARGET_ISSUE) return null;
|
||
const findings = codeQueueBoardHwlabProductRoutingFindings(body);
|
||
return {
|
||
warning: "#20 is only for UniDesk commander/Code Queue/CLI/infra governance; HWLAB user/product issues belong in pikasTech/HWLAB.",
|
||
route: "Create or update the corresponding issue in pikasTech/HWLAB; keep #20 rows limited to UniDesk governance or infrastructure support work.",
|
||
forbiddenPatterns: [
|
||
"pikasTech/HWLAB#<N>",
|
||
"github.com/pikasTech/HWLAB/issues/<N>",
|
||
"HWLAB#<N>",
|
||
"HWLAB product/live validation rows",
|
||
],
|
||
allowedScope: ["commander governance", "Code Queue supervision", "UniDesk CLI guardrails", "UniDesk infrastructure"],
|
||
detected: findings.length > 0,
|
||
findings,
|
||
};
|
||
}
|
||
|
||
export function codeQueueBoardHintHasHwlabProductRouting(hint: Record<string, unknown> | null): boolean {
|
||
if (hint === null) return false;
|
||
const routing = hint.hwlabProductRouting;
|
||
return typeof routing === "object" && routing !== null && (routing as { detected?: unknown }).detected === true;
|
||
}
|
||
|
||
export function codeQueueBoardHintHasCommanderBriefUpdates(hint: Record<string, unknown> | null): boolean {
|
||
if (hint === null) return false;
|
||
const commanderBrief = hint.commanderBrief;
|
||
return typeof commanderBrief === "object" && commanderBrief !== null && (commanderBrief as { detected?: unknown }).detected === true;
|
||
}
|
||
|
||
export function codeQueueBoardCommanderBriefHint(boardIssueNumber: number, body?: string): Record<string, unknown> | null {
|
||
if (boardIssueNumber !== CODE_QUEUE_BOARD_TARGET_ISSUE) return null;
|
||
const headings = body === undefined ? [] : commanderBriefUpdateHeadings(body);
|
||
const hwlabProductRouting = codeQueueBoardHwlabProductRoutingHint(boardIssueNumber, body);
|
||
const commanderBriefDetected = headings.length > 0;
|
||
const hwlabProductDetected = codeQueueBoardHintHasHwlabProductRouting({ hwlabProductRouting });
|
||
return {
|
||
warning: "#20 is the long-term UniDesk commander/Code Queue/CLI/infra governance board only; do not write daily commander brief updates or HWLAB product/user issue tracking into #20.",
|
||
route: commanderBriefDetected
|
||
? "Move daily progress notes to the daily rolling commander brief issue referenced in #20's 指挥简报索引, using --body-profile commander-brief."
|
||
: hwlabProductDetected
|
||
? "Move HWLAB product/user issue tracking to pikasTech/HWLAB; keep #20 limited to UniDesk commander/Code Queue/CLI/infra governance."
|
||
: "Use #20 only for UniDesk commander/Code Queue/CLI/infra governance rows.",
|
||
forbiddenHeadings: ["## 更新 YYYY-MM-DD HH:mm 北京时间", "## YYYY-MM-DD HH:mm 北京时间指挥更新", "### YYYY-MM-DD HH:mm CST:..."],
|
||
suggestedCommand: "bun scripts/cli.ts gh issue update <daily-brief-issue> --mode replace --body-stdin --body-profile commander-brief --expect-body-sha <sha> <<'EOF'\n<reviewed body>\nEOF",
|
||
detected: commanderBriefDetected || hwlabProductDetected,
|
||
detectedHeadings: headings,
|
||
commanderBrief: {
|
||
detected: commanderBriefDetected,
|
||
detectedHeadings: headings,
|
||
},
|
||
hwlabProductRouting,
|
||
};
|
||
}
|
||
|
||
export function extractTimelineSections(markdown: string): CommanderBriefSection[] {
|
||
const normalized = normalizeNewlines(markdown);
|
||
const lines = normalized.split("\n");
|
||
const sections: CommanderBriefSection[] = [];
|
||
let currentStart = -1;
|
||
for (let index = 0; index < lines.length; index += 1) {
|
||
if (!isTimelineHeading(lines[index])) continue;
|
||
if (currentStart !== -1) {
|
||
sections.push({
|
||
heading: lines[currentStart].trimEnd(),
|
||
text: lines.slice(currentStart, index).join("\n").trimEnd(),
|
||
startLine: currentStart + 1,
|
||
});
|
||
}
|
||
currentStart = index;
|
||
}
|
||
if (currentStart !== -1) {
|
||
sections.push({
|
||
heading: lines[currentStart].trimEnd(),
|
||
text: lines.slice(currentStart).join("\n").trimEnd(),
|
||
startLine: currentStart + 1,
|
||
});
|
||
}
|
||
return sections.filter((section) => section.text.length > 0);
|
||
}
|
||
|
||
export function commanderBriefDiff(oldBodyRaw: string, newBodyRaw: string): CommanderBriefDiff {
|
||
const oldBody = normalizeNewlines(oldBodyRaw);
|
||
const newBody = normalizeNewlines(newBodyRaw);
|
||
if (oldBody === newBody) {
|
||
return {
|
||
ok: false,
|
||
mode: "identical",
|
||
message: "",
|
||
chars: 0,
|
||
sections: [],
|
||
sectionCount: 0,
|
||
skippedReason: "issue body is unchanged",
|
||
};
|
||
}
|
||
|
||
if (newBody.startsWith(oldBody)) {
|
||
const suffix = newBody.slice(oldBody.length);
|
||
const sections = extractTimelineSections(suffix).map((section) => section.text);
|
||
const message = sections.join("\n\n").trim();
|
||
if (message.length === 0) {
|
||
return {
|
||
ok: false,
|
||
mode: "append-only",
|
||
message: "",
|
||
chars: 0,
|
||
sections: [],
|
||
sectionCount: 0,
|
||
skippedReason: "append-only suffix contains no new commander brief update section",
|
||
};
|
||
}
|
||
return { ok: true, mode: "append-only", message, chars: message.length, sections, sectionCount: sections.length };
|
||
}
|
||
|
||
const oldSections = new Set(extractTimelineSections(oldBody).map((section) => section.text));
|
||
const allNewSections = extractTimelineSections(newBody);
|
||
const newSections = allNewSections.map((section) => section.text).filter((section) => !oldSections.has(section));
|
||
if (newSections.length === 0) {
|
||
return {
|
||
ok: false,
|
||
mode: "heading-diff",
|
||
message: "",
|
||
chars: 0,
|
||
sections: [],
|
||
sectionCount: 0,
|
||
skippedReason: "no new commander brief update section found; non-timeline edits are not notified",
|
||
};
|
||
}
|
||
const duplicateNewHeadings = allNewSections.some((section, index) => allNewSections.findIndex((candidate) => candidate.heading === section.heading) !== index);
|
||
const oldHeadings = new Set(extractTimelineSections(oldBody).map((section) => section.heading));
|
||
const ambiguousChangedHeadings = allNewSections.some((section) => oldHeadings.has(section.heading) && !oldSections.has(section.text));
|
||
if (duplicateNewHeadings || ambiguousChangedHeadings) {
|
||
return {
|
||
ok: false,
|
||
mode: "unreliable",
|
||
message: "",
|
||
chars: 0,
|
||
sections: [],
|
||
sectionCount: 0,
|
||
skippedReason: duplicateNewHeadings
|
||
? "duplicate update headings make commander brief diff unreliable"
|
||
: "an existing update section changed; cannot reliably isolate only new timeline content",
|
||
};
|
||
}
|
||
const message = newSections.join("\n\n").trim();
|
||
return { ok: true, mode: "heading-diff", message, chars: message.length, sections: newSections, sectionCount: newSections.length };
|
||
}
|