diff --git a/bin/gstack-review-plan b/bin/gstack-review-plan index a6ea1740d..24a661895 100755 --- a/bin/gstack-review-plan +++ b/bin/gstack-review-plan @@ -39,7 +39,8 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do --adversarial-ran Step 11's native adversarial pass already ran (a repeat skips it) --gate-tagged gate hunks the gate scan tagged (default: run gstack-gate-diff; failure means unavailable) --gated a,b specialists the hit-rate stats tag GATE_CANDIDATE - --force a,b|all specialists forced by prompt flags (--all-specialists is all) + --force a,b|all checklists forced into the batched specialist by prompt flags (--all-specialists is all); maintainability, + performance and simplification are read only when forced --reviewed-numbers the numeric-claim fingerprint the outside voice already reviewed --depth-floor standard|full raise the depth to at least this (never lowers it); /ship passes its prompt flag on every run --range-base the fix range's base (this pass's PASS_HEAD): after a fixed blocker on a diff below full depth, a diff --git a/docs/review-policy.md b/docs/review-policy.md index 94c561245..744e47101 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -187,9 +187,18 @@ in a data file still schedules the outside voice. | Depth | When | Pass 1 dispatches | |---|---|---| | **light**, runtime 0 | docs and tests only, or only registered outputs | the core checklist only; the outside voice when the diff publishes numbers | -| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (testing, maintainability and security checklists, the specialists scope selects and the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers | -| **standard** | more than 50 runtime lines | the code set with every threshold (specialists at 50, simplification at 100, Red Team at 200) read on runtime lines; specialists leave docs and registered outputs out of their diff, and every specialist but Testing leaves tests out too; adversarial and outside voice read everything | -| **full** | gate class, or any classifier error | the gate set, unchanged: every reviewer on the whole diff, the Red Team always, no hit-rate gating | +| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (the batched checklists below plus the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers | +| **standard** | more than 50 runtime lines | one batched specialist at `medium` and the Red Team at 200 runtime lines (or on a CRITICAL); the batched reader leaves docs and registered outputs out of its diff; adversarial and outside voice read everything | +| **full** | gate class, or any classifier error | one batched specialist on the whole diff, the Red Team always, no hit-rate gating | + +**Batched checklists (the lean ensemble, GSTA-167 part 2).** One specialist +subagent reads Testing and Security on every code or gate diff, plus +State-machines, Data-migration, API-contract and Design when scope asks (a +forced checklist always joins). Maintainability, Performance and Simplification +are read only when forced (`SKIP:` names them). The Red Team reads the batched +reader's merged findings. A free re-score of saved C and D findings lost 0 of +the 29 blockers (handoffs/GSTA-167/lean-ensemble-rescore.md); live Arm L must pass +before this merges. A fixed blocking finding, a CRITICAL or an open blocker raises a light diff to standard for the next step (`step=standard`): its specialists read at least 50 lines @@ -265,6 +274,8 @@ the specialists, audits and watchers at the host default too Team and the native adversarial pass on the range instead of the specialist army. - A tests-only diff (runtime 0) no longer dispatches the Testing specialist: the core checklist and the gate scan review it. +- Every depth dispatches one batched specialist instead of one reader per + checklist; Maintainability, Performance and Simplification run only when forced. - Light no longer runs a separate Red Team: the batched specialist carries its prompt. At runtime 0 light runs no adversarial pass either. - Standard reads thresholds on runtime lines, not the whole diff, so a test-heavy diff --git a/docs/skills.md b/docs/skills.md index 405d690e8..8e760a90c 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -733,7 +733,7 @@ Every `/ship` run builds a code path map from your diff, searches for correspond ### Review gate -`/ship` displays historical review readiness in the [Review Readiness Dashboard](#review-readiness-dashboard) during preflight. A missing Eng Review is reported without an extra question; it does not replace or waive the current pre-landing review. Step 9 still runs the checklist, applicable specialists and bounded exploratory QA, with its existing approval and completion gates. +`/ship` displays historical review readiness in the [Review Readiness Dashboard](#review-readiness-dashboard) during preflight. A missing Eng Review is reported without an extra question; it does not replace or waive the current pre-landing review. Step 9 still runs the checklist, one batched specialist (Testing and Security, plus the checklists scope selects) and bounded exploratory QA, with its existing approval and completion gates. A lot of branches die when the interesting work is done and only the boring release work is left. Humans procrastinate that part. AI should not. diff --git a/lib/review-depth.ts b/lib/review-depth.ts index 201745ab0..a7af650d3 100644 --- a/lib/review-depth.ts +++ b/lib/review-depth.ts @@ -29,8 +29,10 @@ export const RED_TEAM_LINES = 200; export const LARGE_DIFF_LINES = 100; /** The largest fix range, in runtime lines, a pass 2 after a blocking fix reviews as a scoped pass (GSTA-152 D6); a starting value. */ export const SCOPED_RANGE_LINES = 150; -/** The checklists light's batched specialist always reads, whatever the scope. */ -export const LIGHT_FIXED: readonly Specialist[] = ['testing', 'maintainability', 'security']; +/** The checklists the batched specialist always reads on code and gate class, whatever the depth or scope (GSTA-167 part 2). */ +export const LIGHT_FIXED: readonly Specialist[] = ['testing', 'security']; +/** The checklists the batched specialist adds when scope asks; maintainability, performance and simplification are read only when forced. */ +export const BATCH_SCOPED: readonly Specialist[] = ['data-migration', 'api-contract', 'design', 'state-machines']; /** * Each blocking role's effort (GSTA-167 part 3, CREW 30c): specialists at medium, which is how the no-loss controls ran * them; the Red Team, the native adversarial pass and the fix-check at full (the lane's reasoning); the outside voice at @@ -64,7 +66,7 @@ const DATA_CONFIG = [ /** A hash of the constants and patterns that decide a depth, not of this file's bytes: a comment edit keeps it, a threshold edit changes it. */ export const DEPTH_RULES = createHash('sha256').update(JSON.stringify({ - SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED, SCOPED_RANGE_LINES, ROLE_EFFORT, + SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED, BATCH_SCOPED, SCOPED_RANGE_LINES, ROLE_EFFORT, patterns: Object.entries({ GATE_PATH, AGENT_PROSE, TEST_PATH, RELEASE_METADATA, DOC_PATH: [DOC_PATH], DATA_PATH: [DATA_PATH], DATA_CONFIG }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]), })).digest('hex').slice(0, 16); @@ -290,7 +292,7 @@ export interface PlanInput extends ClassInput { /** false under --no-tiers or review_effort_tiers off: specialists run at the host's default effort. */ tiers?: boolean; } -/** One reviewer. `exclude` holds git pathspec argv items the reviewer's diff leaves out; `checklists` names what light's batched specialist reads. */ +/** One reviewer. `exclude` holds git pathspec argv items the reviewer's diff leaves out; `checklists` names what the batched specialist reads. */ export interface Dispatch { role: Role; name: string; effort: Effort; exclude: string[]; checklists?: string[]; reason?: string } export interface ReviewPlan extends DepthResult { /** The depth this step runs at: light raised to standard by a fixed blocker, a CRITICAL or an open blocker. */ @@ -332,6 +334,29 @@ export function specialistsFor(cls: DiffClass, i: PlanInput, skipped: string[] = }); } +/** + * The checklists the one batched specialist reads (the lean ensemble, GSTA-167 part 2): the class sets the base (none for docs, + * testing for tests, LIGHT_FIXED for code and gate), BATCH_SCOPED joins when scope asks and hit-rate gating allows (gate class + * ignores gating), and a forced checklist always joins. + */ +export function batchChecklists(cls: DiffClass, i: PlanInput, skipped: string[] = []): Specialist[] { + const forced = forcedBy(i); + const codeLike = cls === 'code' || cls === 'gate'; + const base: readonly Specialist[] = codeLike ? LIGHT_FIXED : cls === 'tests' ? ['testing'] : []; + if (cls === 'docs') skipped.push('specialists (docs class)'); + if (cls === 'tests') skipped.push('specialists except testing (tests class)'); + const extra = SPECIALISTS.filter(n => { + if (base.includes(n)) return false; + if (forced(n)) return true; + if (!codeLike || !BATCH_SCOPED.includes(n) || !scopeWants[n]!(i.scope ?? {})) return false; + if (cls !== 'gate' && (i.gated ?? []).includes(n)) { skipped.push(`${n} (gated by hit rate)`); return false; } + return true; + }); + const dropped = SPECIALISTS.filter(n => codeLike && !BATCH_SCOPED.includes(n) && !base.includes(n) && !extra.includes(n)); + if (dropped.length) skipped.push(`${dropped.join(', ')} (lean ensemble: read only when forced)`); + return [...base, ...extra]; +} + /** A path as one git exclude pathspec; a glob character or a leading colon makes it literal. */ const excludeSpec = (p: string) => `:(exclude${/[*?[\]\\]/.test(p) || p.startsWith(':') ? ',literal' : ''})${p}`; const shellQuote = (s: string) => `'${s.replace(/'/g, "'\\''")}'`; @@ -355,15 +380,13 @@ export function planReview(i: PlanInput): ReviewPlan { const dispatches: Dispatch[] = []; const add = (role: Role, name: string, effort: Effort, exclude: string[] = [], extra: Partial = {}) => dispatches.push({ role, name, effort, exclude, ...extra }); const docsAndOutputs = i.paths.filter(p => pathClass(i, p) === 'docs' || isOutput(i, p)).map(excludeSpec); - const tests = i.paths.filter(p => pathClass(i, p) === 'tests' && !isOutput(i, p)).map(excludeSpec); const numbersChanged = i.numbers.publishes && i.reviewedNumbers !== undefined && i.reviewedNumbers !== i.numbers.fingerprint; const firstVoices = h.fullPasses === 0 || !i.adversarialRan; const specialist: Effort = i.tiers === false ? 'host' : ROLE_EFFORT.specialist; if (next.step === 'full' && stepDepth === 'light') { const forced = forcedBy(i); if (runtimeLines > 0) { - const scoped = SPECIALISTS.filter(n => !LIGHT_FIXED.includes(n) && (forced(n) || (scopeWants[n]?.(i.scope ?? {}) && !(i.gated ?? []).includes(n)))); - add('specialist', 'batched', ROLE_EFFORT['red-team'], docsAndOutputs, { checklists: [...LIGHT_FIXED, ...scoped, 'red-team'] }); + add('specialist', 'batched', ROLE_EFFORT['red-team'], docsAndOutputs, { checklists: [...batchChecklists('code', i, skipped), 'red-team'] }); skipped.push('red-team (light depth: the batched specialist carries the Red Team prompt)'); if (firstVoices) add('adversarial-native', 'adversarial', 'full', [], { reason: LIGHT_ADVERSARIAL_REASON }); } else { @@ -378,7 +401,8 @@ export function planReview(i: PlanInput): ReviewPlan { const dcls: DiffClass = full ? 'gate' : cls; const raised = !full && runtimeLines <= SMALL_DIFF_LINES; const lines = full ? i.diffLines : Math.max(runtimeLines, raised ? SMALL_DIFF_LINES : 0); - for (const n of specialistsFor(dcls, { ...i, diffLines: lines }, skipped)) add('specialist', n, specialist, full ? [] : n === 'testing' ? docsAndOutputs : [...docsAndOutputs, ...tests]); + const checklists = batchChecklists(dcls, i, skipped); + if (checklists.length) add('specialist', 'batched', specialist, full ? [] : docsAndOutputs, { checklists }); const redTeam = dcls === 'gate' || (dcls === 'code' && (lines > RED_TEAM_LINES || !!i.criticalFound || raised)); if (redTeam) add('red-team', 'red-team', 'full'); else skipped.push(`red-team (${dcls} class${dcls === 'code' ? `, ${lines} runtime lines, no critical` : ''})`); diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md index 15281b71d..450f3adae 100644 --- a/ship/sections/review-army.md +++ b/ship/sections/review-army.md @@ -23,8 +23,8 @@ Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth, (specialists, Red Team, Step 11's voices), each at its `effort=` on the same model family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low; a host that cannot set it runs its default) and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules; -`specialist batched` (light depth) is one subagent reading every checklist its -`checklists=` names, Red Team included, tagging each finding with its checklist. +`specialist batched` is one subagent reading every checklist its +`checklists=` names (light adds Red Team), tagging each finding with its checklist. A `SKIP:` reviewer logs `{"dispatched":false,"reason":"scope"}`. Audits run at `AUDIT_EFFORT:`. The core checklist, gate scan and QA always run. Item 6's row adds `"dispatches":[{"role":R,"prompt_bytes":N,"diff_bytes":N}]`, `"depth"`, diff --git a/ship/sections/review-army.md.tmpl b/ship/sections/review-army.md.tmpl index 8c370f98c..8b349c4e9 100644 --- a/ship/sections/review-army.md.tmpl +++ b/ship/sections/review-army.md.tmpl @@ -21,8 +21,8 @@ Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth, (specialists, Red Team, Step 11's voices), each at its `effort=` on the same model family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low; a host that cannot set it runs its default) and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules; -`specialist batched` (light depth) is one subagent reading every checklist its -`checklists=` names, Red Team included, tagging each finding with its checklist. +`specialist batched` is one subagent reading every checklist its +`checklists=` names (light adds Red Team), tagging each finding with its checklist. A `SKIP:` reviewer logs `{"dispatched":false,"reason":"scope"}`. Audits run at `AUDIT_EFFORT:`. The core checklist, gate scan and QA always run. Item 6's row adds `"dispatches":[{"role":R,"prompt_bytes":N,"diff_bytes":N}]`, `"depth"`, diff --git a/test/review-depth.test.ts b/test/review-depth.test.ts index 84f8d3f0e..0c7620c69 100644 --- a/test/review-depth.test.ts +++ b/test/review-depth.test.ts @@ -10,6 +10,7 @@ const ROOT = path.join(import.meta.dir, '..'); const none = scanNumbers({}); const base = (o: Partial): PlanInput => ({ paths: ['src/app.ts'], scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' }, gateTagged: 0, diffLines: 120, history: { fullPasses: 0, last: 'none' }, numbers: none, ...o }); const roles = (p: ReturnType) => p.dispatches.map(d => `${d.role}:${d.name}:${d.effort}`); +const batch = (p: ReturnType) => p.dispatches.find(d => d.role === 'specialist' && d.name === 'batched')?.checklists; describe('classifyPath: agent-read markdown is code', () => { test.each([ @@ -84,12 +85,14 @@ describe('planReview', () => { }); test('code class pass 1: specialists at medium, the native adversarial at full, the outside voice at its own', () => { const p = planReview(base({})); - expect(roles(p)).toEqual(['specialist:testing:medium', 'specialist:maintainability:medium', 'specialist:security:medium', 'specialist:performance:medium', 'specialist:simplification:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(batch(p)).toEqual(['testing', 'security']); + expect(p.skipped).toContain('maintainability, performance, simplification (lean ensemble: read only when forced)'); }); test('gate class forces red team and ignores hit-rate gating', () => { - const p = planReview(base({ gateTagged: 1, gated: ['security'] })); + const p = planReview(base({ gateTagged: 1, gated: ['security', 'state-machines'], scope: { SCOPE_BACKEND: 'true' } })); expect(roles(p)).toContain('red-team:red-team:full'); - expect(roles(p)).toContain('specialist:security:medium'); + expect(batch(p)).toEqual(['testing', 'security', 'state-machines']); }); test('code class honours hit-rate gating and small-diff skip', () => { expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:medium'); @@ -155,7 +158,9 @@ describe('GSTA-131 pass-1 review repairs', () => { expect(scanNumbers({ 'test/fixtures/bench.json': ['{ "ratio": 0.74 }'] }).publishes).toBe(true); }); test('--all-specialists forces every specialist; a code diff past 200 lines or with a CRITICAL adds the Red Team', () => { - expect(planReview(base({ force: 'all' })).dispatches.filter(d => d.role === 'specialist').length).toBe(9); + const all = planReview(base({ force: 'all' })); + expect(all.dispatches.filter(d => d.role === 'specialist').length).toBe(1); + expect([...batch(all)!].sort()).toEqual(['api-contract', 'data-migration', 'design', 'maintainability', 'performance', 'security', 'simplification', 'state-machines', 'testing']); const has = (o: Partial) => roles(planReview(base(o))).some(r => r.startsWith('red-team')); expect(has({ diffLines: 120 })).toBe(false); expect(has({ diffLines: 201 })).toBe(true); @@ -172,12 +177,14 @@ describe('GSTA-131 coverage: Red Team and specialist arms', () => { expect(roles(planReview(base({ diffLines: 20, gateTagged: 1 })))).toContain('red-team:red-team'); }); test('scope arms select their specialist, state machines default on when scope is unknown, and --force all reaches docs class', () => { - const picked = (scope: Record) => roles(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false', ...scope } }))); - expect(picked({ SCOPE_MIGRATIONS: 'true' })).toContain('specialist:data-migration'); - expect(picked({ SCOPE_API: 'true' })).toContain('specialist:api-contract'); - expect(picked({ SCOPE_FRONTEND: 'true' })).toContain('specialist:design'); - expect(roles(planReview(base({ scope: {} })))).toContain('specialist:state-machines'); - expect(roles(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false' } })))).not.toContain('specialist:state-machines'); + const picked = (scope: Record) => batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false', ...scope } }))); + expect(picked({ SCOPE_MIGRATIONS: 'true' })).toContain('data-migration'); + expect(picked({ SCOPE_API: 'true' })).toContain('api-contract'); + expect(picked({ SCOPE_FRONTEND: 'true' })).toEqual(['testing', 'security', 'design']); + expect(batch(planReview(base({ scope: {} })))).toContain('state-machines'); + expect(batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false' } })))).not.toContain('state-machines'); + expect(batch(planReview(base({ force: ['maintainability'] })))).toEqual(['testing', 'security', 'maintainability']); + expect(batch(planReview(base({ gated: ['design'], scope: { SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } })))).toEqual(['testing', 'security']); const docsAll = planReview(base({ paths: ['docs/a.md'], force: 'all' })); expect(docsAll.class).toBe('docs'); expect(docsAll.dispatches.filter(d => d.role === 'specialist').length).toBeGreaterThan(5); @@ -198,7 +205,7 @@ describe('review depth: the matrix', () => { const p = planReview(mixed({ perPath: per({ 'src/app.ts': 48, 'test/a.test.ts': 190, 'docs/a.md': 9 }), diffLines: 247 })); expect([p.depth, p.runtimeLines]).toEqual(['light', 48]); expect(p.dispatches).toEqual([ - { role: 'specialist', name: 'batched', effort: 'full', exclude: [':(exclude)docs/a.md'], checklists: ['testing', 'maintainability', 'security', 'performance', 'red-team'] }, + { role: 'specialist', name: 'batched', effort: 'full', exclude: [':(exclude)docs/a.md'], checklists: ['testing', 'security', 'red-team'] }, { role: 'adversarial-native', name: 'adversarial', effort: 'full', exclude: [], reason: 'backtest inconclusive (GSTA-147 D5)' }, ]); expect(p.skipped.join('; ')).toContain('red-team (light depth'); @@ -207,10 +214,10 @@ describe('review depth: the matrix', () => { test('runtime 51 is standard: thresholds read runtime lines, reviewers read their own hunks', () => { const p = planReview(mixed({ perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 400, 'README.md': 3 }), diffLines: 454 })); expect([p.depth, p.runtimeLines]).toEqual(['standard', 51]); - expect(roles(p)).toEqual(['specialist:testing:medium', 'specialist:maintainability:medium', 'specialist:performance:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(batch(p)).toEqual(['testing', 'security']); const ex = Object.fromEntries(p.dispatches.map(d => [d.name, d.exclude])); - expect(ex.testing).toEqual([':(exclude)README.md']); - expect(ex.maintainability).toEqual([':(exclude)README.md', ':(exclude)test/a.test.ts']); + expect(ex.batched).toEqual([':(exclude)README.md']); expect([ex.adversarial, ex['outside-voice']]).toEqual([[], []]); }); test.each([ @@ -257,7 +264,7 @@ describe('review depth: the matrix', () => { test('a fixed blocker in light raises the next full step to the standard set, Red Team included', () => { const p = planReview(mixed({ perPath: per({ 'src/app.ts': 30 }), history: { fullPasses: 1, last: 'full', fixedBlocking: true } })); expect([p.depth, p.stepDepth]).toEqual(['light', 'standard']); - expect(roles(p)).toEqual(['specialist:testing:medium', 'specialist:maintainability:medium', 'specialist:performance:medium', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(roles(p)).toEqual(['specialist:batched:medium', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); }); test('--depth-floor raises and never lowers', () => { expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'full' })).toBe('full'); @@ -311,7 +318,7 @@ describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () = test('the #19 shape plans one fix-check, the Red Team and the native adversarial pass, not the specialist army', () => { const before = planReview(n19({ history: { fullPasses: 1, last: 'full', fixedBlocking: true } })); expect(before.next.step).toBe('full'); - expect(before.dispatches.length).toBeGreaterThanOrEqual(6); + expect(roles(before)).toEqual(['specialist:batched:medium', 'red-team:red-team:full']); const p = planReview(n19()); expect(p.next.step).toBe('scoped'); expect(p.next.reason).toContain('24 runtime lines'); @@ -457,3 +464,34 @@ describe('GSTA-167 part 4a: data files no code names are docs (classifyDiff only expect(classifyDiff({ paths: ['src/app.ts', 'wiki/old.json'], deleted: ['wiki/old.json'], consumed: [], gateTagged: 0 }).class).toBe('gate'); }); }); + +describe('lean ensemble: one batched specialist at standard and full depth (GSTA-167 part 2)', () => { + const fixture = (name: string): PlanInput => { + const x = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/review-depth', `${name}.json`), 'utf8')); + return { paths: x.paths, deleted: x.deleted, stampOnly: x.stamp_only ?? [], scope: x.scope, gateTagged: x.gate_tagged, diffLines: x.diff_lines, numbers: x.numbers, perPath: x.per_path, registry: x.registry_outputs ? { sha: null, outputs: x.registry_outputs } : undefined, history: { fullPasses: 0, last: 'none' } }; + }; + const specialists = (p: ReturnType) => p.dispatches.filter(d => d.role === 'specialist'); + // Value: protects=the lean ensemble's reader set (testing and security always, the four scoped checklists when scope asks, + // maintainability, performance and simplification only when forced) on captured diffs; fails_when=a plan dispatches more than + // one specialist reader at standard or full, drops testing or security, or the Red Team or voices change; seam=planReview + test('the captured gate fixture plans one batched reader, the Red Team and both voices', () => { + const p = planReview(fixture('code')); + expect([p.class, p.depth]).toEqual(['gate', 'full']); + expect(roles(p)).toEqual(['specialist:batched:medium', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(batch(p)).toEqual(['testing', 'security', 'state-machines']); + expect(batch(planReview({ ...fixture('code'), scope: { ...fixture('code').scope, SCOPE_MIGRATIONS: 'true', SCOPE_API: 'true', SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } }))).toEqual(['testing', 'security', 'data-migration', 'api-contract', 'design']); + }); + test('the captured docs fixture dispatches no reader at standard and one batched reader when floored to full', () => { + expect(specialists(planReview({ ...fixture('docs-only'), floor: 'standard' }))).toEqual([]); + const full = planReview({ ...fixture('docs-only'), floor: 'full' }); + expect(specialists(full).map(d => [d.name, d.effort, d.checklists])).toEqual([['batched', 'medium', ['testing', 'security', 'state-machines']]]); + }); + test('a standard code plan reads one batched reader on its own hunks, and a forced checklist joins it', () => { + const p = planReview(base({ diffLines: 400, scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' } })); + expect(specialists(p).map(d => [d.name, d.checklists])).toEqual([['batched', ['testing', 'security']]]); + expect(roles(p)).toContain('red-team:red-team:full'); + expect(batch(planReview(base({ force: ['performance', 'simplification'] })))).toEqual(['testing', 'security', 'performance', 'simplification']); + expect(specialists(planReview(base({ tiers: false })))[0]!.effort).toBe('host'); + }); +}); + diff --git a/test/review-packets-removed.test.ts b/test/review-packets-removed.test.ts index 459f7b84c..0b5bdb7b0 100644 --- a/test/review-packets-removed.test.ts +++ b/test/review-packets-removed.test.ts @@ -54,10 +54,11 @@ const NO_LOSS_REPLAYS = { '#3638': 'no-loss round 3: main restored the pre-#3621 reviewers (v1.91.331.0)', 'PR-a': "GSTA-167 variant G Arm 4: structural plan identity at the 14 cohort heads and the free exact re-score of saved C, D and A' findings (parts 4a and 4b)", '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)", } as const; type Replay = keyof typeof NO_LOSS_REPLAYS; const REVIEWER_BRIEFS: Record = { - 'ship/sections/review-army.md': { sha: '7ee6cfac4432a83a8a91d77c6adb3574c23a762c7f8b4fb379d5b8a8d7398730', replay: 'G1' }, + 'ship/sections/review-army.md': { sha: 'b7ca291a1cc63750bfb9710a4c12fd971523b9598da2a32ef80fb258eb0eceab', replay: 'L' }, 'ship/sections/adversarial.md': { sha: 'c48aa7717c7438941fa23418367e19a62e07edda7445d0d5a55924e172dd5d33', replay: 'G1' }, 'review/sections/review-army.md': { sha: '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967', replay: '#3638' }, 'review/sections/adversarial.md': { sha: 'e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255', replay: 'G1' }, @@ -74,7 +75,7 @@ const REVIEWER_BRIEFS: Record = { 'review/specialists/state-machines.md': { sha: '597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2', replay: '#3638' }, 'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' }, 'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' }, - 'lib/review-depth.ts': { sha: 'f5599365da06fe6d9829701c3d7e7f716703d87b9b577835e2dc5c938a8f6e6b', replay: 'G1' }, + 'lib/review-depth.ts': { sha: 'd4715a5f534d1470c63da879a3d66bb090a9b4929065d1f06c30acd97ef77bb0', replay: 'L' }, }; describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => { diff --git a/test/review-plan-cli.test.ts b/test/review-plan-cli.test.ts index 557b23acd..f81dc7d7a 100644 --- a/test/review-plan-cli.test.ts +++ b/test/review-plan-cli.test.ts @@ -41,12 +41,12 @@ describe('gstack-review-plan', () => { expect(plan.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'medium')).toBe(true); expect(plan.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full'); expect(plan.audit_effort).toBe('light'); - expect(run(r, []).stdout).toContain('DISPATCH: specialist testing effort=medium'); + expect(run(r, []).stdout).toContain('DISPATCH: specialist batched effort=medium checklists=testing,security,state-machines'); const host = JSON.parse(run(r, ['--json', '--no-tiers']).stdout); expect(host.audit_effort).toBe('host'); expect(host.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'host')).toBe(true); expect(host.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full'); - expect(run(r, ['--no-tiers']).stdout).toContain('DISPATCH: specialist testing effort=host'); + expect(run(r, ['--no-tiers']).stdout).toContain('DISPATCH: specialist batched effort=host checklists=testing,security,state-machines'); }); 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']); @@ -134,25 +134,25 @@ describe('gstack-review-plan: review depth', () => { const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim(); expect(out).toMatch(/^REVIEW_DEPTH: light runtime_lines=10 rules=[0-9a-f]{16} reasons=runtime=10<=50 codes=none$/m); expect(out).toContain(`GATE_SCAN: tagged=0 head=${head}`); - expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,maintainability,security,performance,state-machines,red-team'); + expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,security,state-machines,red-team'); expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'); }); test('DIFF_CMD is shell-quoted per dispatch and excludes what the reviewer does not read; DIFF_EXCLUDE: none when empty', () => { const r = repo({ 'src/a.ts': code(80), 'docs/my notes*.md': '# n\n', 'test/a.test.ts': code(3) }); const out = run(r, []).stdout; - expect(out).toContain("DIFF_CMD: testing git diff -- . ':(exclude,literal)docs/my notes*.md'"); - expect(out).toContain("DIFF_CMD: maintainability git diff -- . ':(exclude,literal)docs/my notes*.md' ':(exclude)test/a.test.ts'"); + expect(out).toContain("DIFF_CMD: batched git diff -- . ':(exclude,literal)docs/my notes*.md'\n"); + expect(out).toContain('SKIP: maintainability, performance, simplification (lean ensemble: read only when forced)'); expect(out).toContain('DIFF_CMD: adversarial git diff \nDIFF_EXCLUDE: adversarial none'); - const cmd = /^DIFF_CMD: maintainability (.*)$/m.exec(out)![1]!.replace('', 'main'); + const cmd = /^DIFF_CMD: batched (.*)$/m.exec(out)![1]!.replace('', 'main'); const diff = spawnSync('bash', ['-c', cmd.replace('git diff main', 'git diff --name-only main')], { cwd: r.dir, encoding: 'utf8' }); - expect(diff.stdout.trim().split('\n')).toEqual(['src/a.ts']); + expect(diff.stdout.trim().split('\n')).toEqual(['src/a.ts', 'test/a.test.ts']); }); test('--json carries depth, runtime_lines, rules, errors, gate_scan and every dispatch\'s exclude array', () => { const r = repo({ 'src/a.ts': code(80), 'README.md': '# y\n' }); const plan = JSON.parse(run(r, ['--json']).stdout); expect(plan).toMatchObject({ depth: 'standard', step_depth: 'standard', runtime_lines: 80, errors: [], codes: [], gate_scan: { tagged: 0 } }); expect(plan.rules).toMatch(/^[0-9a-f]{16}$/); - expect(plan.dispatches.find((d: any) => d.name === 'maintainability').exclude).toEqual([':(exclude)README.md']); + expect(plan.dispatches.find((d: any) => d.name === 'batched').exclude).toEqual([':(exclude)README.md']); expect(plan.dispatches.find((d: any) => d.role === 'outside-voice').exclude).toEqual([]); }); test('--depth-floor full raises a light diff, never lowers a full one, and an invalid floor is a usage error', () => { diff --git a/test/size-ledger/ship/gsta-167-g-c.json b/test/size-ledger/ship/gsta-167-g-c.json new file mode 100644 index 000000000..c7053a4d0 --- /dev/null +++ b/test/size-ledger/ship/gsta-167-g-c.json @@ -0,0 +1,4 @@ +{ + "bytes": -11, + "why": "GSTA-167 variant G part 2: the batched specialist line covers every depth (light adds Red Team)" +}