mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
fix: review pass 2 (delta): usage text, pass-marker capture group, GitHub-host origin parsing, idempotent trend.csv
- usage() strips the header comment decoration again; --help text asserted - passCount takes the first all-digit capture group; --pass-marker without a capture group exits 2 - --repo falls back to the origin remote only on github.com or GH_HOST; a local-path origin exits 2 - trend.csv keeps one row set per repo and window (a rerun replaces its rows) and always carries its header - /retro Step 7.5 points at SCORECARD_TREND for the weeks; the doc cites the per-PR baseline CSV Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2219c78ebe
commit
cacc6003ea
6 files changed
+38
-16
No files matched your search
@@ -32,7 +32,7 @@ import {
|
||||
|
||||
function usage(code: number): never {
|
||||
const lines = fs.readFileSync(new URL(import.meta.url), 'utf8').split('\n');
|
||||
const text = lines.slice(1, lines.indexOf(' */')).map(l => l.replace(/^\/?\*+\s?/, '')).join('\n');
|
||||
const text = lines.slice(2, lines.indexOf(' */')).map(l => l.replace(/^ ?\* ?/, '')).join('\n');
|
||||
(code ? console.error : console.log)(text);
|
||||
process.exit(code);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ let readyMarker: RegExp;
|
||||
try { readyMarker = new RegExp(flags.get('ready-marker') ?? DEFAULTS.readyMarker, 'm'); } catch (e: any) { console.error(`--ready-marker: ${e.message}`); usage(2); }
|
||||
let passMarker: RegExp;
|
||||
try { passMarker = new RegExp(flags.get('pass-marker') ?? DEFAULTS.passMarker, 'm'); } catch (e: any) { console.error(`--pass-marker: ${e.message}`); usage(2); }
|
||||
if (new RegExp(passMarker.source + '|').exec('')!.length < 2) { console.error('--pass-marker needs a capture group for the pass count'); usage(2); }
|
||||
const passCap = Number(flags.get('pass-cap') ?? DEFAULTS.passCap);
|
||||
if (!Number.isInteger(passCap) || passCap < 1) { console.error('--pass-cap must be a positive integer'); usage(2); }
|
||||
const gateEvent = flags.get('gate-event') ?? DEFAULTS.gateEvent;
|
||||
@@ -64,8 +65,9 @@ if (!Number.isInteger(maxArtifactRuns) || maxArtifactRuns < 0) { console.error('
|
||||
|
||||
function originRepo(): string {
|
||||
const origin = spawnSync('git', ['remote', 'get-url', 'origin'], { encoding: 'utf8', timeout: 10_000 });
|
||||
const slug = /[:/]([^/:]+\/[^/]+?)(?:\.git)?\/?$/.exec((origin.stdout ?? '').trim())?.[1];
|
||||
if (origin.status !== 0 || !slug) { console.error('--repo owner/name is required: the origin remote is missing or not owner/name'); usage(2); }
|
||||
const host = (process.env.GH_HOST || 'github.com').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const slug = new RegExp(`(?:^|[@/])${host}[:/]([^/:]+\\/[^/]+?)(?:\\.git)?\\/?$`).exec((origin.stdout ?? '').trim())?.[1];
|
||||
if (origin.status !== 0 || !slug) { console.error(`--repo owner/name is required: the origin remote is missing or not a ${process.env.GH_HOST || 'github.com'} owner/name`); usage(2); }
|
||||
return slug;
|
||||
}
|
||||
|
||||
@@ -97,8 +99,11 @@ fs.writeFileSync(csvFile, renderCsv(card, rows));
|
||||
fs.writeFileSync(path.join(outDir, 'scorecard.md'), renderMarkdown(card, rows));
|
||||
if (json) fs.writeFileSync(path.join(outDir, 'scorecard.json'), JSON.stringify({ card, rows }, null, 2) + '\n');
|
||||
const trendFile = path.join(outDir, 'trend.csv');
|
||||
const [csvHeader, ...csvRows] = renderCsv(card, rows).split('\n');
|
||||
fs.appendFileSync(trendFile, (fs.existsSync(trendFile) ? '' : csvHeader + '\n') + csvRows.join('\n'));
|
||||
const [csvHeader, ...csvRows] = renderCsv(card, rows).split('\n').filter(Boolean);
|
||||
const thisWindow = csvRows[0]!.split(',').slice(0, 3).join(',');
|
||||
const kept = (fs.existsSync(trendFile) ? fs.readFileSync(trendFile, 'utf8').split('\n').filter(Boolean) : [])
|
||||
.filter(line => line !== csvHeader && !line.startsWith(thisWindow + ','));
|
||||
fs.writeFileSync(trendFile, [csvHeader, ...kept, ...csvRows].join('\n') + '\n');
|
||||
|
||||
process.stdout.write(renderMarkdown(card, rows));
|
||||
process.stdout.write(`SCORECARD_CSV: ${csvFile}\n`);
|
||||
|
||||
@@ -257,8 +257,8 @@ gstack-factory-scorecard --repo owner/name --since-days 7 \
|
||||
[--pass-cap 3] [--out-dir .context/factory-scorecard]
|
||||
```
|
||||
|
||||
`--repo` defaults to the origin remote's GitHub owner/name and is required
|
||||
without one. Each run writes this window's `scorecard.md` and `scorecard.csv`
|
||||
`--repo` defaults to the origin remote's owner/name on github.com (or
|
||||
`GH_HOST`) and is required without one. Each run writes this window's `scorecard.md` and `scorecard.csv`
|
||||
to `--out-dir` and appends the same rows to `trend.csv` there, so a weekly
|
||||
retro accumulates its own trend without renaming files.
|
||||
|
||||
@@ -301,9 +301,10 @@ gate runs, artifacts read for the newest 60):
|
||||
Read the gap honestly: lead time and throughput are where they should be, the
|
||||
gate is green and its cost is mostly known, and flakiness is the line that is
|
||||
not. The daily pass exists for exactly that line; the scorecard's job is to
|
||||
show next week whether it moved. The inventory lane's baseline CSV, when it
|
||||
lands in the Captain's Drive, supersedes these numbers for trend tracking;
|
||||
from then on `trend.csv` in `--out-dir` carries the weeks.
|
||||
show next week whether it moved. The inventory lane's per-PR baseline CSV in
|
||||
the Captain's Drive (142 merged PRs over the eleven days before this window
|
||||
closed) holds the longer history behind these numbers; from here on
|
||||
`trend.csv` in `--out-dir` carries the weeks.
|
||||
|
||||
## What is Capy-specific and what is not
|
||||
|
||||
|
||||
@@ -167,8 +167,10 @@ export function readMergedPrs(source: ScorecardSource, o: ScorecardOptions, sinc
|
||||
export function passCount(text: string, marker: RegExp): number | null {
|
||||
let max: number | null = null;
|
||||
for (const m of text.matchAll(new RegExp(marker.source, marker.flags.includes('g') ? marker.flags : marker.flags + 'g'))) {
|
||||
const n = Number(m.slice(1).find(g => g !== undefined));
|
||||
if (Number.isFinite(n) && (max === null || n > max)) max = n;
|
||||
const digits = m.slice(1).find(g => g !== undefined && /^\d+$/.test(g));
|
||||
if (digits === undefined) continue;
|
||||
const n = Number(digits);
|
||||
if (max === null || n > max) max = n;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
+1
-1
@@ -683,7 +683,7 @@ gh auth status >/dev/null 2>&1 && "$_FS" --base "<default>" --since-days "<windo
|
||||
|| echo "FACTORY_SCORECARD: unavailable"
|
||||
```
|
||||
|
||||
Paste the table under **Factory scorecard**, third column included (what one of the best factories shows on that line). Keep any `unavailable (...)` wording; never write 0 for it. `SCORECARD_CSV:` is the week's trend row. `docs/recommended-harness.md#the-factory-scorecard` explains each line and the `--crew`, `--gate-workflow`, `--free-workflow`, `--train-prefix` knobs.
|
||||
Paste the table under **Factory scorecard**, third column included (what one of the best factories shows on that line). Keep any `unavailable (...)` wording; never write 0 for it. `SCORECARD_TREND:` accumulates the weeks. `docs/recommended-harness.md#the-factory-scorecard` explains each line and the `--crew`, `--gate-workflow`, `--free-workflow`, `--train-prefix` knobs.
|
||||
|
||||
### Step 8: Focus Score + Ship of the Week
|
||||
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ gh auth status >/dev/null 2>&1 && "$_FS" --base "<default>" --since-days "<windo
|
||||
|| echo "FACTORY_SCORECARD: unavailable"
|
||||
```
|
||||
|
||||
Paste the table under **Factory scorecard**, third column included (what one of the best factories shows on that line). Keep any `unavailable (...)` wording; never write 0 for it. `SCORECARD_CSV:` is the week's trend row. `docs/recommended-harness.md#the-factory-scorecard` explains each line and the `--crew`, `--gate-workflow`, `--free-workflow`, `--train-prefix` knobs.
|
||||
Paste the table under **Factory scorecard**, third column included (what one of the best factories shows on that line). Keep any `unavailable (...)` wording; never write 0 for it. `SCORECARD_TREND:` accumulates the weeks. `docs/recommended-harness.md#the-factory-scorecard` explains each line and the `--crew`, `--gate-workflow`, `--free-workflow`, `--train-prefix` knobs.
|
||||
|
||||
### Step 8: Focus Score + Ship of the Week
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ describe('factory-scorecard: pieces', () => {
|
||||
expect(readComments(source, 'acme/widgets', 102, markers(/READY #\d+/m, '2026-10-25T12:00:00Z')).readyAt).toBeNull();
|
||||
expect(readComments(source, 'acme/widgets', 102, markers(/READY #\d+/m)).readyAt).toBe('2026-10-26T01:00:00Z');
|
||||
expect(passCount('"passes": 4 then passes=1 then REVIEW_PASSES: 2', new RegExp(DEFAULTS.passMarker, 'm'))).toBe(4);
|
||||
expect(passCount('reviews: 3 and passes=2', /(passes|reviews)[=: ]+(\d+)/m)).toBe(3);
|
||||
expect(passCount('no count here', new RegExp(DEFAULTS.passMarker, 'm'))).toBeNull();
|
||||
});
|
||||
|
||||
@@ -312,7 +313,10 @@ describe('factory-scorecard: the bin', () => {
|
||||
expect(trend.length).toBe(1 + Object.keys(BENCHMARKS).length);
|
||||
const again = run(['--repo', 'acme/widgets', '--fixtures', FIXTURE, '--now', '2026-10-27T12:00:00Z'], cwd);
|
||||
expect(again.status, again.stderr).toBe(0);
|
||||
expect(fs.readFileSync(path.join(cwd, '.context', 'factory-scorecard', 'trend.csv'), 'utf8').split('\n').filter(Boolean).length).toBe(1 + 2 * Object.keys(BENCHMARKS).length);
|
||||
const trendLines = () => fs.readFileSync(path.join(cwd, '.context', 'factory-scorecard', 'trend.csv'), 'utf8').split('\n').filter(Boolean).length;
|
||||
expect(trendLines()).toBe(1 + 2 * Object.keys(BENCHMARKS).length);
|
||||
expect(run(['--repo', 'acme/widgets', '--fixtures', FIXTURE, '--now', '2026-10-27T12:00:00Z'], cwd).status).toBe(0);
|
||||
expect(trendLines()).toBe(1 + 2 * Object.keys(BENCHMARKS).length);
|
||||
expect(nested.stdout).toContain(`SCORECARD_TREND: ${path.join('.context', 'factory-scorecard', 'trend.csv')}`);
|
||||
});
|
||||
|
||||
@@ -333,12 +337,22 @@ describe('factory-scorecard: the bin', () => {
|
||||
const noRepo = run(['--fixtures', FIXTURE], cwd);
|
||||
expect(noRepo.status).toBe(2);
|
||||
expect(noRepo.stderr).toContain('--repo owner/name is required');
|
||||
const local = scratch();
|
||||
spawnSync('git', ['init', '-q', local]);
|
||||
spawnSync('git', ['-C', local, 'remote', 'add', 'origin', '/srv/git/repo.git']);
|
||||
expect(run(['--fixtures', FIXTURE], local).stderr).toContain('--repo owner/name is required');
|
||||
spawnSync('git', ['-C', local, 'remote', 'set-url', 'origin', 'git@ghe.example.test:acme/widgets.git']);
|
||||
const ghe = spawnSync(BIN, ['--fixtures', FIXTURE, '--now', '2026-10-26T12:00:00Z'], { cwd: local, encoding: 'utf8', timeout: 60_000, env: { ...process.env, HOME: local, GH_HOST: 'ghe.example.test' } });
|
||||
expect(ghe.status, ghe.stderr).toBe(0);
|
||||
expect(ghe.stdout).toContain('| Merged PRs | 3 (crew 2, outside 1) |');
|
||||
expect(run(['--pass-marker', 'passes', '--fixtures', FIXTURE], cwd).stderr).toContain('--pass-marker needs a capture group');
|
||||
const stray = run(['stray'], cwd);
|
||||
expect(stray.status).toBe(2);
|
||||
expect(stray.stderr).toContain('unexpected argument: stray');
|
||||
const help = run(['--help'], cwd);
|
||||
expect(help.status).toBe(0);
|
||||
expect(help.stdout).toContain('gstack-factory-scorecard');
|
||||
expect(help.stdout).toContain('gstack-factory-scorecard [--repo owner/name]');
|
||||
expect(help.stdout).not.toContain(' * ');
|
||||
});
|
||||
|
||||
test('the retro template runs the bin from the runtime root and reads its table', () => {
|
||||
|
||||
Reference in new issue
Block a user