Files
gstack/bin/gstack-state-matrix
T
2026-10-16 23:30:27 +00:00

77 lines
4.4 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* gstack-state-matrix — the round-trip matrix for proposal, hold and approval state machines (field list 2, W3).
*
* gstack-state-matrix detect [--base <ref>] [--diff-base <sha>]
* gstack-state-matrix check <specialist-output|-> [--repo <dir>] [--expect <expectation.json>] [--json]
*
* `detect` prints `SCOPE_STATE_MACHINE=true|false` (shell-safe for
* `source <(...)`, with `# state-machine:` comment lines naming the hits) from
* the lines the branch adds since its merge base with origin/<base>.
* `check` reads the state-machines specialist's output and prints
* `STATE_MATRIX_GAP:` per gap and `STATE_MATRIX_CHECK: ok | incomplete gaps=<n>`
* last; exit 0 ok, 1 incomplete (STATE_MATRIX_INCOMPLETE). Logic: lib/state-matrix.ts.
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import { addedLines } from '../lib/banned-terms';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { resultLine } from '../lib/result-codes';
import { resolveBaseBranch } from '../lib/ship-policy';
import { checkStateMatrix, detectStateMachine, renderStateMatrixCheck, type Expectation } from '../lib/state-matrix';
const HELP = `gstack-state-matrix — kind × owner action × resulting row for proposal, hold and approval state machines
detect [--base <ref>] [--diff-base <sha>] SCOPE_STATE_MACHINE=true|false for the branch's added code lines
check <file|-> [--repo <dir>] [--expect <json>] [--json]
gaps in the state-machines specialist's output (review/specialists/state-machines.md)
${exitTableHelp()}`;
function flag(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}
function git(args: string[], cwd?: string): { ok: boolean; out: string } {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 120_000, maxBuffer: 256 * 1024 * 1024 });
return { ok: r.status === 0, out: r.stdout ?? '' };
}
function detect(rest: string[]): number {
const root = git(['rev-parse', '--show-toplevel']);
if (!root.ok) { process.stdout.write('SCOPE_STATE_MACHINE=false\n# not inside a git repository\n'); return EXIT.usage; }
const repoRoot = root.out.trim();
const base = resolveBaseBranch(repoRoot, flag(rest, '--base'));
const mb = flag(rest, '--diff-base') ?? git(['merge-base', `origin/${base}`, 'HEAD'], repoRoot).out.trim();
const diff = mb ? git(['diff', '--unified=0', '--no-color', mb], repoRoot) : { ok: false, out: '' };
if (!diff.ok) { process.stdout.write(`SCOPE_STATE_MACHINE=false\n# no merge base with origin/${base}; run: git fetch origin ${base}\n`); return EXIT.usage; }
const { detected, hits } = detectStateMachine(addedLines(diff.out));
const lines = [`SCOPE_STATE_MACHINE=${detected}`, ...hits.slice(0, 12).map(h => `# state-machine: ${h.file}:${h.line} ${[...h.subject, ...h.action].join(',')}`)];
process.stdout.write(lines.join('\n') + '\n');
return EXIT.ok;
}
function check(rest: string[]): number {
const input = rest.find((a, i) => !a.startsWith('--') && !['--repo', '--expect'].includes(rest[i - 1] ?? ''));
if (!input) { process.stderr.write(`gstack-state-matrix check: a specialist output file (or -) is required\n${HELP}\n`); return EXIT.usage; }
const text = input === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(input, 'utf8');
const repoRoot = flag(rest, '--repo') ?? (git(['rev-parse', '--show-toplevel']).out.trim() || process.cwd());
const expectFile = flag(rest, '--expect');
const expect = expectFile ? JSON.parse(fs.readFileSync(expectFile, 'utf8')) as Expectation : undefined;
const result = checkStateMatrix(text, repoRoot, expect);
process.stdout.write(rest.includes('--json') ? JSON.stringify(result) + '\n' : renderStateMatrixCheck(result));
if (result.gaps.length) { process.stderr.write(resultLine('gstack-state-matrix check', 'STATE_MATRIX_INCOMPLETE', `${result.gaps.length} gap(s)`) + '\n'); return EXIT.fail; }
return EXIT.ok;
}
function main(argv: string[]): number {
const [cmd, ...rest] = argv;
if (!cmd || cmd === '--help' || cmd === '-h') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; }
if (cmd === 'detect') return detect(rest);
if (cmd === 'check') return check(rest);
process.stderr.write(`gstack-state-matrix: unknown command ${cmd}\n${HELP}\n`);
return EXIT.usage;
}
process.exitCode = main(process.argv.slice(2));