#!/usr/bin/env bun /** * Build the GStack 2.0 backlog map from frozen GitHub API snapshots. * * The GitHub issues endpoint includes pull requests, while the pull-request * endpoint repeats those records with PR-specific fields. This generator * recursively flattens page arrays/envelopes, reconciles those duplicates by * repository + number, and retains every source occurrence for auditability. * * Classification is deliberately conservative: titles are considered first; * bodies only match narrow two-signal expressions; unmatched work lands in the * governance intake with NEEDS_EVIDENCE. The required port PR list is the only * hand-curated mapping table. */ import * as fs from 'fs'; import * as path from 'path'; import { createHash } from 'crypto'; const DEFAULT_BASE_SHA = 'bb57306d98c97011b0919c6132705a15b1579781'; const REQUIRED_PORT_PRS = [ 610, 645, 679, 884, 1071, 1484, 1636, 1777, 1920, 2014, 2023, 2030, 2037, 2141, 2186, 2189, ] as const; const COMPONENTS = [ 'plan', 'design', 'qa', 'debug', 'review', 'ship', 'runtime-paths-state', 'runtime-browser', 'runtime-ios', 'runtime-context-dev', 'runtime-pdf-diagram', 'installation-migration', 'docs-governance', ] as const; const DISPOSITIONS = [ 'PORT_JUDGMENT', 'FIX_IN_GSTACK_2', 'SUPERSEDED_BY_CONSOLIDATION', 'RETAIN_INTERNAL', 'DEFER_COMMUNITY', 'NEEDS_EVIDENCE', 'NOT_CORE', ] as const; const CATEGORIES = [ 'infrastructure', 'judgment', 'workflow', 'context', 'UX', 'governance', 'not-core', ] as const; const JUDGMENT_MODULES = [ 'office-hours', 'ceo-review', 'engineering-review', 'dx-review', 'specification', 'consultation', 'alternatives', 'html-generation', 'plan-review', 'live-review', 'ios-hig-review', 'web-qa', 'report-only', 'fix-and-verify', 'performance', 'developer-experience', 'ios-qa', 'investigation', 'ios-fix', 'core-review', 'security-review', 'health', 'outside-voice', 'release', 'land-deploy', 'canary', 'docs', 'none', ] as const; const ROOT_CAUSE_FAMILIES = [ 'installation-and-migration', 'host-capability-and-portability', 'path-state-and-lifecycle', 'browser-control-and-evidence', 'device-control-and-evidence', 'context-memory-and-isolation', 'artifact-generation-and-rendering', 'workflow-orchestration-and-handoffs', 'judgment-quality-and-calibration', 'verification-and-regression-coverage', 'security-trust-and-governance', 'documentation-and-discoverability', 'out-of-scope-or-insufficient-signal', ] as const; type Component = typeof COMPONENTS[number]; type Disposition = typeof DISPOSITIONS[number]; type Category = typeof CATEGORIES[number]; type JudgmentModule = typeof JUDGMENT_MODULES[number]; type RootCauseFamily = typeof ROOT_CAUSE_FAMILIES[number]; interface GitHubRecord { [key: string]: unknown; number: number; title: string; body?: string | null; html_url?: string; url?: string; repository_url?: string; state?: string; state_reason?: string | null; draft?: boolean; created_at?: string; updated_at?: string; closed_at?: string | null; merged_at?: string | null; pull_request?: unknown; user?: { login?: string }; labels?: Array<{ name?: string }>; base?: { ref?: string; repo?: { full_name?: string } }; head?: { ref?: string; repo?: { full_name?: string } }; } interface GitHubFileRecord { filename: string; status?: string; additions?: number; deletions?: number; changes?: number; } interface SnapshotSpec { id: string; repo: string; kind: 'open-items' | 'open-prs' | 'labels'; file: string; } interface Mapping { rootCauseFamily: RootCauseFamily; component: Component; disposition: Disposition; replacementTest: string; relatedJudgmentModule: JudgmentModule; category: Category; classification: { rule: string; input: 'manual-required-pr' | 'title' | 'title+body' | 'fallback'; note: string; }; } interface ComponentRule { id: string; component: Component; rootCauseFamily: RootCauseFamily; category: Category; title: RegExp; body?: RegExp; } const REPLACEMENT_TESTS: Record = { plan: { id: 'judgment.plan.contract', assertion: 'Replay a curated plan fixture and assert the recommendation, rationale, scope gate, and user-control point.', }, design: { id: 'judgment.design.contract', assertion: 'Replay representative design inputs and assert hierarchy, constraint preservation, evidence, and explicit taste decisions.', }, qa: { id: 'workflow.qa.evidence-contract', assertion: 'Exercise a real repro and assert evidence-before-finding, stable artifact paths, and report/fix mode boundaries.', }, debug: { id: 'workflow.debug.root-cause-contract', assertion: 'Reproduce the fault and assert investigation evidence precedes the smallest verified fix.', }, review: { id: 'judgment.review.precision-contract', assertion: 'Run seeded true-positive and false-positive diffs and assert calibrated findings with file-and-line evidence.', }, ship: { id: 'workflow.ship.state-machine-contract', assertion: 'Replay clean, dirty, failing-CI, reviewer-gated, and deploy-failure states without skipping an irreversible gate.', }, 'runtime-paths-state': { id: 'runtime.paths-state.matrix-contract', assertion: 'Resolve paths and process state across supported OS, shell, install-root, environment, and stale-state fixtures.', }, 'runtime-browser': { id: 'runtime.browser.lifecycle-contract', assertion: 'Launch, attach, act, capture evidence, recover from a busy daemon, and shut down without losing live session state.', }, 'runtime-ios': { id: 'runtime.ios.device-contract', assertion: 'Connect, authorize, inspect, act, capture, reconnect, and clean up against a device/bridge contract fixture.', }, 'runtime-context-dev': { id: 'runtime.context-isolation-contract', assertion: 'Round-trip context and learning state across project, branch, user, expiry, malformed-state, and concurrent-writer fixtures.', }, 'runtime-pdf-diagram': { id: 'runtime.artifact-render-contract', assertion: 'Generate, render, reopen, and visually verify deterministic PDF/diagram artifacts with hostile text and page-size fixtures.', }, 'installation-migration': { id: 'installation.host-matrix-contract', assertion: 'Install, upgrade, relink, and uninstall from standard and custom roots on every supported host without shadowing user content.', }, 'docs-governance': { id: 'docs.generated-source-contract', assertion: 'Regenerate canonical docs/catalogs and assert source ownership, link integrity, freshness, and an evidence-backed intake decision.', }, }; const COMPONENT_RULES: ComponentRule[] = [ { id: 'installation-migration', component: 'installation-migration', rootCauseFamily: 'installation-and-migration', category: 'infrastructure', title: /\b(setup|install(?:er|ation|ing)?|uninstall|upgrade|migration|relink|symlink|plugin|marketplace|skill(?:s)?\.sh|distribution|package manager)\b/i, body: /(?:install|setup|upgrade).{0,100}(?:root|path|host|symlink)|(?:symlink|plugin).{0,100}(?:missing|broken|shadow)/is, }, { id: 'runtime-browser', component: 'runtime-browser', rootCauseFamily: 'browser-control-and-evidence', category: 'infrastructure', title: /\b(browse|browser|chrome|chromium|playwright|puppeteer|cookie|cdp|extension|sidebar|headless|screenshot|snapshot|scrape)\b/i, body: /(?:browser|chrome|chromium|playwright).{0,100}(?:daemon|session|launch|attach|cookie|screenshot)|(?:screenshot|snapshot).{0,100}(?:browser|evidence)/is, }, { id: 'runtime-ios', component: 'runtime-ios', rootCauseFamily: 'device-control-and-evidence', category: 'infrastructure', title: /\b(iOS|iPhone|SwiftUI|Xcode|CoreDevice|Tailscale|mobile|Appium|device bridge)\b/i, body: /(?:iOS|iPhone|SwiftUI|Xcode|CoreDevice|Appium).{0,100}(?:device|bridge|test|screen|connect)/is, }, { id: 'runtime-pdf-diagram', component: 'runtime-pdf-diagram', rootCauseFamily: 'artifact-generation-and-rendering', category: 'workflow', title: /\b(PDF|diagram|mermaid|excalidraw|SVG|typst|pandoc|artifact render(?:er|ing)?|render(?:er|ing)? (?:PDF|diagram|SVG))\b/i, body: /(?:PDF|diagram|mermaid|excalidraw).{0,100}(?:render|layout|export|artifact)/is, }, { id: 'runtime-context-dev', component: 'runtime-context-dev', rootCauseFamily: 'context-memory-and-isolation', category: 'context', title: /\b(context|memory|gbrain|learning|learnings|retro|profile|taste|preference|resume|restore|session intelligence|compaction|transcript)\b/i, body: /(?:context|memory|gbrain|learning|preference).{0,100}(?:state|store|persist|isolation|branch|session|stale)/is, }, { id: 'ship', component: 'ship', rootCauseFamily: 'workflow-orchestration-and-handoffs', category: 'workflow', title: /\b(ship|shipping|deploy|deployment|land-and-deploy|canary|release|merge|version(?:ing)?|changelog|pull request|\bPR\b|CI\/CD|GitHub Actions)\b/i, body: /(?:ship|deploy|merge|release|pull request).{0,100}(?:gate|workflow|reviewer|version|failure|state)/is, }, { id: 'review', component: 'review', rootCauseFamily: 'judgment-quality-and-calibration', category: 'judgment', title: /\b(review|reviewer|security|CSO|threat|OWASP|health check|outside voice|codex|claude challenge|audit finding)\b/i, body: /(?:review|security|audit).{0,100}(?:finding|false positive|checklist|threat|trust|diff)/is, }, { id: 'debug', component: 'debug', rootCauseFamily: 'workflow-orchestration-and-handoffs', category: 'workflow', title: /\b(investigat(?:e|ion)|debug(?:ger|ging)?|root cause|bisect|stack trace|reproduc(?:e|tion)|bug fixer)\b/i, body: /(?:debug|investigate|root cause|bisect).{0,100}(?:repro|evidence|fix|regression)/is, }, { id: 'qa', component: 'qa', rootCauseFamily: 'verification-and-regression-coverage', category: 'workflow', title: /\b(QA|qa-only|test(?:ing|s)?|eval(?:uation|s)?|benchmark|performance|regression|evidence|fixture|coverage|devex|developer experience)\b/i, body: /(?:test|eval|QA|benchmark|evidence).{0,100}(?:fixture|assert|regression|coverage|finding|performance)/is, }, { id: 'design', component: 'design', rootCauseFamily: 'judgment-quality-and-calibration', category: 'judgment', title: /\b(design|UI|UX|aesthetic|visual|CSS|HTML|font|typography|color|layout|wireframe|responsive|accessibility)\b/i, body: /(?:design|UI|UX|aesthetic|visual).{0,100}(?:system|review|layout|hierarchy|accessibility|HTML|CSS)/is, }, { id: 'plan', component: 'plan', rootCauseFamily: 'judgment-quality-and-calibration', category: 'judgment', title: /\b(plan|planning|autoplan|office.hours|CEO|founder|engineering review|architecture|spec(?:ification)?|scope|brainstorm|consultation|data model|schema)\b/i, body: /(?:plan|scope|architecture|specification|data model).{0,100}(?:decision|review|tradeoff|phase|recommendation)/is, }, { id: 'docs-governance', component: 'docs-governance', rootCauseFamily: 'documentation-and-discoverability', category: 'governance', title: /\b(doc(?:s|umentation)?|README|tutorial|how-to|reference|translation|localization|locale|language|catalog|frontmatter|license|governance|community)\b/i, body: /(?:docs|documentation|README|translation|catalog).{0,100}(?:missing|stale|generate|link|language|discover)/is, }, { id: 'runtime-paths-state', component: 'runtime-paths-state', rootCauseFamily: 'path-state-and-lifecycle', category: 'infrastructure', title: /\b(path|state|Windows|Linux|macOS|WSL|shell|bash|PowerShell|Bun|process|lockfile|environment variable|env var|filesystem|directory|runtime)\b/i, body: /(?:path|state|process|runtime|filesystem|directory).{0,100}(?:Windows|Linux|macOS|shell|lock|stale|environment|root)/is, }, ]; const REQUIRED_OVERRIDES: Record> = { 610: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'review', relatedJudgmentModule: 'core-review', category: 'judgment' }, 645: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'review', relatedJudgmentModule: 'core-review', category: 'judgment' }, 679: { rootCauseFamily: 'documentation-and-discoverability', component: 'docs-governance', relatedJudgmentModule: 'docs', category: 'UX' }, 884: { rootCauseFamily: 'security-trust-and-governance', component: 'ship', relatedJudgmentModule: 'land-deploy', category: 'governance' }, 1071: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'plan', relatedJudgmentModule: 'engineering-review', category: 'judgment' }, 1484: { rootCauseFamily: 'verification-and-regression-coverage', component: 'qa', relatedJudgmentModule: 'web-qa', category: 'workflow' }, 1636: { rootCauseFamily: 'context-memory-and-isolation', component: 'runtime-context-dev', relatedJudgmentModule: 'none', category: 'context' }, 1777: { rootCauseFamily: 'context-memory-and-isolation', component: 'runtime-context-dev', relatedJudgmentModule: 'none', category: 'context' }, 1920: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'design', relatedJudgmentModule: 'live-review', category: 'judgment' }, 2014: { rootCauseFamily: 'workflow-orchestration-and-handoffs', component: 'plan', relatedJudgmentModule: 'plan-review', category: 'judgment' }, 2023: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'plan', relatedJudgmentModule: 'plan-review', category: 'judgment' }, 2030: { rootCauseFamily: 'context-memory-and-isolation', component: 'runtime-context-dev', relatedJudgmentModule: 'none', category: 'context' }, 2037: { rootCauseFamily: 'verification-and-regression-coverage', component: 'runtime-context-dev', relatedJudgmentModule: 'none', category: 'context' }, 2141: { rootCauseFamily: 'judgment-quality-and-calibration', component: 'review', relatedJudgmentModule: 'core-review', category: 'judgment' }, 2186: { rootCauseFamily: 'security-trust-and-governance', component: 'debug', relatedJudgmentModule: 'investigation', category: 'governance' }, 2189: { rootCauseFamily: 'verification-and-regression-coverage', component: 'design', relatedJudgmentModule: 'consultation', category: 'judgment' }, }; function parseArgs(argv: string[]) { const result = { snapshotDir: '/tmp', portPrDir: '/tmp/gstack2-port-prs', output: path.resolve(import.meta.dir, '../../docs/gstack-2/BACKLOG-MAP.json'), baseSha: DEFAULT_BASE_SHA, }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; const value = argv[index + 1]; if (argument === '--snapshot-dir' && value) result.snapshotDir = value; else if (argument === '--port-pr-dir' && value) result.portPrDir = value; else if (argument === '--output' && value) result.output = path.resolve(value); else if (argument === '--base-sha' && value) result.baseSha = value; else throw new Error(`Unknown or incomplete argument: ${argument}`); index += 1; } return result; } function readJSON(file: string): unknown { return JSON.parse(fs.readFileSync(file, 'utf8')); } function sha256File(file: string): string { return createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } function isGitHubRecord(value: unknown): value is GitHubRecord { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const candidate = value as Record; return Number.isInteger(candidate.number) && typeof candidate.title === 'string'; } function isLabelRecord(value: unknown): value is { name: string; color?: string; description?: string | null; default?: boolean } { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const candidate = value as Record; return typeof candidate.name === 'string' && typeof candidate.color === 'string' && !('number' in candidate); } function isFileRecord(value: unknown): value is GitHubFileRecord { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; return typeof (value as Record).filename === 'string'; } function flattenMatching(value: unknown, predicate: (candidate: unknown) => candidate is T): T[] { if (Array.isArray(value)) return value.flatMap((entry) => flattenMatching(entry, predicate)); if (predicate(value)) return [value]; if (!value || typeof value !== 'object') return []; const envelope = value as Record; for (const key of ['items', 'nodes', 'data', 'results', 'pages']) { if (key in envelope) return flattenMatching(envelope[key], predicate); } return []; } function pageCount(value: unknown): number { if (!Array.isArray(value)) return 1; if (value.length === 0) return 0; return value.every(Array.isArray) ? value.length : 1; } function repoFromRecord(record: GitHubRecord, fallback: string): string { if (typeof record.repository_url === 'string') { const match = record.repository_url.match(/repos\/([^/]+\/[^/]+)$/); if (match) return match[1]; } const baseRepo = record.base?.repo?.full_name; if (baseRepo) return baseRepo; const html = record.html_url ?? record.url; if (html) { const match = html.match(/(?:github\.com|api\.github\.com\/repos)\/([^/]+\/[^/]+)/); if (match) return match[1]; } return fallback; } function replacementTest(component: Component): string { return REPLACEMENT_TESTS[component].id; } function relatedJudgmentModule(component: Component, text: string): JudgmentModule { if (/office.hours/i.test(text)) return 'office-hours'; if (/\bCEO\b|founder/i.test(text)) return 'ceo-review'; if (/plan.eng|engineering review|architecture|data model|schema|JSONField/i.test(text)) return 'engineering-review'; if (/plan.devex|\bdx\b|developer experience|devex/i.test(text)) return 'dx-review'; if (/\bspec(?:ification)?\b/i.test(text)) return 'specification'; if (/design.consultation|design system/i.test(text)) return 'consultation'; if (/design.shotgun|alternative|variant/i.test(text)) return 'alternatives'; if (/design.html|html generation/i.test(text)) return 'html-generation'; if (/plan.design|design plan|autoplan/i.test(text)) return 'plan-review'; if (/ios.design|\bHIG\b/i.test(text)) return 'ios-hig-review'; if (/design.review|visual audit/i.test(text)) return 'live-review'; if (/qa.only|report.only/i.test(text)) return 'report-only'; if (/ios.qa/i.test(text)) return 'ios-qa'; if (/devex.review|developer experience/i.test(text)) return 'developer-experience'; if (/benchmark|performance/i.test(text)) return 'performance'; if (/ios.fix/i.test(text)) return 'ios-fix'; if (/investigat|debug|bisect|root cause/i.test(text)) return 'investigation'; if (/\bCSO\b|security|OWASP|threat/i.test(text)) return 'security-review'; if (/health/i.test(text)) return 'health'; if (/outside voice|codex|claude challenge/i.test(text)) return 'outside-voice'; if (/land.and.deploy|deploy|merge gate/i.test(text)) return 'land-deploy'; if (/canary/i.test(text)) return 'canary'; if (/docs|documentation|README|tutorial/i.test(text)) return 'docs'; const defaults: Partial> = { plan: 'plan-review', design: 'live-review', qa: /fix/i.test(text) ? 'fix-and-verify' : 'web-qa', debug: 'investigation', review: 'core-review', ship: 'release', 'docs-governance': 'docs', }; return defaults[component] ?? 'none'; } function isNotCoreTitle(title: string): boolean { const normalized = title.trim(); return /^(eh|hi|hello|hi bro[^a-z]*|test|testing|سلام|你好|hola)[.!?\s\p{Emoji_Presentation}]*$/iu.test(normalized) || /\b(airdrop|casino|gambling|loan offer|crypto pump|buy followers|dating service)\b/i.test(normalized); } function dispositionFor(record: GitHubRecord, repo: string, category: Category): { value: Disposition; rule: string } { const title = record.title; const labels = (record.labels ?? []).map((label) => label.name ?? '').join(' '); const isPR = Boolean(record.pull_request || record.base || record.head); if (repo === 'time-attack/gstack') return { value: 'RETAIN_INTERNAL', rule: 'time-attack-internal' }; if (category === 'not-core' || /\b(invalid|wontfix)\b/i.test(labels)) return { value: 'NOT_CORE', rule: 'not-core-signal' }; if (/\b(duplicate|duplicated|boilerplate|consolidat|same frontmatter name|alias.+shadow|shadow.+alias)\b/i.test(title)) { return { value: 'SUPERSEDED_BY_CONSOLIDATION', rule: 'consolidation-signal' }; } if (/\b(translation|localization|locale|add (?:initial )?support for|adapter|new host|community skill|language pack)\b/i.test(title) && !/\b(fix|bug|broken|crash|fail|missing)\b/i.test(title)) { return { value: 'DEFER_COMMUNITY', rule: 'community-expansion' }; } if (!isPR && /\b(fix|bug|broken|breaks?|crash(?:es)?|fail(?:s|ure|ing)?|missing|leak(?:s|ed)?|silently|stale|incorrect|cannot|can't|doesn't|does not|hangs?|corrupt)\b/i.test(title)) { return { value: 'FIX_IN_GSTACK_2', rule: 'open-bug' }; } return { value: 'NEEDS_EVIDENCE', rule: isPR ? 'unvetted-community-pr' : 'ambiguous-open-issue' }; } function classify(record: GitHubRecord, repo: string): Mapping { if (repo === 'garrytan/gstack' && REQUIRED_PORT_PRS.includes(record.number as typeof REQUIRED_PORT_PRS[number])) { const override = REQUIRED_OVERRIDES[record.number]; return { ...override, disposition: 'PORT_JUDGMENT', replacementTest: replacementTest(override.component), classification: { rule: `required-port-pr-${record.number}`, input: 'manual-required-pr', note: 'Explicitly selected port candidate; mapping reviewed against its detail and changed-file snapshots.', }, }; } if (isNotCoreTitle(record.title)) { return { rootCauseFamily: 'out-of-scope-or-insufficient-signal', component: 'docs-governance', disposition: repo === 'time-attack/gstack' ? 'RETAIN_INTERNAL' : 'NOT_CORE', replacementTest: replacementTest('docs-governance'), relatedJudgmentModule: 'none', category: 'not-core', classification: { rule: 'obvious-no-signal-or-spam-title', input: 'title', note: 'Only exact low-information greetings/tests and explicit spam terms are classified not-core.', }, }; } const title = record.title; const body = typeof record.body === 'string' ? record.body.slice(0, 12_000) : ''; let selected = COMPONENT_RULES.find((rule) => rule.title.test(title)); let input: Mapping['classification']['input'] = 'title'; if (!selected) { selected = COMPONENT_RULES.find((rule) => rule.body?.test(body)); input = selected ? 'title+body' : 'fallback'; } if (!selected) { const disposition = dispositionFor(record, repo, 'governance'); return { rootCauseFamily: 'out-of-scope-or-insufficient-signal', component: 'docs-governance', disposition: disposition.value, replacementTest: replacementTest('docs-governance'), relatedJudgmentModule: 'none', category: 'governance', classification: { rule: `fallback:${disposition.rule}`, input: 'fallback', note: 'No conservative title or two-signal body rule matched; retained for governance triage.', }, }; } let rootCauseFamily = selected.rootCauseFamily; let category = selected.category; if (/\b(security|untrusted|secret|credential|injection|auth(?:entication|orization)?|permission|OWASP|threat|vulnerab|CVE|API key|key bytes)\b/i.test(title)) { rootCauseFamily = 'security-trust-and-governance'; category = 'governance'; } else if (/\b(test|eval|regression|fixture|assertion|coverage|false positive|false negative|evidence)\b/i.test(title)) { rootCauseFamily = 'verification-and-regression-coverage'; } else if (/\b(host|adapter|Windows|Linux|macOS|WSL|Codex|Claude|Gemini|OpenCode|Cursor|Kiro|Copilot|Antigravity)\b/i.test(title) && ['installation-migration', 'runtime-paths-state'].includes(selected.component)) { rootCauseFamily = 'host-capability-and-portability'; } if (/\b(language|translation|localization|locale|accessibility|responsive|user-facing|UX)\b/i.test(title)) category = 'UX'; const disposition = dispositionFor(record, repo, category); return { rootCauseFamily, component: selected.component, disposition: disposition.value, replacementTest: replacementTest(selected.component), relatedJudgmentModule: relatedJudgmentModule(selected.component, title), category, classification: { rule: `${selected.id}:${disposition.rule}`, input, note: input === 'title+body' ? 'Title was ambiguous; a narrow two-signal body rule selected the component.' : 'First matching ordered title rule selected the component; disposition remained conservative.', }, }; } function sortedCount(values: T[]): Record { const counts = new Map(); for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1); return Object.fromEntries([...counts.entries()].sort(([left], [right]) => left.localeCompare(right))); } function main() { const args = parseArgs(process.argv.slice(2)); const snapshotSpecs: SnapshotSpec[] = [ { id: 'time-attack/open-items', repo: 'time-attack/gstack', kind: 'open-items', file: path.join(args.snapshotDir, 'gstack2-open-items-pages.json') }, { id: 'time-attack/open-prs', repo: 'time-attack/gstack', kind: 'open-prs', file: path.join(args.snapshotDir, 'gstack2-open-prs-pages.json') }, { id: 'time-attack/labels', repo: 'time-attack/gstack', kind: 'labels', file: path.join(args.snapshotDir, 'gstack2-label-pages.json') }, { id: 'garrytan/open-items', repo: 'garrytan/gstack', kind: 'open-items', file: path.join(args.snapshotDir, 'gstack2-upstream-open-items-pages.json') }, { id: 'garrytan/open-prs', repo: 'garrytan/gstack', kind: 'open-prs', file: path.join(args.snapshotDir, 'gstack2-upstream-open-prs-pages.json') }, { id: 'garrytan/labels', repo: 'garrytan/gstack', kind: 'labels', file: path.join(args.snapshotDir, 'gstack2-upstream-label-pages.json') }, ]; const sourceSnapshots: Array> = []; const labelsByRepo = new Map>>(); const accumulators = new Map; openItemSeen: boolean; openPrSeen: boolean; requiredDetailSeen: boolean; files: GitHubFileRecord[]; }>(); let openEndpointRecords = 0; for (const spec of snapshotSpecs) { const raw = readJSON(spec.file); if (spec.kind === 'labels') { const labels = flattenMatching(raw, isLabelRecord) .map((label) => ({ name: label.name, color: label.color ?? null, description: label.description ?? null, default: label.default ?? false })) .sort((left, right) => String(left.name).localeCompare(String(right.name))); labelsByRepo.set(spec.repo, labels); sourceSnapshots.push({ id: spec.id, path: spec.file, sha256: sha256File(spec.file), pageArrays: pageCount(raw), flattenedRecords: labels.length }); continue; } const records = flattenMatching(raw, isGitHubRecord); openEndpointRecords += records.length; sourceSnapshots.push({ id: spec.id, path: spec.file, sha256: sha256File(spec.file), pageArrays: pageCount(raw), flattenedRecords: records.length }); for (const record of records) { const repo = repoFromRecord(record, spec.repo); const key = `${repo}#${record.number}`; const existing = accumulators.get(key); if (existing) { existing.record = { ...existing.record, ...record }; existing.sources.add(spec.id); if (spec.kind === 'open-items') existing.openItemSeen = true; if (spec.kind === 'open-prs') existing.openPrSeen = true; } else { accumulators.set(key, { repo, number: record.number, record: { ...record }, sources: new Set([spec.id]), openItemSeen: spec.kind === 'open-items', openPrSeen: spec.kind === 'open-prs', requiredDetailSeen: false, files: [], }); } } } for (const number of REQUIRED_PORT_PRS) { const detailFile = path.join(args.portPrDir, `${number}.json`); const filesFile = path.join(args.portPrDir, `${number}-files.json`); const rawDetail = readJSON(detailFile); const rawFiles = readJSON(filesFile); const detailRecords = flattenMatching(rawDetail, isGitHubRecord); const fileRecords = flattenMatching(rawFiles, isFileRecord) .map((file) => ({ filename: file.filename, status: file.status, additions: file.additions, deletions: file.deletions, changes: file.changes })) .sort((left, right) => left.filename.localeCompare(right.filename)); if (detailRecords.length !== 1) throw new Error(`Expected one PR detail record for #${number}, found ${detailRecords.length}`); const detail = detailRecords[0]; const repo = repoFromRecord(detail, 'garrytan/gstack'); const key = `${repo}#${number}`; const existing = accumulators.get(key); if (!existing) throw new Error(`Required PR ${key} is absent from the open snapshots`); existing.record = { ...existing.record, ...detail }; existing.sources.add(`required-port/${number}`); existing.requiredDetailSeen = true; existing.files = fileRecords; sourceSnapshots.push({ id: `required-port/${number}/detail`, path: detailFile, sha256: sha256File(detailFile), pageArrays: pageCount(rawDetail), flattenedRecords: 1 }); sourceSnapshots.push({ id: `required-port/${number}/files`, path: filesFile, sha256: sha256File(filesFile), pageArrays: pageCount(rawFiles), flattenedRecords: fileRecords.length }); } const items = [...accumulators.values()] .sort((left, right) => left.repo.localeCompare(right.repo) || left.number - right.number) .map((entry) => { const record = entry.record; const mapping = classify(record, entry.repo); const isPR = entry.openPrSeen || Boolean(record.pull_request || record.base || record.head); const body = typeof record.body === 'string' ? record.body : ''; const portTrace = entry.requiredDetailSeen ? { required: true, detailSnapshot: path.join(args.portPrDir, `${entry.number}.json`), filesSnapshot: path.join(args.portPrDir, `${entry.number}-files.json`), changedFileCount: entry.files.length, files: entry.files, } : null; return { id: `${entry.repo}#${entry.number}`, repo: entry.repo, number: entry.number, kind: isPR ? 'pull-request' : 'issue', title: record.title, url: record.html_url ?? record.url ?? null, author: record.user?.login ?? null, dates: { createdAt: record.created_at ?? null, updatedAt: record.updated_at ?? null, closedAt: record.closed_at ?? null, mergedAt: record.merged_at ?? null, }, state: record.state ?? null, stateReason: record.state_reason ?? null, draft: isPR ? (record.draft ?? false) : null, baseRef: isPR ? (record.base?.ref ?? null) : null, headRef: isPR ? (record.head?.ref ?? null) : null, labels: (record.labels ?? []).map((label) => label.name).filter(Boolean).sort(), bodyPresent: body.length > 0, bodyLength: body.length, sourceOccurrences: [...entry.sources].sort(), sourceReconciliation: { openItems: entry.openItemSeen, openPRs: entry.openPrSeen, requiredDetail: entry.requiredDetailSeen, }, portTrace, ...mapping, }; }); const requiredPortPRs = REQUIRED_PORT_PRS.map((number) => { const item = items.find((candidate) => candidate.repo === 'garrytan/gstack' && candidate.number === number); if (!item || !item.portTrace) throw new Error(`Required port trace missing for garrytan/gstack#${number}`); return { number, itemId: item.id, title: item.title, url: item.url, author: item.author, dates: item.dates, component: item.component, relatedJudgmentModule: item.relatedJudgmentModule, replacementTest: item.replacementTest, detailSnapshot: item.portTrace.detailSnapshot, filesSnapshot: item.portTrace.filesSnapshot, changedFileCount: item.portTrace.changedFileCount, traced: true, }; }); const endpointOccurrenceCount = items.reduce((total, item) => total + Number(item.sourceReconciliation.openItems) + Number(item.sourceReconciliation.openPRs), 0); const snapshotAsOf = items .map((item) => item.dates.updatedAt) .filter((date): date is string => typeof date === 'string') .sort() .at(-1) ?? null; const output = { schemaVersion: 1, purpose: 'Deterministic evidence map from the frozen time-attack/gstack and garrytan/gstack open-backlog snapshots into GStack 2.0 ownership and verification contracts.', baseSha: args.baseSha, snapshotAsOf, generator: 'scripts/gstack2/generate-backlog-map.ts', methodology: { flattening: 'Recursively flatten array pages and common items/nodes/data/results/pages envelopes; never select only the first page.', reconciliation: 'The issues endpoint includes PRs. Records are keyed by repo#number, PR-specific fields enrich issue records, and sourceOccurrences/sourceReconciliation retain raw endpoint provenance.', heuristics: 'Ordered title rules first. Bodies are capped at 12,000 characters and used only by narrow two-signal expressions. No network, LLM, label mutation, or repository mutation occurs.', fallback: 'Ambiguous items remain in docs-governance intake with NEEDS_EVIDENCE and out-of-scope-or-insufficient-signal; this is triage ownership, not a claim that the item is documentation work.', requiredPorts: 'Only the enumerated required PRs receive hand-reviewed PORT_JUDGMENT overrides; their detail and changed-file snapshots are traced below and on each item.', }, taxonomy: { categories: CATEGORIES, components: COMPONENTS, dispositions: DISPOSITIONS, relatedJudgmentModules: JUDGMENT_MODULES, rootCauseFamilies: ROOT_CAUSE_FAMILIES, replacementTests: Object.fromEntries(COMPONENTS.map((component) => [REPLACEMENT_TESTS[component].id, { component, assertion: REPLACEMENT_TESTS[component].assertion }])), dispositionMeaning: { PORT_JUDGMENT: 'Required, explicitly traced judgment/policy candidate; port intent, not a blind patch application.', FIX_IN_GSTACK_2: 'Clear open defect signal to solve in the consolidated architecture.', SUPERSEDED_BY_CONSOLIDATION: 'Duplicate/shadow/boilerplate concern absorbed by the consolidated architecture.', RETAIN_INTERNAL: 'Existing time-attack/gstack work that remains part of the internal baseline.', DEFER_COMMUNITY: 'Additive host/language/community expansion held behind core parity.', NEEDS_EVIDENCE: 'Plausible but unverified; requires repro, contract, or product decision before implementation.', NOT_CORE: 'Obvious no-signal/spam or explicitly invalid/wontfix work.', }, }, summary: { uniqueItems: items.length, openEndpointRecords, reconciledEndpointOccurrences: endpointOccurrenceCount, duplicateEndpointRecords: openEndpointRecords - items.length, requiredPortPRs: requiredPortPRs.length, requiredPortPRsTraced: requiredPortPRs.filter((entry) => entry.traced).length, labelCatalogRecords: [...labelsByRepo.values()].reduce((total, labels) => total + labels.length, 0), byRepo: sortedCount(items.map((item) => item.repo)), byKind: sortedCount(items.map((item) => item.kind)), byCategory: sortedCount(items.map((item) => item.category)), byComponent: sortedCount(items.map((item) => item.component)), byDisposition: sortedCount(items.map((item) => item.disposition)), byRootCauseFamily: sortedCount(items.map((item) => item.rootCauseFamily)), byRelatedJudgmentModule: sortedCount(items.map((item) => item.relatedJudgmentModule)), }, sourceSnapshots, labelCatalogs: Object.fromEntries([...labelsByRepo.entries()].sort(([left], [right]) => left.localeCompare(right))), requiredPortPRs, items, }; fs.mkdirSync(path.dirname(args.output), { recursive: true }); fs.writeFileSync(args.output, `${JSON.stringify(output, null, 2)}\n`); process.stdout.write(`Wrote ${items.length} mapped items to ${args.output}\n`); } main();