mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
review: classify the PR type and always check repo conventions (#356)
The checklist now opens with a PR-type line (application, CI/infra, scripts/tooling, docs-only or mixed). Application and mixed diffs run every category; other types always run five categories, including the new Repo Conventions & Platform Fit, and report the rest as not applicable in one line instead of skipping silently. No-loss replay: GSTA-224 H1 preregistered no-loss run (run id gsta224-h1-20261031, Opus 5.5, 81 calls, $9.53) against main's checklist with a no-gstack arm; every preregistered bar passed. The review/checklist.md pin in test/review-packets-removed.test.ts moves to a32963ef with replay 'GSTA-224 H1'. Refs GSTA-224 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
92a9350a32
commit
3d795e18e3
4 files changed
+162
-3
No files matched your search
@@ -0,0 +1,10 @@
|
||||
**/review now names the kind of PR it is reviewing, so a CI or script change gets checks that fit it instead of a page of "not applicable".** The review checklist was written for application code. A PR that only changed CI or shell scripts came back with every category marked not applicable, which looked thorough but checked nothing, and nothing asked whether a new script even fit the repo (#356: a Windows `.bat` build script added to a bash + Makefile repo with Linux-only CI).
|
||||
|
||||
### Changed
|
||||
- **`review/checklist.md` starts with a PR type taken from the changed paths:** docs-only, CI/infra, scripts/tooling, application or mixed. Code under `bin/`, `scripts/` or `tools/` that is not shell, batch or PowerShell, package manifests and lockfiles, prompt templates (`*.tmpl`) and skill Markdown count as application, and so does any path the reviewer can't place. Application and mixed diffs run every category exactly as before; nothing is skipped.
|
||||
- **On a CI/infra, scripts/tooling or docs-only diff,** Gate Integrity, Shell Injection, Completeness Gaps, Distribution & CI/CD Pipeline and the new Repo Conventions & Platform Fit category always run. Every other category with nothing to check is reported once, in one line (`Not applicable (PR type: <type>): <categories>`), and never counts as passed.
|
||||
- **New category, Repo Conventions & Platform Fit:** new files use the languages, tools and naming the repo already uses for the same job, scripts run on the platforms the CI runners and docs target, and new tooling doesn't repeat an existing tool. A finding has to name its concrete consequence.
|
||||
- **Distribution & CI/CD Pipeline gains script checks:** strict mode (`set -euo pipefail` or the repo's equivalent) in scripts CI or a build relies on, quoted variables, commands that work on the CI runners' OS, third-party actions pinned to a release tag or commit SHA, and tools installed at a pinned version. These checks apply to test-only CI changes too.
|
||||
|
||||
### Tests
|
||||
- `test/review-checklist-pr-type.test.ts` builds a classifier from the patterns the checklist itself lists and runs real path sets through it: a `.tmpl` change, `bin/` TypeScript, an extensionless `bin/` executable, `package.json` + `bun.lock`, a workflow-only change, a `.bat` add, docs, and mixed changes. It also checks the five types, the always-run list, the one-line not-applicable report, the new category and the line budget. All 19 tests fail on the previous checklist.
|
||||
+20
-2
@@ -4,6 +4,15 @@
|
||||
|
||||
Review the merge-base diff from the caller, including its selected uncommitted and new source. Use the caller's detected base, not a hardcoded branch. Cite `file:line` and suggest fixes. Only flag real problems.
|
||||
|
||||
**PR type (decide first, from the changed paths; write it as the first line of your review, `PR type: <type>`):**
|
||||
- **docs-only:** every path is Markdown or plain text under `docs/` or a README/CHANGELOG, and none is a prompt, template or skill file.
|
||||
- **CI/infra:** `.github/**`, other CI config (`.gitlab-ci.yml`, `.circleci/**`, `Jenkinsfile`, Buildkite), `Dockerfile*`, compose files, Terraform, Kubernetes or Helm manifests.
|
||||
- **scripts/tooling:** shell, batch and PowerShell files (`*.sh`, `*.bash`, `*.bat`, `*.cmd`, `*.ps1`), `Makefile`, lint and format config.
|
||||
- **application:** everything else. Code in other languages under `bin/`, `scripts/` or `tools/` (an extensionless file there included), package manifests and lockfiles, prompt templates (`*.tmpl`) and skill Markdown are application, as is any path you cannot place. When in doubt, application.
|
||||
- **mixed:** paths of more than one type above.
|
||||
|
||||
On application and mixed diffs, run every category below exactly as written; nothing is skipped. On a CI/infra, scripts/tooling or docs-only diff, always run Gate Integrity, Shell Injection, Completeness Gaps, Distribution & CI/CD Pipeline and Repo Conventions & Platform Fit, and report every other category whose subject is absent from the diff once, in one line: `Not applicable (PR type: <type>): <categories>`. A not-applicable category never counts as passed.
|
||||
|
||||
**Two-pass review:**
|
||||
- **Pass 1 (CRITICAL):** Run SQL & Data Safety, Race Conditions, LLM Output Trust Boundary, Shell Injection, Enum Completeness, and Gate Integrity first. Highest severity.
|
||||
- **Pass 2 (INFORMATIONAL):** Run remaining categories below. Lower severity but still actioned.
|
||||
@@ -137,11 +146,19 @@ A gate is anything that would have made this change fail: a test, an assertion,
|
||||
- Cross-platform builds: verify CI matrix covers all target OS/arch combinations, or documents which are untested
|
||||
- Version tag format consistency: `v1.2.3` vs `1.2.3` — must match across VERSION file, git tags, and publish scripts
|
||||
- Publish step idempotency: re-running the publish workflow should not fail (e.g., `gh release delete` before `gh release create`)
|
||||
- Scripts CI or a build relies on: strict mode (`set -euo pipefail` or the repo's equivalent) so a failed command fails the job, quoted variables, and commands that work on the CI runners' OS
|
||||
- Pinned versions: third-party actions pinned to a release tag or commit SHA (never a branch such as `@main`), and tools installed at a pinned version
|
||||
|
||||
**DO NOT flag:**
|
||||
- Web services with existing auto-deploy pipelines (Docker build + K8s deploy)
|
||||
- Internal tools not distributed outside the team
|
||||
- Test-only CI changes (adding test steps, not publish steps)
|
||||
- Test-only CI changes (adding test steps, not publish steps), except for the script and pinning checks above
|
||||
|
||||
#### Repo Conventions & Platform Fit (runs on every PR type)
|
||||
A finding here must name its concrete consequence (a script the CI runners cannot run, a second tool doing the same job); a style preference alone is not a finding.
|
||||
- New files use the languages, tools and naming the repo already uses for the same job: a `.bat` script in a repo whose scripts are bash plus a Makefile is a finding.
|
||||
- Scripts run on the platforms the CI runners and docs target.
|
||||
- New tooling repeats no existing tool (a second task runner, linter or install script beside the one the repo has).
|
||||
|
||||
---
|
||||
|
||||
@@ -171,7 +188,8 @@ CRITICAL (highest severity): INFORMATIONAL (main agent): SPECIALIST (p
|
||||
└─ Gate Integrity ├─ Time Window Safety ├─ API Contract specialist
|
||||
├─ Type Coercion at Boundaries ├─ Simplification (advisory)
|
||||
├─ View/Frontend └─ Red Team (conditional)
|
||||
└─ Distribution & CI/CD Pipeline
|
||||
├─ Distribution & CI/CD Pipeline
|
||||
└─ Repo Conventions & Platform Fit
|
||||
|
||||
All findings are actioned via Fix-First Review. Severity determines
|
||||
presentation order and classification of AUTO-FIX vs ASK — critical
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* /review's PR-type step (GSTA-224 H1, issue #356). The checklist names the PR type from the changed paths, keeps every
|
||||
* category on application and mixed diffs, always runs five categories on CI/infra, scripts/tooling and docs-only diffs,
|
||||
* and reports the rest in one not-applicable line. The classifier below is built from the patterns the checklist itself
|
||||
* lists, so a reworded rule that drops a pattern fails here before a reviewer reads it.
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const checklist = fs.readFileSync(path.join(import.meta.dir, '..', 'review', 'checklist.md'), 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
function between(text: string, start: string, end: string): string {
|
||||
const i = text.indexOf(start);
|
||||
expect(i).toBeGreaterThanOrEqual(0);
|
||||
const j = text.indexOf(end, i + start.length);
|
||||
expect(j).toBeGreaterThan(i);
|
||||
return text.slice(i, j);
|
||||
}
|
||||
|
||||
const block = () => between(checklist, '**PR type', '**Two-pass review:**');
|
||||
const TYPES = ['docs-only', 'CI/infra', 'scripts/tooling', 'application', 'mixed'] as const;
|
||||
type PrType = typeof TYPES[number];
|
||||
|
||||
function rule(type: PrType): string {
|
||||
const line = block().split('\n').find(l => l.startsWith('- **' + type + ':**'));
|
||||
expect(line).toBeDefined();
|
||||
return line!;
|
||||
}
|
||||
const ticks = (line: string) => [...line.matchAll(/\x60([^\x60]+)\x60/g)].map(m => m[1]!);
|
||||
const globRe = (glob: string) => new RegExp('^' + glob.replace(/[.+^\${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\u0000/g, '.*') + '$');
|
||||
const base = (p: string) => p.slice(p.lastIndexOf('/') + 1);
|
||||
|
||||
const MANIFESTS = ['package.json', 'bun.lock', 'bun.lockb', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.toml', 'Cargo.lock', 'go.mod', 'go.sum', 'Gemfile', 'Gemfile.lock', 'pyproject.toml', 'uv.lock'];
|
||||
const LINT_CONFIG = /^(\.eslintrc.*|eslint\.config\..*|\.prettierrc.*|prettier\.config\..*|biome\.json|\.shellcheckrc|\.editorconfig)$/;
|
||||
|
||||
function classifier() {
|
||||
const ci = ticks(rule('CI/infra')).map(globRe);
|
||||
const scriptRes = ticks(rule('scripts/tooling')).map(globRe);
|
||||
const app = rule('application');
|
||||
const appDirs = ticks(app).filter(t => t.endsWith('/'));
|
||||
const appGlobs = ticks(app).filter(t => t.startsWith('*')).map(globRe);
|
||||
expect(appDirs).toEqual(['bin/', 'scripts/', 'tools/']);
|
||||
expect(app).toContain('package manifests and lockfiles');
|
||||
expect(app).toContain('skill Markdown');
|
||||
expect(rule('docs-only')).toContain('\x60docs/\x60');
|
||||
const isScript = (p: string) => scriptRes.some(r => r.test(base(p)));
|
||||
return (p: string): Exclude<PrType, 'mixed'> => {
|
||||
if (appDirs.some(d => p.startsWith(d)) && !isScript(p)) return 'application';
|
||||
if (MANIFESTS.includes(base(p)) || appGlobs.some(r => r.test(base(p))) || base(p) === 'SKILL.md') return 'application';
|
||||
if (/\.(md|txt)$/.test(p) && (p.startsWith('docs/') || /^(README|CHANGELOG)/.test(base(p)))) return 'docs-only';
|
||||
if (ci.some(r => r.test(p) || r.test(base(p))) || /(^|\/)(docker-)?compose\.ya?ml$|\.tf$/.test(p)) return 'CI/infra';
|
||||
if (isScript(p) || LINT_CONFIG.test(base(p))) return 'scripts/tooling';
|
||||
return 'application';
|
||||
};
|
||||
}
|
||||
|
||||
function prType(paths: string[]): PrType {
|
||||
const one = classifier();
|
||||
const types = new Set(paths.map(one));
|
||||
return types.size > 1 ? 'mixed' : [...types][0]!;
|
||||
}
|
||||
|
||||
describe('review checklist PR type (GSTA-224 H1)', () => {
|
||||
test('the PR type is decided first and names the five types in order', () => {
|
||||
const b = block();
|
||||
expect(checklist.indexOf('**PR type')).toBeLessThan(checklist.indexOf('## Review Categories'));
|
||||
expect(b).toContain('\x60PR type: <type>\x60');
|
||||
let at = -1;
|
||||
for (const type of TYPES) {
|
||||
const i = b.indexOf('- **' + type + ':**');
|
||||
expect(i).toBeGreaterThan(at);
|
||||
at = i;
|
||||
}
|
||||
expect(rule('application')).toContain('When in doubt, application.');
|
||||
});
|
||||
|
||||
test.each(([
|
||||
['a .tmpl change', ['review/SKILL.md.tmpl', 'review/SKILL.md'], 'application'],
|
||||
['a bin/ TypeScript change', ['bin/gstack-design-detect.ts'], 'application'],
|
||||
['an extensionless bin/ executable', ['bin/gstack-approval-classes'], 'application'],
|
||||
['scripts/ TypeScript', ['scripts/skill-bin-drift.ts'], 'application'],
|
||||
['package.json + bun.lock', ['package.json', 'bun.lock'], 'application'],
|
||||
['a workflow-only change', ['.github/workflows/free-tests.yml'], 'CI/infra'],
|
||||
['a Dockerfile', ['Dockerfile.ci'], 'CI/infra'],
|
||||
['a .bat add at the root', ['build.bat'], 'scripts/tooling'],
|
||||
['a .bat add under scripts/', ['scripts/build.bat'], 'scripts/tooling'],
|
||||
['a shell script and the Makefile', ['scripts/release.sh', 'Makefile'], 'scripts/tooling'],
|
||||
['docs only', ['docs/skills.md', 'README.md'], 'docs-only'],
|
||||
['docs plus a checklist prompt', ['docs/skills.md', 'review/checklist.md'], 'mixed'],
|
||||
['a workflow plus application code', ['.github/workflows/ci.yml', 'lib/review-brief.ts'], 'mixed'],
|
||||
['a workflow plus a .bat script', ['.github/workflows/ci.yml', 'build.bat'], 'mixed'],
|
||||
['an unplaceable path', ['assets/logo.svg'], 'application'],
|
||||
] as Array<[string, string[], PrType]>).map(([name, paths, expected]) => [name + ' is ' + expected, paths, expected] as const))('%s', (_name, paths, expected) => {
|
||||
expect(prType(paths)).toBe(expected);
|
||||
});
|
||||
|
||||
test('application and mixed diffs run everything; other types always run five categories and report the rest in one line', () => {
|
||||
const scope = block().slice(block().indexOf('On application and mixed diffs'));
|
||||
expect(scope).toContain('On application and mixed diffs, run every category below exactly as written; nothing is skipped.');
|
||||
const always = /always run (.+?), and report/.exec(scope);
|
||||
expect(always).not.toBeNull();
|
||||
const names = always![1]!.split(/, | and (?=Repo)/);
|
||||
expect(names).toEqual(['Gate Integrity', 'Shell Injection', 'Completeness Gaps', 'Distribution & CI/CD Pipeline', 'Repo Conventions & Platform Fit']);
|
||||
const headings = checklist.split('\n').filter(l => l.startsWith('#### ')).map(l => l.slice(5));
|
||||
for (const name of names) expect(headings.some(h => h.startsWith(name))).toBe(true);
|
||||
expect(scope).toContain('once, in one line: \x60Not applicable (PR type: <type>): <categories>\x60');
|
||||
expect(scope).toContain('never counts as passed');
|
||||
});
|
||||
|
||||
test('Repo Conventions & Platform Fit needs a concrete consequence and Distribution gains the script checks', () => {
|
||||
const conventions = between(checklist, '#### Repo Conventions & Platform Fit', '\n---');
|
||||
expect(conventions).toContain('concrete consequence');
|
||||
expect(conventions).toContain('a \x60.bat\x60 script in a repo whose scripts are bash plus a Makefile is a finding');
|
||||
expect(conventions).toContain('platforms the CI runners and docs target');
|
||||
expect(conventions).toContain('repeats no existing tool');
|
||||
const distribution = between(checklist, '#### Distribution & CI/CD Pipeline', '#### Repo Conventions');
|
||||
for (const token of ['\x60set -euo pipefail\x60', 'quoted variables', "CI runners' OS", 'release tag or commit SHA', 'pinned version']) expect(distribution).toContain(token);
|
||||
expect(distribution).toContain('except for the script and pinning checks above');
|
||||
expect(between(checklist, '## Severity Classification', '## Fix-First Heuristic')).toContain('└─ Repo Conventions & Platform Fit');
|
||||
});
|
||||
|
||||
test('the addition stays near 25 lines', () => {
|
||||
const conventions = between(checklist, '#### Repo Conventions & Platform Fit', '\n---');
|
||||
const scriptChecks = checklist.split('\n').filter(l => l.startsWith('- Scripts CI or a build relies on:') || l.startsWith('- Pinned versions:'));
|
||||
expect(scriptChecks).toHaveLength(2);
|
||||
const lines = (block() + conventions).trimEnd().split('\n').length + scriptChecks.length + 1;
|
||||
expect(lines).toBeLessThanOrEqual(25);
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,7 @@ const NO_LOSS_REPLAYS = {
|
||||
'G1': "GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)",
|
||||
'L': "GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)",
|
||||
'GST-257': "GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged",
|
||||
'GSTA-224 H1': 'GSTA-224 H1 preregistered no-loss run: the PR-type checklist against main\'s checklist and a no-gstack arm (Opus 5.5, 81 calls), 5 behavior cases, 10 Round 7 application blockers and 2 clean controls; every bar passed (handoffs/GSTA-224/h1-noloss-results.md)',
|
||||
} as const;
|
||||
type Replay = keyof typeof NO_LOSS_REPLAYS;
|
||||
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
|
||||
@@ -63,7 +64,7 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
|
||||
'ship/sections/adversarial.md': { sha: 'd776ee5bc6e0c13930f8f4de05bad2f1ed9e52bc9fc5df80ae3ba7680455b2bb', replay: 'GST-257' },
|
||||
'review/sections/review-army.md': { sha: '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967', replay: '#3638' },
|
||||
'review/sections/adversarial.md': { sha: 'e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255', replay: 'G1' },
|
||||
'review/checklist.md': { sha: '607788afeb2c83f2dff1dccd58d212616ec72393d28cda2fa18690c1a83df699', replay: '#3638' },
|
||||
'review/checklist.md': { sha: 'a32963ef38d9d3613c58c805e8517cd2e593c63769b30dd7d40e2592fa48fc78', replay: 'GSTA-224 H1' },
|
||||
'review/design-checklist.md': { sha: 'b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83', replay: '#3638' },
|
||||
'review/gate-integrity.md': { sha: 'f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332', replay: '#3638' },
|
||||
'review/specialists/api-contract.md': { sha: '263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95', replay: '#3638' },
|
||||
|
||||
Reference in new issue
Block a user