mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* wip(GSTA-131): review depth, fix-check transitions, dispatch counters, carry pregate, autoplan round policy Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * wip(GSTA-131): reviewers stay at full effort; audits light Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ship): Step 9 dispatches what gstack-review-plan plans; fix-check after below-floor fixes; cap two full passes plus one fix-check Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: review-dispatch-compare has a free lane; review-depth fixtures registered free; resumed max_rounds case off the old default Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(GSTA-131): ship review pass-1 repairs: fix-check binds under its own skill, Step 11.5 accepts a fix-check or a planned adversarial skip, delta carry with a pregate, carry --pregate pinned to the checkout, quiet-after-cap revise regates, skill-dir markdown and rule docs classed code, gate signals from tests class up, plan fails closed on git errors, per-class compare ratios Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(GSTA-131): ship review pass-2 repairs: Step 11 follows the last full-pass plan, Important Rules restored, ship goldens regenerated, true per-class medians, review-policy transitions and once-per-invocation voices, docs/** assets stay docs-class, CLI and fix-check binding tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GSTA-131 review depth, fix-check, delta /ship and autoplan rounds Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: ship-policy cap and counters, review-policy /review cycle cap, unattended passes (docs audit) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): 400-word findings cap fits the /review size pins (five host pins lowered by 2); CHANGELOG max_rounds wording Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): gate integrity names the size-ledger entry and the lowered /review pins Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the round-budget case asserts the default through ROUNDS output only (pregate literals heuristic) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: curate ship-receipt-carry-pregate out of the Windows lane like its --pregate siblings Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): end the Windows curation sentence instead of a mid-sentence colon Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship): one decision each for the review-loop exits, the planned adversarial skip and Step 16's TODOS rerun (screen judge repair) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship): fix-check repairs: pin the renamed Step 11 heading, keep the below-floor exit at the cap, name the plan run a repeat reuses, print the native skip on outside-only plans Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
133 lines
8.2 KiB
TypeScript
133 lines
8.2 KiB
TypeScript
/**
|
||
* Field wave item 11: /autoplan scope keys on what the Implementation plan
|
||
* changes. UI needs a rendered surface (a rendered file path, or a UI term
|
||
* next to a visual noun); bare UI words and Review-record prose never add the
|
||
* Design phase. Provenance is per axis (flag, marker, detected), `--no-dx`
|
||
* turns DX off, a first-line scope marker in the plan is read (never
|
||
* written), and the interactive path reads the same detector through
|
||
* `gstack-autoplan-snapshot.ts scope`. Runner-level cases run on the
|
||
* simulated Capy host (test/helpers/capy-host.ts).
|
||
*/
|
||
import { afterAll, describe, expect, test } from 'bun:test';
|
||
import { spawnSync } from 'node:child_process';
|
||
import * as fs from 'node:fs';
|
||
import * as path from 'node:path';
|
||
import { detectScope, detectUiScope, phaseOrder, sectionScopeOff } from '../lib/autoplan-prompts';
|
||
import { capyHost } from './helpers/capy-host';
|
||
import { AUTOPLAN, ROOT, env, readRun } from './helpers/autoplan-run-fixture';
|
||
|
||
const host = capyHost({ verifiedBun: '1.4.2' });
|
||
afterAll(() => host.cleanup());
|
||
let n = 0;
|
||
const planFile = (body: string, record = '', firstLine = '') => {
|
||
const dir = path.join(host.base, `plan-${++n}`);
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
const file = path.join(dir, 'plan.md');
|
||
fs.writeFileSync(file, `${firstLine ? `${firstLine}\n` : ''}# Plan ${n}\n\n## Implementation plan\n\n${body}\n\n## Review record\n\n${record}\n`);
|
||
return file;
|
||
};
|
||
const hostEnv = () => env({ ...host.env, PATH: host.launchPath, GSTACK_SESSION_KIND: 'unattended', GSTACK_STATE_ROOT: host.stateRoot, GSTACK_HOME: host.stateRoot });
|
||
const next = (args: string[]) => spawnSync('bun', [AUTOPLAN, 'next', ...args], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 120_000 });
|
||
const snapshotScope = (file: string) => {
|
||
const r = spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-autoplan-snapshot.ts'), 'scope', file], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 60_000 });
|
||
expect(r.status, r.stderr).toBe(0);
|
||
return JSON.parse(r.stdout);
|
||
};
|
||
|
||
const CLI_PROSE = 'The sync component reads frontmatter; a malformed form is held. The doctor screen output (terminal) lists each held file with a status line.';
|
||
const DX_PROSE = 'Add a CLI flag --since to the sync command and document the flag in the command help.';
|
||
|
||
describe('UI evidence is a rendered surface in the Implementation plan', () => {
|
||
test('non-visual CLI prose with UI words is weak evidence: no Design phase, sections still load', () => {
|
||
const r = detectUiScope(CLI_PROSE);
|
||
expect(r.uiRequired).toBe(false);
|
||
expect(r.uiMatches).toBe(3);
|
||
expect(r.uiEvidence).toEqual({ terms: [{ term: 'component', count: 1 }, { term: 'screen', count: 1 }, { term: 'form', count: 1 }], visual: [], files: [] });
|
||
const scope = detectScope(planFile(CLI_PROSE), ROOT, {});
|
||
expect(scope.ui).toBe(false);
|
||
expect(phaseOrder(scope)).not.toContain('design');
|
||
expect(sectionScopeOff('ui', scope)).toBe(false);
|
||
});
|
||
|
||
test('UI words only in the Review record give ui=false (Review record prose is not scanned)', () => {
|
||
const scope = detectScope(planFile('Retry the webhook worker with backoff in lib/worker.ts.', 'The Settings page layout gets a new button and a modal; the sidebar nav moves.'), ROOT, {});
|
||
expect(scope.ui).toBe(false);
|
||
expect(scope.ui_matches).toBe(0);
|
||
});
|
||
|
||
test('a rendered file path, quoted or backticked, gives ui=true and is printed as evidence', () => {
|
||
expect(detectUiScope('Edit src/components/Settings.tsx to add the toggle.').uiEvidence.files).toEqual(['src/components/Settings.tsx']);
|
||
expect(detectUiScope('Render "app/views/home/index.html.erb" with the new partial.').uiRequired).toBe(true);
|
||
expect(detectUiScope('Move the colors into `web/theme.scss`.').uiEvidence.files).toEqual(['web/theme.scss']);
|
||
expect(detectUiScope('Touch lib/pages.ts and docs/components.md only.').uiRequired).toBe(false);
|
||
expect(detectScope(planFile('Edit src/components/Settings.tsx to add a retry toggle.'), ROOT, {}).ui).toBe(true);
|
||
});
|
||
|
||
test('a UI term next to a visual noun is UI; the same term far from one is not', () => {
|
||
const near = detectUiScope('Add a retry button to the settings page.');
|
||
expect(near.uiRequired).toBe(true);
|
||
expect(near.uiEvidence.visual).toEqual(['button~page']);
|
||
expect(detectUiScope('The status layout of the JSON report is stable.').uiRequired).toBe(false);
|
||
expect(detectUiScope('Add a button. Write a long unrelated paragraph about the queue and its retry budget and the man page.').uiRequired).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('per-axis provenance, --no-dx and the plan marker', () => {
|
||
test('--no-ui alone labels a detected DX as detected, not flag-forced', () => {
|
||
const scope = detectScope(planFile(DX_PROSE), ROOT, { ui: false });
|
||
expect(scope.dx).toBe(true);
|
||
expect(scope.sources).toEqual({ ui: 'flag', dx: 'detected' });
|
||
});
|
||
|
||
test('a first-line marker with no CLI flags sets scope from the marker, and the source plan is untouched', () => {
|
||
const file = planFile('Edit src/components/Settings.tsx to add a retry toggle.', '', '<!-- gstack-autoplan-scope: --no-ui --developer-tool -->');
|
||
const before = fs.readFileSync(file);
|
||
const scope = detectScope(file, ROOT, {});
|
||
expect(scope).toMatchObject({ ui: false, dx: true, developer_tool: true, sources: { ui: 'marker', dx: 'marker' }, pin: null });
|
||
expect(fs.readFileSync(file).equals(before)).toBe(true);
|
||
expect(() => detectScope(planFile('x', '', '<!-- gstack-autoplan-scope: --bogus -->'), ROOT, {})).toThrow(/unknown scope flag --bogus/);
|
||
});
|
||
|
||
test('runner: CLI flags print SCOPE evidence and a SCOPE_PIN hint, never write the plan, and persist in run.json', () => {
|
||
const file = planFile(`${DX_PROSE} ${CLI_PROSE}`);
|
||
const before = fs.readFileSync(file);
|
||
const out = path.join(host.base, 'run-pin');
|
||
const r = next(['--out', out, '--plan', file, '--no-ui', '--developer-tool']);
|
||
expect(r.status, r.stderr).toBe(0);
|
||
expect(r.stdout).toMatch(/^SCOPE: ui=no \(terms: component×1, screen×1, form×1; visual: none; files: none\) dx=yes \(detected: [^)]*flag×2[^)]*\) docs_only=no \(detected\) source=ui:flag,dx:flag$/m);
|
||
expect(r.stdout).toContain(`SCOPE_PIN: add <!-- gstack-autoplan-scope: --no-ui --developer-tool --> as the first line of ${file} to keep these flags`);
|
||
expect(fs.readFileSync(file).equals(before)).toBe(true);
|
||
expect(readRun(out).scope).toMatchObject({ cli_flags: ['--no-ui', '--developer-tool'], marker: null, sources: { ui: 'flag', dx: 'flag' } });
|
||
expect(r.stdout).toContain('source=ui:flag,dx:flag; checklist=');
|
||
});
|
||
|
||
test('runner: a marker that already says the CLI flags prints no SCOPE_PIN', () => {
|
||
const file = planFile(DX_PROSE, '', '<!-- gstack-autoplan-scope: --developer-tool --no-ui -->');
|
||
const r = next(['--out', path.join(host.base, 'run-pinned'), '--plan', file, '--no-ui', '--developer-tool']);
|
||
expect(r.status, r.stderr).toBe(0);
|
||
expect(r.stdout).toMatch(/^SCOPE: .* source=ui:flag,dx:flag$/m);
|
||
expect(r.stdout).not.toContain('SCOPE_PIN:');
|
||
});
|
||
|
||
test('runner: --no-dx removes the DX phase from a plan whose terms detect it', () => {
|
||
const r = next(['--out', path.join(host.base, 'run-nodx'), '--plan', planFile(DX_PROSE), '--no-ui', '--no-dx']);
|
||
expect(r.status, r.stderr).toBe(0);
|
||
expect(r.stdout).toMatch(/^PHASE: ceo snapshot=[0-9a-f]{64} order=ceo,eng$/m);
|
||
expect(r.stdout).toMatch(/^SCOPE: ui=no .* dx=no .* source=ui:flag,dx:flag$/m);
|
||
});
|
||
});
|
||
|
||
describe('interactive parity: one detector', () => {
|
||
test('snapshot scope returns uiRequired with its evidence', () => {
|
||
expect(snapshotScope(planFile('Edit src/components/Settings.tsx.'))).toMatchObject({ uiRequired: true, uiEvidence: { files: ['src/components/Settings.tsx'] } });
|
||
expect(snapshotScope(planFile(CLI_PROSE, 'The Settings page layout gets a new button.'))).toMatchObject({ uiRequired: false, uiMatches: 3 });
|
||
});
|
||
|
||
test('autoplan Step 2 takes UI scope from the returned uiRequired instead of a prose grep', () => {
|
||
const step2 = fs.readFileSync(path.join(ROOT, 'autoplan', 'SKILL.md.tmpl'), 'utf8').split('### Step 2: Read context')[1]!.split('### Step 3:')[0]!;
|
||
expect(step2).toContain('`uiRequired`');
|
||
expect(step2).not.toMatch(/grep the plan/i);
|
||
expect(fs.readFileSync(path.join(ROOT, 'autoplan', 'SKILL.md'), 'utf8')).toContain('`uiRequired`');
|
||
});
|
||
});
|