diff --git a/docs/trajectories/typescript-bun-migration/RESUME.md b/docs/trajectories/typescript-bun-migration/RESUME.md index 8ce4a4c89..778d78832 100644 --- a/docs/trajectories/typescript-bun-migration/RESUME.md +++ b/docs/trajectories/typescript-bun-migration/RESUME.md @@ -1,9 +1,9 @@ # Trajectory — TypeScript / Bun migration -**Status**: Active (Lane B slice 10 merged — [#883](https://github.com/Lucent-Financial-Group/Zeta/pull/883), commit `271bc38`) -**Milestone**: 30 ported + 2 in-flight = 32 total (2 from #849 + 3 from #866 + 3 from #868 + 3 from #870 + 2 from #872 + 3 from #874 + 3 from #876 + 3 from #878 + 3 from #880 + 3 from #882 + 2 from #883 = 30 merged; +2 in-flight in slice-11). Slice-11 opens **skill-catalog cluster + nuget audit** (backfill_dv2_frontmatter + audit-packages). 12 Bucket B files remain. +**Status**: Active (Lane B slice 11 merged — [#884](https://github.com/Lucent-Financial-Group/Zeta/pull/884), commit `9237756`) +**Milestone**: 32 ported + 1 in-flight = 33 total (2 from #849 + 3 from #866 + 3 from #868 + 3 from #870 + 2 from #872 + 3 from #874 + 3 from #876 + 3 from #878 + 3 from #880 + 3 from #882 + 2 from #883 + 2 from #884 = 32 merged; +1 in-flight in slice-12). Slice-12 opens **backlog index regenerator** (backlog/generate-index). 10 Bucket B files remain. **Current blocker**: None. -**Next concrete action**: Pick a coherent next slice from Bucket B (12 files remaining). Per Gate B: read-only scope first, then re-verify the layered baseline currency before first mutating action. +**Next concrete action**: Pick a coherent next slice from Bucket B (10 files remaining). Per Gate B: read-only scope first, then re-verify the layered baseline currency before first mutating action. **Last updated**: 2026-04-30 ## Why this trajectory exists @@ -25,6 +25,11 @@ Per the maintainer-channel correction via the multi-AI review surface (2026-04-2 | [#872](https://github.com/Lucent-Financial-Group/Zeta/pull/872) | 2026-04-30 (commit `2f3275a`) | `tools/alignment/audit_skills.{sh→ts}`, `tools/alignment/citations.{sh→ts}` | Merged | | [#874](https://github.com/Lucent-Financial-Group/Zeta/pull/874) | 2026-04-30 (commit `3f33b51`) | `tools/hygiene/audit-tick-history-bounded-growth.{sh→ts}`, `tools/hygiene/audit-post-setup-script-stack.{sh→ts}`, `tools/hygiene/audit-missing-prevention-layers.{sh→ts}` | Merged | | [#876](https://github.com/Lucent-Financial-Group/Zeta/pull/876) | 2026-04-30 (commit `02baabc`) | `tools/hygiene/check-no-conflict-markers.{sh→ts}`, `tools/hygiene/check-archive-header-section33.{sh→ts}`, `tools/hygiene/check-tick-history-order.{sh→ts}` | Merged | +| [#878](https://github.com/Lucent-Financial-Group/Zeta/pull/878) | 2026-04-30 | `tools/lint/no-empty-dirs.{sh→ts}`, `tools/lint/safety-clause-audit.{sh→ts}`, `tools/lint/doc-comment-history-audit.{sh→ts}` | Merged | +| [#880](https://github.com/Lucent-Financial-Group/Zeta/pull/880) | 2026-04-30 (commit `988de70`) | `tools/lint/runner-version-freshness.{sh→ts}`, `tools/lint/no-directives-otto-prose.{sh→ts}`, `tools/audit/live-lock-audit.{sh→ts}` | Merged | +| [#882](https://github.com/Lucent-Financial-Group/Zeta/pull/882) | 2026-04-30 (commit `02266a7`) | `tools/hygiene/validate-agencysignature-pr-body.{sh→ts}`, `tools/hygiene/audit-agencysignature-main-tip.{sh→ts}`, `tools/hygiene/capture-tick-snapshot.{sh→ts}` | Merged | +| [#883](https://github.com/Lucent-Financial-Group/Zeta/pull/883) | 2026-04-30 (commit `271bc38`) | `tools/hygiene/counterweight-audit.{sh→ts}`, `tools/hygiene/append-tick-history-row.{sh→ts}` | Merged | +| [#884](https://github.com/Lucent-Financial-Group/Zeta/pull/884) | 2026-04-30 (commit `9237756`) | `tools/skill-catalog/backfill_dv2_frontmatter.{sh→ts}`, `tools/audit-packages.{sh→ts}` | Merged | ## Inventory — Python (tools/, Zeta-authored) @@ -59,28 +64,21 @@ tools/profile.sh Rationale: TS/Bun is itself one of the things `install.sh` installs. These scripts cannot depend on Bun. -### Bucket B — Should become TypeScript (19 files remaining) +### Bucket B — Should become TypeScript (10 files remaining) -Post-install scripts that operate on the repo (lints, audits, hygiene checks, peer-call wrappers, budget reports, git ops). Same shape as the scripts ported in #849, #866, #868, #870, #872, #874, #876, #878. Twenty-three originally-listed audit/lint scripts are now ported to TS and removed from this list (3 in slice-8 in flight); the bash originals remain in-tree as the equivalence reference and will retire once the TS ports have soaked. +Post-install scripts that operate on the repo (lints, audits, hygiene checks, peer-call wrappers, budget reports, git ops). Same shape as the scripts ported in #849, #866, #868, #870, #872, #874, #876, #878, #880, #882, #883, #884. The originally-listed audit/lint scripts have progressively ported (1 in slice-12 in flight); the bash originals remain in-tree as the equivalence reference and will retire once the TS ports have soaked. ```text -tools/audit-packages.sh -tools/backlog/generate-index.sh +tools/backlog/generate-index.sh # in flight (slice 12) tools/budget/daily-cost-report.sh tools/budget/project-runway.sh tools/budget/snapshot-burn.sh tools/git/batch-resolve-pr-threads.sh tools/git/push-with-retry.sh -tools/hygiene/append-tick-history-row.sh -tools/hygiene/audit-agencysignature-main-tip.sh -tools/hygiene/capture-tick-snapshot.sh -tools/hygiene/counterweight-audit.sh -tools/hygiene/validate-agencysignature-pr-body.sh tools/peer-call/codex.sh tools/peer-call/gemini.sh tools/peer-call/grok.sh tools/pr-preservation/archive-pr.sh -tools/skill-catalog/backfill_dv2_frontmatter.sh ``` Rationale: type safety, structured error handling, easier testing, jq/awk/grep replaced by JS object operations, gh CLI shell-out replaced by Octokit when valuable. @@ -100,9 +98,9 @@ tools/lint/safety-clause-audit.sh Rationale: borderline — depends on whether the lint can be expressed as cleanly in TS as it currently is in shell. Worth a small comparison before committing the port. -### Bucket D — Ported, bash retained (23 files) +### Bucket D — Ported, bash retained (32 files) -The TS ports landed in #866 + #868 + #870 + #872 + #874 + #876 + #878 (3 more in slice-8 PR in flight); the bash originals stay in-tree as equivalence references and will retire once the TS ports have soaked. +The TS ports landed in #866 + #868 + #870 + #872 + #874 + #876 + #878 + #880 + #882 + #883 + #884; the bash originals stay in-tree as equivalence references and will retire once the TS ports have soaked. ```text tools/hygiene/audit-md032-plus-linestart.sh # ported in #866 @@ -125,9 +123,16 @@ tools/hygiene/check-tick-history-order.sh # ported in #876 tools/lint/no-empty-dirs.sh # ported in #878 tools/lint/safety-clause-audit.sh # ported in #878 tools/lint/doc-comment-history-audit.sh # ported in #878 -tools/lint/runner-version-freshness.sh # ported in slice-8 PR (in flight) -tools/lint/no-directives-otto-prose.sh # ported in slice-8 PR (in flight) -tools/audit/live-lock-audit.sh # ported in slice-8 PR (in flight) +tools/lint/runner-version-freshness.sh # ported in #880 +tools/lint/no-directives-otto-prose.sh # ported in #880 +tools/audit/live-lock-audit.sh # ported in #880 +tools/hygiene/validate-agencysignature-pr-body.sh # ported in #882 +tools/hygiene/audit-agencysignature-main-tip.sh # ported in #882 +tools/hygiene/capture-tick-snapshot.sh # ported in #882 +tools/hygiene/counterweight-audit.sh # ported in #883 +tools/hygiene/append-tick-history-row.sh # ported in #883 +tools/skill-catalog/backfill_dv2_frontmatter.sh # ported in #884 +tools/audit-packages.sh # ported in #884 ``` ## Recommended next slice diff --git a/docs/trajectories/typescript-bun-migration/slice-audits.md b/docs/trajectories/typescript-bun-migration/slice-audits.md index c9ffead54..94192acf3 100644 --- a/docs/trajectories/typescript-bun-migration/slice-audits.md +++ b/docs/trajectories/typescript-bun-migration/slice-audits.md @@ -411,7 +411,27 @@ Per-port pattern checklist: Slice 6 passes audit. No new patterns recorded — all reused from prior slices. -## Slice 11 — 2 ports (skill-catalog cluster + nuget audit) (PR pending — `lane-b/ts-bun-slice-11-dv2-frontmatter-backfill-2026-04-30`) +## Slice 12 — 1 port (backlog index regenerator) (PR pending — `lane-b/ts-bun-slice-12-backlog-generate-index-2026-04-30`) + +**Slice files**: + +- `tools/backlog/generate-index.{sh→ts}` (regenerates `docs/BACKLOG.md` from per-row `docs/backlog/P/B--.md` files) + +**Comparison points**: identical to slice 11/10/9. Within Gate B 30-day window. + +### Code-pattern audit (per-port) + +- **`generate-index.ts`** (217 → 282 lines): bash awk frontmatter parser (state machine + `gsub` for quote-stripping) → `extractField` + `stripQuotes` helpers; one `RegExp.exec` per known field; bash `find -name 'B-*.md' -type f -print0 | sort -z` → `readdirSync` filter + `localeCompare` by basename. Bash `mktemp` + atomic `mv` rename → `readFileSync` compare + conditional `writeFileSync` (no rewrite when content identical, mirroring bash's "only write if different"). Bash `diff -q` + `diff` invocation in `--check` mode → in-memory line-by-line comparison emitting `<` / `>` diff markers. Phase-1a 50-line safety guard preserved (refuses to overwrite shorter files unless `BACKLOG_WRITE_FORCE=1`). Three modes preserved: write (default) / `--check` / `--stdout`. + +### Equivalence audit + +- **`generate-index`**: byte-equivalent against bash original on `--stdout` mode for the current `docs/backlog/` tree (4 priority tiers × ~70 rows). `--check` produces matching exit codes. Write-mode tested via temp-copy snapshot. + +### Outcome + +Slice 12 passes audit. **Backlog-cluster opened** — first script in this cluster ports cleanly with the in-memory diff pattern (replaces shell-out to `diff`). Bucket B 11 → 10. + +## Slice 11 — 2 ports (skill-catalog cluster + nuget audit) (PR #884, merged 2026-04-30, commit `9237756`) **Slice files**: @@ -434,7 +454,7 @@ Slice 6 passes audit. No new patterns recorded — all reused from prior slices. Slice 11 passes audit. Skill-catalog cluster opened + NuGet audit added. Bucket B 14 → 12. -## Slice 10 — 2 ports (counterweight-cluster + first write-side) (PR pending — `lane-b/ts-bun-slice-10-counterweight-audit-2026-04-30`) +## Slice 10 — 2 ports (counterweight-cluster + first write-side) (PR #883, merged 2026-04-30, commit `271bc38`) **Slice files**: @@ -457,7 +477,7 @@ Slice 11 passes audit. Skill-catalog cluster opened + NuGet audit added. Bucket Slice 10 passes audit. **Counterweight-cluster opened** (Otto-278 cadenced inspect). **First write-side script ported** (append-tick-history-row) — confirms write-side equivalence-test pattern works. Bucket B 16 → 14. -## Slice 9 — 3 ports (agency-signature-pair cluster + snapshot-pinning) (PR pending — `lane-b/ts-bun-slice-9-agencysignature-pair-2026-04-30`) +## Slice 9 — 3 ports (agency-signature-pair cluster + snapshot-pinning) (PR #882, merged 2026-04-30, commit `02266a7`) **Slice files**: @@ -491,7 +511,7 @@ Slice 10 passes audit. **Counterweight-cluster opened** (Otto-278 cadenced inspe Slice 9 passes audit. **Agency-signature-pair cluster opened**: validate-pr-body (pre-merge) + audit-main-tip (post-merge) form Amara's ferry-7 enforcement-instrument set. Bucket B 19 → 16. -## Slice 8 — 3 ports (Cluster H finish + audit-cluster start) (PR pending — `lane-b/ts-bun-slice-8-runner-version-freshness-2026-04-30`) +## Slice 8 — 3 ports (Cluster H finish + audit-cluster start) (PR #880, merged 2026-04-30, commit `988de70`) **Slice files**: diff --git a/tools/backlog/generate-index.ts b/tools/backlog/generate-index.ts new file mode 100644 index 000000000..908043ff6 --- /dev/null +++ b/tools/backlog/generate-index.ts @@ -0,0 +1,282 @@ +#!/usr/bin/env bun +// generate-index.ts — regenerate docs/BACKLOG.md from per-row files +// at docs/backlog/P/B--.md. +// +// TypeScript+Bun port of generate-index.sh, slice 12 of the TS+Bun +// migration. See docs/best-practices/repo-scripting.md. +// +// Walks the per-row files, parses YAML frontmatter, emits a short- +// pointer index sorted by (priority, id). +// +// Usage: +// bun tools/backlog/generate-index.ts # writes docs/BACKLOG.md +// bun tools/backlog/generate-index.ts --check # exit 2 if drift vs committed +// bun tools/backlog/generate-index.ts --stdout # print to stdout, no write +// +// Exit codes: +// 0 success +// 1 environment / dependency error +// 2 drift detected (--check mode only) + +import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs"; +import { basename, join } from "node:path"; +import { spawnSync } from "node:child_process"; + +type ExitCode = 0 | 1 | 2; +type Mode = "write" | "check" | "stdout"; + +const SPAWN_MAX_BUFFER = 64 * 1024 * 1024; + +const TIERS: readonly string[] = ["P0", "P1", "P2", "P3"]; + +const TIER_LABELS: Record = { + P0: "## P0 — critical / blocking", + P1: "## P1 — within 2-3 rounds", + P2: "## P2 — research-grade", + P3: "## P3 — convenience / deferred", +}; + +function repoRoot(): string { + // eslint-disable-next-line sonarjs/no-os-command-from-path + const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { + encoding: "utf8", + maxBuffer: SPAWN_MAX_BUFFER, + }); + if (result.status !== 0) return process.cwd(); + return result.stdout.trim(); +} + +function parseMode(argv: readonly string[]): Mode | null { + if (argv.length === 0) return "write"; + const arg = argv[0]; + if (arg === "--check") return "check"; + if (arg === "--stdout") return "stdout"; + return null; +} + +function trimSpaceTab(s: string): string { + let start = 0; + while (start < s.length) { + const c = s.charCodeAt(start); + if (c !== 0x20 && c !== 0x09) break; + start++; + } + let end = s.length; + while (end > start) { + const c = s.charCodeAt(end - 1); + if (c !== 0x20 && c !== 0x09) break; + end--; + } + return s.slice(start, end); +} + +function stripQuotes(s: string): string { + // Mirror bash awk: gsub of /^"|"$|^[[:space:]]*'|'[[:space:]]*$/ removes + // a single leading " or ' (with optional leading whitespace) and same trailing. + let out = s; + const startsWithDouble = out.startsWith('"'); + const trimmedStart = trimSpaceTab(out); + const startsWithSingle = trimmedStart.startsWith("'"); + if (startsWithDouble) out = out.slice(1); + else if (startsWithSingle) out = trimmedStart.slice(1); + if (out.endsWith('"')) out = out.slice(0, -1); + else { + const trimmedEnd = trimSpaceTab(out); + if (trimmedEnd.endsWith("'")) out = trimmedEnd.slice(0, -1); + } + return trimSpaceTab(out); +} + +function extractField(content: string, field: string): string { + // Bash awk: read FIRST YAML frontmatter block (between two `---`), + // find first line where $1 == "field:", strip the field prefix + + // quotes, return value. + const lines = content.split("\n"); + let inside = false; + for (const line of lines) { + if (line === "---") { + if (!inside) { + inside = true; + continue; + } + break; + } + if (!inside) continue; + const prefix = `${field}:`; + if (!line.startsWith(prefix)) continue; + return stripQuotes(trimSpaceTab(line.slice(prefix.length))); + } + return ""; +} + +function listBacklogFiles(tierDir: string): readonly string[] { + let entries: readonly import("node:fs").Dirent[]; + try { + entries = readdirSync(tierDir, { withFileTypes: true }); + } catch { + return []; + } + const out: string[] = []; + for (const e of entries) { + if (!e.isFile()) continue; + if (!e.name.startsWith("B-")) continue; + if (!e.name.endsWith(".md")) continue; + out.push(join(tierDir, e.name)); + } + return out.sort((a, b) => basename(a).localeCompare(basename(b))); +} + +function checkboxFor(status: string): "[x]" | "[ ]" { + if (status === "closed") return "[x]"; + if (status.startsWith("superseded-by-")) return "[x]"; + return "[ ]"; +} + +function generateContent(backlogDir: string): string { + const out: string[] = []; + out.push("# Backlog Index"); + out.push(""); + out.push(""); + out.push(""); + out.push("_Each entry below is a link to a per-row file under"); + out.push("`docs/backlog/`. Entries with `- [ ]` are open; `- [x]`"); + out.push("are closed (status: closed in frontmatter)._"); + out.push(""); + + for (const tier of TIERS) { + const tierDir = join(backlogDir, tier); + if (!isDirectory(tierDir)) continue; + const files = listBacklogFiles(tierDir); + if (files.length === 0) continue; + out.push(""); + out.push(TIER_LABELS[tier] ?? ""); + out.push(""); + for (const file of files) { + let content: string; + try { + content = readFileSync(file, "utf8"); + } catch { + continue; + } + const id = extractField(content, "id"); + const status = extractField(content, "status"); + const title = extractField(content, "title"); + const linkPath = `backlog/${tier}/${basename(file)}`; + const checkbox = checkboxFor(status); + out.push(`- ${checkbox} **[${id}](${linkPath})** ${title}`); + } + } + + out.push(""); + out.push(""); + out.push(""); + return out.join("\n"); +} + +function fileExists(path: string): boolean { + try { + statSync(path); + return true; + } catch { + return false; + } +} + +function isDirectory(path: string): boolean { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } +} + +function readLineCount(path: string): number { + try { + return readFileSync(path, "utf8").split("\n").length; + } catch { + return 0; + } +} + +function emitWriteRefuse(indexPath: string): ExitCode { + process.stderr.write( + "generate-index.ts: refusing to overwrite existing\n", + ); + process.stderr.write( + `${indexPath} — file has substantial content\n`, + ); + process.stderr.write( + "(Phase-1a guard). Phase 2 content-migration PR should\n", + ); + process.stderr.write( + "set BACKLOG_WRITE_FORCE=1 to authorize the overwrite\n", + ); + process.stderr.write("once per-row files have been populated.\n"); + process.stderr.write("\n"); + process.stderr.write("Use --stdout to preview, --check to compare against\n"); + process.stderr.write("committed, or set BACKLOG_WRITE_FORCE=1 to force.\n"); + return 1; +} + +function emitCheckDrift(content: string, indexPath: string): ExitCode { + if (!fileExists(indexPath)) { + process.stderr.write(`drift: ${indexPath} does not exist\n`); + return 2; + } + const existing = readFileSync(indexPath, "utf8"); + if (existing === content) { + process.stdout.write(`ok: ${indexPath} matches generator output\n`); + return 0; + } + process.stderr.write(`drift: ${indexPath} differs from generator output\n`); + process.stderr.write("diff:\n"); + // Best-effort line-level diff (no shell `diff` invocation). + const aLines = content.split("\n"); + const bLines = existing.split("\n"); + const maxLen = Math.max(aLines.length, bLines.length); + for (let i = 0; i < maxLen; i++) { + if ((aLines[i] ?? "") === (bLines[i] ?? "")) continue; + if (aLines[i] !== undefined) process.stderr.write(`< ${aLines[i] ?? ""}\n`); + if (bLines[i] !== undefined) process.stderr.write(`> ${bLines[i] ?? ""}\n`); + } + return 2; +} + +function emitWrite(content: string, indexPath: string): ExitCode { + if (fileExists(indexPath) && process.env.BACKLOG_WRITE_FORCE !== "1") { + const lineCount = readLineCount(indexPath); + if (lineCount > 50) return emitWriteRefuse(indexPath); + } + writeFileSync(indexPath, content); + process.stdout.write(`wrote ${indexPath}\n`); + return 0; +} + +export function main(argv: readonly string[]): ExitCode { + const mode = parseMode(argv); + if (mode === null) { + const arg = argv[0] ?? ""; + process.stderr.write(`unknown arg: ${arg}\n`); + return 1; + } + + const root = repoRoot(); + const backlogDir = join(root, "docs", "backlog"); + const indexPath = join(root, "docs", "BACKLOG.md"); + + const content = generateContent(backlogDir); + + if (mode === "stdout") { + process.stdout.write(content); + return 0; + } + if (mode === "check") return emitCheckDrift(content, indexPath); + return emitWrite(content, indexPath); +} + +if (import.meta.main) { + process.exit(main(process.argv.slice(2))); +}