record gstack 2 baseline and backlog map

This commit is contained in:
Sinabina
2026-07-16 17:46:41 -07:00
parent 2c487305d6
commit ce37bd36a9
5 changed files with 35867 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
# GStack 2.0 baseline
This is the measured starting point, not a completion claim. The audit was run
from detached base worktree `/tmp/gstack2-baseline.e2qk7F` at commit
`bb57306d98c97011b0919c6132705a15b1579781` on 2026-07-16.
## Skill surface and size
| Measure | Baseline |
|---|---:|
| Source `SKILL.md.tmpl` templates | 55 |
| Primary generated skill outputs | 54 |
| Generated section files | 16 |
| Primary surface | 61,093 lines / 3,301,287 bytes |
| Audited union | 67,996 lines / 3,695,876 bytes |
| Repeated preambles | 49 |
| Repeated-preamble footprint | 35,802 lines / 2,132,374 bytes |
| Generated hosts | 10 |
The 10 generated hosts observed in the generator/check logs are Claude Code,
OpenAI Codex CLI, Factory Droid, Kiro, OpenCode, Slate, Cursor, OpenClaw,
Hermes, and GBrain.
The catalog-token audit has two materially different numbers:
- Correct YAML parsing of the names and descriptions for 54 skills is about
**1,100 token-equivalents**.
- The official buggy capture reported
`captureBaseline().estTotalCatalogTokens = 4,214`. It applied the same
four-characters-per-token estimate to **16,857 incorrectly parsed
description bytes**. The 4,214 figure is therefore an instrument bug, not the
real catalog cost.
These counts establish the consolidation problem: they do not by themselves
set a target or prove that any repeated instruction is safe to remove.
## Installer/discovery baseline
The old standard-container path has a concrete root-shadow failure. From the
detached base worktree, this command:
```bash
DISABLE_TELEMETRY=1 npx --yes skills add . --list
```
reported `Found 1 skill` and listed only the root `gstack` skill. Under the
skills CLI discovery rule, the shallower root `SKILL.md` is discovered first
and shadows discovery of the nested standard-container skills. Separately,
legacy `./setup` installs 54 host-specific skills. Thus “the legacy installer
works” is not evidence that the standard installer exposes the same surface;
both install roots and collision behavior need migration tests.
## Command baseline
The baseline runner recorded these commands:
```bash
bun install
bun run gen:skill-docs --host all
bun run skill:check
bun run build
bun test
bun run test:windows
bun test design/test
bun test ios-qa/daemon/test
```
Results are intentionally not summarized as “green”:
- dependency installation completed with 297 packages;
- all-host generation reached its final `llms.txt` summary;
- the build reached its final Node server bundle step;
- `skill:check` exited 1 because `claude/SKILL.md` was missing;
- the broad `bun test` capture contains seven explicit failed tests and one
unhandled between-tests error, then ends without Bun's terminal summary or
exit record;
- the Windows-safe run exited 1 on shard 5 of 20;
- the targeted design suite contains one failed timing assertion and no
terminal summary;
- the targeted iOS daemon suite completed with 91 pass / 0 fail.
The precise assertions and log paths are in
[TEST-EVIDENCE.md](./TEST-EVIDENCE.md). An empty
`/tmp/gstack2-baseline-logs/git-status-after.txt` records no tracked/untracked
status output after the baseline runner, but it is not a substitute for test
success.
## Backlog baseline
The frozen API inputs contain 755 unique open GitHub items after reconciling
the issues endpoint (which also contains PRs) with the PR endpoint:
- `garrytan/gstack`: 748 items;
- `time-attack/gstack`: 7 items;
- 326 issues and 429 pull requests in total;
- 1,184 raw open-endpoint records, including 429 reconciled duplicates;
- 16 required upstream PRs with separate detail and changed-file snapshots.
See [BACKLOG-MAP.json](./BACKLOG-MAP.json) for the complete mapping and
`scripts/gstack2/generate-backlog-map.ts` for the deterministic, offline
generator.
+56
View File
@@ -0,0 +1,56 @@
# GStack 2.0 status
**Current state: BASELINED AND MAPPED — NOT IMPLEMENTED, NOT VERIFIED, NOT
DONE.**
This checklist separates evidence capture from delivery so generated plans or
passing narrow tests cannot be mistaken for a completed rewrite.
## Phase checklist
- [x] Freeze the audit base at
`bb57306d98c97011b0919c6132705a15b1579781`.
- [x] Record template/output/section, line, byte, repeated-preamble, catalog,
host, and installer-discovery baselines.
- [x] Capture the baseline command logs without relabeling pre-existing
failures as passes.
- [x] Flatten and reconcile every frozen open issue/PR snapshot into one
deterministic map (755 unique items from 1,184 endpoint records).
- [x] Trace all 16 required PRs to their detail snapshots, changed-file
snapshots, component, judgment module, and replacement-test contract.
- [ ] Review and approve the GStack 2.0 architecture and migration contract.
- [ ] Convert heuristic backlog dispositions into accepted product/engineering
decisions. `NEEDS_EVIDENCE` is intentionally the majority disposition.
- [ ] Implement the consolidated judgment modules and runtime components.
- [ ] Implement the replacement contract tests named in `BACKLOG-MAP.json`.
- [ ] Prove legacy setup migration and standard-installer discovery without
root/nested skill shadowing.
- [ ] Prove behavior and safety parity across all 10 generated hosts.
- [ ] Re-run the broad, Windows-safe, design, and iOS suites with complete
terminal summaries; separate repaired baseline failures from regressions.
- [ ] Produce release evidence and make an explicit ship/no-ship decision.
## Evidence index
| Evidence | Path | State |
|---|---|---|
| Measured baseline | [`BASELINE.md`](./BASELINE.md) | Recorded |
| Baseline command results and failures | [`TEST-EVIDENCE.md`](./TEST-EVIDENCE.md) | Recorded; baseline is not green |
| Deterministic backlog generator | [`../../scripts/gstack2/generate-backlog-map.ts`](../../scripts/gstack2/generate-backlog-map.ts) | Implemented and locally validated |
| Complete mapped backlog | [`BACKLOG-MAP.json`](./BACKLOG-MAP.json) | Generated from frozen snapshots |
| Baseline logs | `/tmp/gstack2-baseline-logs/` | External/ephemeral audit evidence |
| time-attack snapshots | `/tmp/gstack2-{open-items,open-prs,label}-pages.json` | External/ephemeral frozen input |
| garrytan snapshots | `/tmp/gstack2-upstream-{open-items,open-prs,label}-pages.json` | External/ephemeral frozen input |
| Required PR evidence | `/tmp/gstack2-port-prs/{610,...,2189}{,-files}.json` | External/ephemeral frozen input |
## Interpretation rules
- `PORT_JUDGMENT` means “port the decision/policy with a replacement test,”
not “apply the upstream patch verbatim.”
- `FIX_IN_GSTACK_2` still requires a reproduced defect.
- `NEEDS_EVIDENCE`, `DEFER_COMMUNITY`, and
`SUPERSEDED_BY_CONSOLIDATION` remain review decisions, not closed GitHub
items. The generator performs no external mutation.
- The heuristic map is deterministic triage. Title-first and narrow body rules
make it auditable, but human approval is still required before implementation
or upstream disposition changes.
+70
View File
@@ -0,0 +1,70 @@
# GStack 2.0 baseline test evidence
Scope: detached base worktree `/tmp/gstack2-baseline.e2qk7F`, commit
`bb57306d98c97011b0919c6132705a15b1579781`, captured 2026-07-16. These are
baseline observations only; no result below was produced by the GStack 2.0
implementation.
## Command ledger
| Command | Evidence | Observed result |
|---|---|---|
| `bun install` | `/tmp/gstack2-baseline-logs/bun-install.log` | Completed; Bun 1.3.14 reported 297 packages installed. |
| `bun run gen:skill-docs --host all` | `/tmp/gstack2-baseline-logs/gen-all.log` | Reached the final GBrain token table and `llms.txt` summary (`55 skills, 76 browse commands`); no error marker is present. |
| `bun run skill:check` | `/tmp/gstack2-baseline-logs/skill-check.log` | **Exit 1**: `claude/SKILL.md — generated file missing`. The same run reported 54/54 outputs for each of the 10 hosts and fresh generated files, but the missing primary output keeps the command red. |
| `bun run build` | `/tmp/gstack2-baseline-logs/build.log` | Reached the final Node-compatible server bundle (`server-node.mjs`, 0.83 MB); no error marker is present. |
| `bun test` | `/tmp/gstack2-baseline-logs/bun-test.log` | **Not green and incompletely terminated**: 4,292 `(pass)` markers, 488 `(skip)` markers, seven explicit `(fail)` markers, and one unhandled between-tests error. The capture ends after `browse/test/findport.test.ts` without Bun's aggregate summary or exit record. |
| `bun run test:windows` | `/tmp/gstack2-baseline-logs/test-windows.log` | **Exit 1** on shard 5/20. The runner selected 189 Windows-safe files and excluded 155. Completed shard summaries contain 593 passes, 14 skips, and one failed test before the run stopped. |
| `bun test design/test` | `/tmp/gstack2-baseline-logs/design-test.log` | **Not green**: 64 pass markers and one explicit failure. The log ends on daemon shutdown without an aggregate summary or exit record. |
| `bun test ios-qa/daemon/test` | `/tmp/gstack2-baseline-logs/ios-daemon-test.log` | **Pass**: 91 pass, 0 fail, 217 assertions across 10 files. |
`/tmp/gstack2-baseline-logs/git-status-after.txt` is empty, recording no status
entries after the runner. The logs are external audit artifacts and are not
vendored into the repository.
## Pre-existing `bun test` failures
The broad-suite log records these seven explicit failures:
1. `gstack-decision-search --recent / --scope / datamark > --scope filters by scope`
expected output containing `branch-call` and received an empty string.
2. `gstack-gbrain-detect > emits valid JSON even when nothing is configured`
expected status 0 and received 127.
3. `gstack-gbrain-detect > reports gstack_brain_git: true when GSTACK_HOME has a .git dir`
attempted to parse empty output and raised `Unexpected EOF`.
4. `gstack-gbrain-detect > reports gbrain_config + engine when ~/.gbrain/config.json exists`
attempted to parse empty output and raised `Unexpected EOF`.
5. `gstack-gbrain-detect > malformed config returns null engine, does not crash`
expected status 0 and received 127.
6. `gstack-gbrain-detect > detects a mocked gbrain binary on PATH and reports its version`
expected status 0 and received 127.
7. `resolve-user-slug fallback chain > persists resolution to user_slug_at_<hash> on first call`
expected a hashed key and observed `user_slug_at_local: persisttest`.
Separately, `test/gbrain-refresh-install-render.test.ts` raised an unhandled
between-tests error: `Could not locate gbrain-refresh ok) branch`. Because the
log has no terminal Bun summary, the marker counts are evidence of what ran,
not a claim that all files were reached or that the command had only those
failures.
## Other pre-existing failures
- `skill:check`: the primary `claude/SKILL.md` generated file was absent; the
script explicitly exited with code 1.
- Windows-safe shard 5:
`Source-level guard: terminal-agent > lazy spawn: claude PTY is spawned in message handler, not on upgrade`
still looked for `spawnClaude(`, while the source excerpt used
`maybeSpawnPty(...)`. The shard reported 82 pass / 1 skip / 1 fail and the
remaining 15 shards did not run.
- Design target:
`generateVariant Retry-After handling > HTTP-date: honors a future date with no extra leading exponential`
expected a delay of at least 2,500 ms and observed 2,246 ms.
## What this evidence does not establish
- It does not establish a green baseline.
- It does not establish that GStack 2.0 preserves behavior; replacement
contract tests have not yet been implemented.
- It does not turn incomplete logs into passes.
- It does not attribute these failures to later changes; every failure above
was captured from the stated base SHA before GStack 2.0 work.
+810
View File
@@ -0,0 +1,810 @@
#!/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<Component, { id: string; assertion: string }> = {
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<number, Omit<Mapping, 'disposition' | 'replacementTest' | 'classification'>> = {
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<string, unknown>;
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<string, unknown>;
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<string, unknown>).filename === 'string';
}
function flattenMatching<T>(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<string, unknown>;
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<Record<Component, JudgmentModule>> = {
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<T extends string>(values: T[]): Record<string, number> {
const counts = new Map<string, number>();
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<Record<string, unknown>> = [];
const labelsByRepo = new Map<string, Array<Record<string, unknown>>>();
const accumulators = new Map<string, {
repo: string;
number: number;
record: GitHubRecord;
sources: Set<string>;
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();