Files
pikasTech-unidesk/scripts/src/gh/body-profiles.ts
T
2026-06-26 00:16:53 +08:00

230 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };
}