mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
wip(GSTA-131): reviewers stay at full effort; audits light
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
fc5ee582e3
commit
3925e9b56d
9 files changed
+68
-63
No files matched your search
+3
-3
@@ -200,9 +200,9 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
|
||||
# # Absent = every gate keeps its effort. GSTACK_CODEX_EFFORT
|
||||
# # overrides it per run, /codex --effort per request;
|
||||
# # /spec keeps medium (120 s budget).
|
||||
# review_effort_tiers: on # on | off. on: read-and-report reviewers (specialists,
|
||||
# # audits, fix-checks, watchers) run at light effort on
|
||||
# # the same model; core and adversarial review keep full.
|
||||
# review_effort_tiers: on # on | off. on: audits, trail reviewers and watchers
|
||||
# # (nothing that can block a ship) run at light effort on
|
||||
# # the same model; every reviewer keeps full.
|
||||
# Docs: docs/model-policy.md
|
||||
#
|
||||
# ─── Advanced ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -15,6 +15,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
import { planReview, reviewPlanLine, scanNumbers, type History } from '../lib/review-depth';
|
||||
|
||||
const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth)
|
||||
@@ -31,7 +32,7 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
|
||||
--gated a,b specialists the hit-rate stats tag GATE_CANDIDATE
|
||||
--force a,b|all specialists forced by prompt flags (--all-specialists is all)
|
||||
--reviewed-numbers <fp> the numeric-claim fingerprint the outside voice already reviewed
|
||||
--no-tiers every role at the host's default effort (as gstack-config review_effort_tiers off does)
|
||||
--no-tiers audits and watchers at the host's default effort too (as gstack-config review_effort_tiers off does)
|
||||
--json the plan as JSON
|
||||
${exitTableHelp()}`;
|
||||
|
||||
@@ -70,30 +71,31 @@ function main(): number {
|
||||
}
|
||||
for (const p of untracked) { try { added[p] = fs.readFileSync(p, 'utf8').split('\n'); } catch { /* unreadable: no claims */ } }
|
||||
const binDir = path.dirname(Bun.fileURLToPath(import.meta.url));
|
||||
const scopeRun = spawnSync('bash', [path.join(binDir, 'gstack-diff-scope'), base], { encoding: 'utf8', timeout: 60_000 });
|
||||
const scopeRun = runScriptSync(path.join(binDir, 'gstack-diff-scope'), [base], { encoding: 'utf8', timeout: 60_000 });
|
||||
const scope: Record<string, string> = {};
|
||||
for (const l of (scopeRun.stdout ?? '').split('\n')) { const m = /^(SCOPE_[A-Z_]+)=(.*)$/.exec(l); if (m) scope[m[1]!] = m[2]!; }
|
||||
if (scopeRun.status !== 0 && !scope.SCOPE_ERROR) scope.SCOPE_ERROR = `exit ${scopeRun.status}`;
|
||||
let gateTagged: number | null = null;
|
||||
if (flag('--gate-tagged') !== undefined) gateTagged = Number(flag('--gate-tagged'));
|
||||
else {
|
||||
const g = spawnSync(path.join(binDir, 'gstack-gate-diff'), [base], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
||||
const g = runScriptSync(path.join(binDir, 'gstack-gate-diff'), [base], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
||||
const m = /GATE_SUMMARY: .*?tagged=\{([^}]*)\}/.exec(g.stdout ?? '');
|
||||
gateTagged = g.status === 0 && m ? m[1]!.split(',').reduce((n, kv) => n + (Number(kv.split(':')[1]) || 0), 0) : null;
|
||||
}
|
||||
if (gateTagged !== null && !Number.isFinite(gateTagged)) gateTagged = null;
|
||||
const force = flag('--force') === 'all' ? 'all' as const : list('--force');
|
||||
const tiersSetting = spawnSync(path.join(binDir, 'gstack-config'), ['get', 'review_effort_tiers'], { encoding: 'utf8', timeout: 15_000 });
|
||||
const tiers = !has('--no-tiers') && (tiersSetting.stdout ?? '').trim() !== 'off';
|
||||
const tiersSetting = runScriptSync(path.join(binDir, 'gstack-config'), ['get', 'review_effort_tiers'], { encoding: 'utf8', timeout: 15_000 });
|
||||
const auditEffort = has('--no-tiers') || (tiersSetting.stdout ?? '').trim() === 'off' ? 'host' : 'light';
|
||||
const plan = planReview({
|
||||
paths, deleted, stampOnly, scope, gateTagged, diffLines, numbers: scanNumbers(added), force, gated: list('--gated'),
|
||||
criticalFound: has('--critical-found'), reviewedNumbers: flag('--reviewed-numbers'), tiers,
|
||||
criticalFound: has('--critical-found'), reviewedNumbers: flag('--reviewed-numbers'),
|
||||
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below'), blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') },
|
||||
});
|
||||
if (has('--json')) { process.stdout.write(JSON.stringify({ ...plan, diff_lines: diffLines, pass, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n'); return EXIT.ok; }
|
||||
if (has('--json')) { process.stdout.write(JSON.stringify({ ...plan, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n'); return EXIT.ok; }
|
||||
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewPlanLine(plan, pass + (plan.next.step === 'full' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`];
|
||||
for (const d of plan.dispatches) out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}`);
|
||||
for (const s of plan.skipped) out.push(`SKIP: ${s}`);
|
||||
out.push(`AUDIT_EFFORT: ${auditEffort} (coverage, plan-completion and docs audits, decision-trail reviewers, watchers; reviewers above stay full)`);
|
||||
process.stdout.write(out.join('\n') + '\n');
|
||||
return EXIT.ok;
|
||||
}
|
||||
|
||||
+12
-9
@@ -156,15 +156,18 @@ set, because a higher effort would spend the time the gate is sized for.
|
||||
|
||||
### Review effort tiers
|
||||
|
||||
Subagents that only read and report run at a lighter reasoning effort on the
|
||||
same model; gstack never swaps in a cheaper model family for them.
|
||||
`gstack-review-plan` names each dispatch's tier: `light` for specialists,
|
||||
audits, fix-checks, decision-trail reviewers and watchers, `full` for the core
|
||||
checklist, the Red Team and the native adversarial pass, `host` for the outside
|
||||
voice (its provider and `codex_effort` above). A host that can set a
|
||||
subagent's effort maps `light` to a medium or low setting; one that cannot runs
|
||||
it at its default. `gstack-config set review_effort_tiers off` (default `on`)
|
||||
renders every role at the host default
|
||||
Every reviewer that can block a ship runs at `full` effort on the lane's
|
||||
model: the core checklist, each specialist, the Red Team, the native
|
||||
adversarial pass and the fix-check. `gstack-review-plan` names each
|
||||
dispatch's effort, and `host` for the outside voice (its provider and
|
||||
`codex_effort` above). Only work that cannot block runs `light`, the same
|
||||
model at a medium or low setting and never a cheaper model family: the coverage,
|
||||
plan-completion and docs audits, decision-trail reviewers and watchers
|
||||
(`AUDIT_EFFORT:` in the plan's output). A paid replay showed why reviewers stay
|
||||
at full: a medium-effort fix-check missed a fail-open in gate code that the same
|
||||
check at full effort caught. A host that cannot set a subagent's effort runs it
|
||||
at its default. `gstack-config set review_effort_tiers off` (default `on`)
|
||||
runs the audits and watchers at the host default too
|
||||
([review-policy.md](review-policy.md#review-depth)).
|
||||
|
||||
## Which workflows use the tiers?
|
||||
|
||||
+12
-11
@@ -111,13 +111,13 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
| a full pass that fixed a blocking finding, below the cap | full pass on the range |
|
||||
| a full pass whose fix touched a gate-class path, below the cap | full pass on the range |
|
||||
| a full pass that fixed only below-floor findings | one fix-check |
|
||||
| a full pass at the cap that fixed a blocking finding | one fix-check (full effort when the fix touched gate code) |
|
||||
| a full pass at the cap that fixed a blocking finding | one fix-check |
|
||||
| a full pass at the cap, or with no fix, leaving a blocking finding open | stop: name it for the owner |
|
||||
| a fix-check that found nothing blocking | done |
|
||||
| a fix-check that found a blocking finding | stop: name it for the owner |
|
||||
|
||||
A **fix-check** is one reviewer reading the touched hunks, the fix list, the
|
||||
test results and the callers of every changed symbol, at light effort. It is
|
||||
test results and the callers of every changed symbol, at full effort. It is
|
||||
logged as a `review-fix-check` row, so it never counts as a pass. No army, no
|
||||
Red Team and no outside voice run on it.
|
||||
|
||||
@@ -148,15 +148,16 @@ outside voice reruns, on the numbers.
|
||||
**The adversarial and outside voices run once**, on pass 1's diff (Step 11).
|
||||
Later full passes rerun the specialists on the range only.
|
||||
|
||||
**Effort tiers.** Reviewers that only read and report (specialists, the Step 7
|
||||
coverage audit, the Step 8 plan-completion audit, the Step 14.5 docs audit,
|
||||
fix-checks, decision-trail reviewers, watchers) run at `effort: light`: the
|
||||
same model at a lower reasoning effort, never a cheaper model family. The core
|
||||
checklist, the Red Team and the native adversarial pass stay at `full`. The
|
||||
outside voice keeps its own provider and effort (`codex_effort`). A host that
|
||||
cannot set a subagent's effort runs it at its default.
|
||||
`gstack-config set review_effort_tiers off` (or `--no-tiers`) renders every
|
||||
role at the host default ([model-policy.md](model-policy.md#reasoning-effort)).
|
||||
**Effort.** Every reviewer that can block a ship (the core checklist,
|
||||
specialists, the Red Team, the native adversarial pass, fix-checks) runs at
|
||||
`full` effort. Work that cannot block (the Step 7 coverage audit, the Step 8
|
||||
plan-completion audit, the Step 14.5 docs audit, decision-trail reviewers,
|
||||
watchers) runs at `light`: the same model at a lower reasoning effort, never a
|
||||
cheaper model family. The outside voice keeps its own provider and effort
|
||||
(`codex_effort`). A host that cannot set a subagent's effort runs it at its
|
||||
default. `gstack-config set review_effort_tiers off` (or `--no-tiers`) runs
|
||||
the audits and watchers at the host default too
|
||||
([model-policy.md](model-policy.md#reasoning-effort)).
|
||||
|
||||
## What the receipt records
|
||||
|
||||
|
||||
+11
-14
@@ -112,7 +112,7 @@ export interface History {
|
||||
/** A fix in the finished step touched a gate-class path. */
|
||||
touchedGate?: boolean;
|
||||
}
|
||||
export interface NextStep { step: Step; reason: string; effort: Effort }
|
||||
export interface NextStep { step: Step; reason: string }
|
||||
|
||||
/**
|
||||
* The transition table (docs/review-policy.md#transitions). Pass 1 is full. A fixed
|
||||
@@ -121,13 +121,13 @@ export interface NextStep { step: Step; reason: string; effort: Effort }
|
||||
* allowed step stops for the owner.
|
||||
*/
|
||||
export function nextStep(h: History): NextStep {
|
||||
if (h.last === 'none') return { step: 'full', reason: 'pass 1 reviews the whole diff', effort: 'full' };
|
||||
if (h.last === 'none') return { step: 'full', reason: 'pass 1 reviews the whole diff' };
|
||||
const fixed = !!(h.fixedBlocking || h.fixedBelow);
|
||||
if (h.last === 'fix-check') return h.blockingOpen || fixed ? { step: 'stop', reason: 'the fix-check found a blocking finding: name it for the owner, never a third full pass', effort: 'full' } : { step: 'done', reason: 'the fix-check found nothing blocking', effort: 'full' };
|
||||
if (h.blockingOpen && h.fullPasses >= FULL_PASS_CAP) return { step: 'stop', reason: `a blocking finding is open after ${FULL_PASS_CAP} full passes: name it for the owner`, effort: 'full' };
|
||||
if (!fixed) return h.blockingOpen ? { step: 'stop', reason: 'a blocking finding is open with no fix applied', effort: 'full' } : { step: 'done', reason: 'the step applied no edits', effort: 'full' };
|
||||
if ((h.fixedBlocking || h.touchedGate) && h.fullPasses < FULL_PASS_CAP) return { step: 'full', reason: h.fixedBlocking ? 'a blocking finding was fixed: full pass on the range' : 'a fix touched a gate-class path: full pass on the range', effort: 'full' };
|
||||
return { step: 'fix-check', reason: h.fixedBlocking ? `full-pass cap ${FULL_PASS_CAP} reached: one fix-check reads the fix` : 'only below-floor fixes: one fix-check reads the touched hunks', effort: h.touchedGate ? 'full' : 'light' };
|
||||
if (h.last === 'fix-check') return h.blockingOpen || fixed ? { step: 'stop', reason: 'the fix-check found a blocking finding: name it for the owner, never a third full pass' } : { step: 'done', reason: 'the fix-check found nothing blocking' };
|
||||
if (h.blockingOpen && h.fullPasses >= FULL_PASS_CAP) return { step: 'stop', reason: `a blocking finding is open after ${FULL_PASS_CAP} full passes: name it for the owner` };
|
||||
if (!fixed) return h.blockingOpen ? { step: 'stop', reason: 'a blocking finding is open with no fix applied' } : { step: 'done', reason: 'the step applied no edits' };
|
||||
if ((h.fixedBlocking || h.touchedGate) && h.fullPasses < FULL_PASS_CAP) return { step: 'full', reason: h.fixedBlocking ? 'a blocking finding was fixed: full pass on the range' : 'a fix touched a gate-class path: full pass on the range' };
|
||||
return { step: 'fix-check', reason: h.fixedBlocking ? `full-pass cap ${FULL_PASS_CAP} reached: one fix-check reads the fix` : 'only below-floor fixes: one fix-check reads the touched hunks' };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -145,8 +145,6 @@ export interface PlanInput extends ClassInput {
|
||||
criticalFound?: boolean;
|
||||
/** The numeric-claim fingerprint the outside voice last reviewed; a change reruns it. */
|
||||
reviewedNumbers?: string;
|
||||
/** review_effort_tiers: off renders every role at the host's default effort. */
|
||||
tiers?: boolean;
|
||||
}
|
||||
export interface Dispatch { role: Role; name: string; effort: Effort }
|
||||
export interface ReviewPlan { class: DiffClass; reasons: string[]; next: NextStep; numbers: NumbersResult; dispatches: Dispatch[]; skipped: string[] }
|
||||
@@ -182,18 +180,17 @@ export function specialistsFor(cls: DiffClass, i: PlanInput, skipped: string[] =
|
||||
export function planReview(i: PlanInput): ReviewPlan {
|
||||
const { class: cls, reasons } = classifyDiff(i);
|
||||
const next = nextStep(i.history);
|
||||
const tier = (e: Effort): Effort => (i.tiers === false ? 'host' : e);
|
||||
const skipped: string[] = [];
|
||||
const dispatches: Dispatch[] = [];
|
||||
const numbersChanged = i.numbers.publishes && i.reviewedNumbers !== undefined && i.reviewedNumbers !== i.numbers.fingerprint;
|
||||
if (next.step === 'full') {
|
||||
for (const n of specialistsFor(cls, i, skipped)) dispatches.push({ role: 'specialist', name: n, effort: tier('light') });
|
||||
for (const n of specialistsFor(cls, i, skipped)) dispatches.push({ role: 'specialist', name: n, effort: 'full' });
|
||||
const redTeam = cls === 'gate' || (cls === 'code' && (i.diffLines > RED_TEAM_LINES || !!i.criticalFound));
|
||||
if (redTeam) dispatches.push({ role: 'red-team', name: 'red-team', effort: tier('full') });
|
||||
if (redTeam) dispatches.push({ role: 'red-team', name: 'red-team', effort: 'full' });
|
||||
else skipped.push(`red-team (${cls} class${cls === 'code' ? `, ${i.diffLines} lines, no critical` : ''})`);
|
||||
if (i.history.fullPasses === 0) {
|
||||
if (cls === 'docs' || cls === 'tests') skipped.push(`adversarial-native (${cls} class)`);
|
||||
else dispatches.push({ role: 'adversarial-native', name: 'adversarial', effort: tier('full') });
|
||||
else dispatches.push({ role: 'adversarial-native', name: 'adversarial', effort: 'full' });
|
||||
if (cls === 'code' || cls === 'gate' || i.numbers.publishes) dispatches.push({ role: 'outside-voice', name: 'outside-voice', effort: 'host' });
|
||||
else skipped.push(`outside-voice (${cls} class, no numeric claims)`);
|
||||
} else {
|
||||
@@ -201,7 +198,7 @@ export function planReview(i: PlanInput): ReviewPlan {
|
||||
if (numbersChanged) dispatches.push({ role: 'outside-voice', name: 'outside-voice (numbers changed)', effort: 'host' });
|
||||
}
|
||||
} else if (next.step === 'fix-check') {
|
||||
dispatches.push({ role: 'fix-check', name: 'fix-check', effort: tier(next.effort) });
|
||||
dispatches.push({ role: 'fix-check', name: 'fix-check', effort: 'full' });
|
||||
if (numbersChanged) dispatches.push({ role: 'outside-voice', name: 'outside-voice (numbers changed)', effort: 'host' });
|
||||
}
|
||||
return { class: cls, reasons, next, numbers: i.numbers, dispatches, skipped };
|
||||
|
||||
@@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
import { planReview, specialistsFor, type DiffClass, type Dispatch, type History, type NumbersResult } from '../lib/review-depth';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
@@ -113,7 +114,7 @@ function capture(sha: string, name: string, out: string): number {
|
||||
try {
|
||||
if (git(['worktree', 'add', '-q', '--detach', dir, sha]).status !== 0) throw new Error(`cannot check out ${sha}`);
|
||||
git(['branch', '-f', 'gsta131-capture-base', `${sha}^`], dir);
|
||||
const r = spawnSync(path.join(ROOT, 'bin', 'gstack-review-plan'), ['--base', 'gsta131-capture-base', '--json'], { cwd: dir, encoding: 'utf8', timeout: 300_000, maxBuffer: 64 * 1024 * 1024 });
|
||||
const r = runScriptSync(path.join(ROOT, 'bin', 'gstack-review-plan'), ['--base', 'gsta131-capture-base', '--json'], { cwd: dir, encoding: 'utf8', timeout: 300_000, maxBuffer: 64 * 1024 * 1024 });
|
||||
if (r.status !== 0) throw new Error(`gstack-review-plan failed: ${r.stderr}`);
|
||||
const plan = JSON.parse(r.stdout);
|
||||
const diffBytes = Buffer.byteLength(git(['diff', `${sha}^`, sha], dir).stdout ?? '');
|
||||
|
||||
+13
-12
@@ -35,10 +35,10 @@ describe('nextStep: the transition table', () => {
|
||||
test('pass 1 is full', () => expect(nextStep({ fullPasses: 0, last: 'none' }).step).toBe('full'));
|
||||
test('no edits is done', () => expect(nextStep({ fullPasses: 1, last: 'full' }).step).toBe('done'));
|
||||
test('a fixed blocker earns full pass 2', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBlocking: true }).step).toBe('full'));
|
||||
test('below-floor fixes earn one light fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true })).toMatchObject({ step: 'fix-check', effort: 'light' }));
|
||||
test('below-floor fixes earn one fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true }).step).toBe('fix-check'));
|
||||
test('a fix touching gate code earns a full pass instead of a fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, touchedGate: true }).step).toBe('full'));
|
||||
test('at the full-pass cap a fixed blocker gets a fix-check', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true }).step).toBe('fix-check'));
|
||||
test('at the cap a gate-touching fix gets a full-effort fix-check', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true })).toMatchObject({ step: 'fix-check', effort: 'full' }));
|
||||
test('at the cap a gate-touching fix still gets a fix-check, never a third full pass', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true }).step).toBe('fix-check'));
|
||||
test('a blocker open at the cap stops', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', blockingOpen: true }).step).toBe('stop'));
|
||||
test('a blocker open with no fix stops', () => expect(nextStep({ fullPasses: 1, last: 'full', blockingOpen: true }).step).toBe('stop'));
|
||||
test('a fix-check that finds a blocker stops, never a third full pass', () => {
|
||||
@@ -73,19 +73,19 @@ describe('planReview', () => {
|
||||
});
|
||||
test('tests class: testing specialist only', () => {
|
||||
const p = planReview(base({ paths: ['test/a.test.ts'], scope: { SCOPE_TESTS: 'true' } }));
|
||||
expect(roles(p)).toEqual(['specialist:testing:light']);
|
||||
expect(roles(p)).toEqual(['specialist:testing:full']);
|
||||
});
|
||||
test('code class pass 1: specialists light, adversarial full, outside voice', () => {
|
||||
test('code class pass 1: every reviewer at full effort, the outside voice at its own', () => {
|
||||
const p = planReview(base({}));
|
||||
expect(roles(p)).toEqual(['specialist:testing:light', 'specialist:maintainability:light', 'specialist:security:light', 'specialist:performance:light', 'specialist:simplification:light', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
|
||||
expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:security:full', 'specialist:performance:full', 'specialist:simplification:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
|
||||
});
|
||||
test('gate class forces red team and ignores hit-rate gating', () => {
|
||||
const p = planReview(base({ gateTagged: 1, gated: ['security'] }));
|
||||
expect(roles(p)).toContain('red-team:red-team:full');
|
||||
expect(roles(p)).toContain('specialist:security:light');
|
||||
expect(roles(p)).toContain('specialist:security:full');
|
||||
});
|
||||
test('code class honours hit-rate gating and small-diff skip', () => {
|
||||
expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:light');
|
||||
expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:full');
|
||||
expect(planReview(base({ diffLines: 20 })).dispatches.filter(d => d.role === 'specialist')).toEqual([]);
|
||||
});
|
||||
test('pass 2 reruns the army on the range but not the adversarial or outside voice', () => {
|
||||
@@ -93,16 +93,17 @@ describe('planReview', () => {
|
||||
expect(p.next.step).toBe('full');
|
||||
expect(roles(p).some(r => r.startsWith('outside-voice') || r.startsWith('adversarial'))).toBe(false);
|
||||
});
|
||||
test('a fix-check is one light dispatch', () => {
|
||||
expect(roles(planReview(base({ history: { fullPasses: 1, last: 'full', fixedBelow: true } })))).toEqual(['fix-check:fix-check:light']);
|
||||
test('a fix-check is one full-effort dispatch', () => {
|
||||
expect(roles(planReview(base({ history: { fullPasses: 1, last: 'full', fixedBelow: true } })))).toEqual(['fix-check:fix-check:full']);
|
||||
});
|
||||
test('changed numeric claims rerun only the outside voice', () => {
|
||||
const numbers = scanNumbers({ 'docs/bench.md': ['3x faster'] });
|
||||
const p = planReview(base({ numbers, reviewedNumbers: 'old', history: { fullPasses: 1, last: 'full', fixedBelow: true } }));
|
||||
expect(roles(p)).toEqual(['fix-check:fix-check:light', 'outside-voice:outside-voice (numbers changed):host']);
|
||||
expect(roles(p)).toEqual(['fix-check:fix-check:full', 'outside-voice:outside-voice (numbers changed):host']);
|
||||
});
|
||||
test('tiers off renders every role at the host default', () => {
|
||||
expect(planReview(base({ tiers: false })).dispatches.every(d => d.effort === 'host')).toBe(true);
|
||||
test('no reviewer runs below full effort (GSTA-131 eval: a light fix-check missed a fail-open a full one caught)', () => {
|
||||
for (const h of [{ fullPasses: 0, last: 'none' }, { fullPasses: 1, last: 'full', fixedBelow: true }, { fullPasses: 1, last: 'full', fixedBlocking: true }] as const)
|
||||
expect(planReview(base({ gateTagged: 1, history: h })).dispatches.filter(d => d.role !== 'outside-voice').every(d => d.effort === 'full')).toBe(true);
|
||||
});
|
||||
test('done dispatches nothing', () => expect(planReview(base({ history: { fullPasses: 1, last: 'full' } })).dispatches).toEqual([]));
|
||||
});
|
||||
@@ -33,18 +33,18 @@ describe('gstack-review-plan', () => {
|
||||
const r = run(repo({ 'docs/bench.md': 'Median latency fell 42% to 180 ms.\n' }), []);
|
||||
expect(r.stdout).toContain('DISPATCH: outside-voice outside-voice effort=host');
|
||||
});
|
||||
test('--json carries the class, the next step and every dispatch with its effort; --no-tiers renders host effort', () => {
|
||||
test('--json carries the class, the next step, every dispatch with its effort and the audit effort; --no-tiers renders audits at host effort', () => {
|
||||
const r = repo({ 'src/a.ts': Array.from({ length: 80 }, (_, i) => `export const v${i} = ${i};`).join('\n') + '\n' });
|
||||
const plan = JSON.parse(run(r, ['--json']).stdout);
|
||||
expect(plan).toMatchObject({ class: 'code', next: { step: 'full' }, pass: 0 });
|
||||
expect(plan.dispatches.find((d: any) => d.role === 'specialist').effort).toBe('light');
|
||||
const host = JSON.parse(run(r, ['--json', '--no-tiers']).stdout);
|
||||
expect(host.dispatches.every((d: any) => d.effort === 'host')).toBe(true);
|
||||
expect(plan.dispatches.find((d: any) => d.role === 'specialist').effort).toBe('full');
|
||||
expect(plan.audit_effort).toBe('light');
|
||||
expect(JSON.parse(run(r, ['--json', '--no-tiers']).stdout).audit_effort).toBe('host');
|
||||
});
|
||||
test('a below-floor fix after pass 1 plans one fix-check', () => {
|
||||
const r = run(repo({ 'src/a.ts': 'export const a = 1;\n' }), ['--pass', '1', '--last', 'full', '--fixed-below']);
|
||||
expect(r.stdout).toContain('next=fix-check dispatches=1');
|
||||
expect(r.stdout).toContain('DISPATCH: fix-check fix-check effort=light');
|
||||
expect(r.stdout).toContain('DISPATCH: fix-check fix-check effort=full');
|
||||
});
|
||||
test('usage errors exit 2', () => {
|
||||
expect(spawnSync(BIN, [], { encoding: 'utf8', timeout: 10_000 }).status).toBe(2);
|
||||
|
||||
@@ -55,5 +55,5 @@ describe('carry with a recorded pregate', () => {
|
||||
const after = extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
|
||||
expect(after.head).toBe(head);
|
||||
expect(after.pregate).toContain(`current for ${head.slice(0, 12)}`);
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
Reference in new issue
Block a user