mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
record gstack 2 baseline and backlog map
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user