mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 01:38:10 +01:00
Merge lane B-aux into the autoplan wave: scope, PLAN_REALITY v2 and DRIVER.md wired into the runner
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
decde8e6de
24 files changed
+888
-59
No files matched your search
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Field wave item 13: autoplan/DRIVER.md is the compact, host-neutral entry a
|
||||
* parent agent reads to drive /autoplan through `gstack-autoplan`. It is
|
||||
* generated from autoplan/DRIVER.md.tmpl by resolvers that read the code
|
||||
* (phase order, scope flags, run files, RESULT FORMAT, result codes, the
|
||||
* coordinator contract), stays under 160 lines with no harness vocabulary,
|
||||
* is in the generator's normal, out-dir and dry-run inventories, and is
|
||||
* found from an installed root through its recorded `.source-path`.
|
||||
*/
|
||||
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 { FILES } from '../lib/autoplan-state';
|
||||
import { PHASES, RESULT_FORMAT } from '../lib/autoplan-prompts';
|
||||
import { SCOPE_FLAG_TOKENS } from '../lib/autoplan-scope';
|
||||
import { COORDINATOR_CONTRACT } from '../lib/coordinator-contract';
|
||||
import { parseReply, type GateList } from '../lib/gate-list';
|
||||
import { resolveSourceRoot } from '../lib/source-root';
|
||||
import { AUTOPLAN_DRIVER_COMMANDS, DRIVER_CODES, GATE_REPLY_FORMS } from '../scripts/resolvers/autoplan-driver';
|
||||
import { runGeneration } from '../scripts/gen-skill-docs';
|
||||
import { capyHost } from './helpers/capy-host';
|
||||
import { AUTOPLAN, ROOT, SMALL_PLAN, env } from './helpers/autoplan-run-fixture';
|
||||
|
||||
const DRIVER = path.join(ROOT, 'autoplan', 'DRIVER.md');
|
||||
const doc = fs.readFileSync(DRIVER, 'utf8');
|
||||
const host = capyHost({ verifiedBun: '1.4.2' });
|
||||
afterAll(() => host.cleanup());
|
||||
const hostEnv = () => env({ ...host.env, PATH: host.launchPath, GSTACK_STATE_ROOT: host.stateRoot, GSTACK_HOME: host.stateRoot });
|
||||
|
||||
describe('autoplan/DRIVER.md content comes from the code', () => {
|
||||
test('phase order, scope flags, run files, RESULT FORMAT, result codes and the coordinator contract are rendered', () => {
|
||||
expect(doc).toContain(`Order: ${PHASES.join(' → ')}.`);
|
||||
for (const flag of SCOPE_FLAG_TOKENS) expect(doc).toContain(`\`${flag}\``);
|
||||
for (const file of Object.values(FILES)) expect(doc).toContain(`\`${file}\``);
|
||||
expect(doc).toContain(RESULT_FORMAT('<phase>', '<snapshot sha256>', '<result file>').replace(/^\n---\n/, '').trimEnd());
|
||||
for (const code of DRIVER_CODES) expect(doc).toMatch(new RegExp(`^- \`${code}\`: \\S`, 'm'));
|
||||
expect(doc).toContain(COORDINATOR_CONTRACT);
|
||||
for (const c of AUTOPLAN_DRIVER_COMMANDS) expect(doc).toContain(`- \`${c.cmd}\``);
|
||||
for (const f of GATE_REPLY_FORMS) expect(doc).toContain(`- \`${f.reply}\`: `);
|
||||
});
|
||||
|
||||
test('the plan-specified commands and the amendment interlock are present', () => {
|
||||
for (const token of ['submit --out <dir> --phase <p> --voice native|outside --record', '--attest "<reason>"', '--unavailable|--failed',
|
||||
'revise --out <dir> --plan <plan v2> --incorporates <decision ids>', '--withdraw <decision id>', '[--scaffold]', '`--no-dx`', '`all as-is`', 'PLAN_AMEND_PENDING', 'APPLY:']) {
|
||||
expect(doc).toContain(token);
|
||||
}
|
||||
});
|
||||
|
||||
test('at most 160 lines and no harness vocabulary', () => {
|
||||
expect(doc.split('\n').length).toBeLessThanOrEqual(160);
|
||||
for (const term of ['PreToolUse', 'PostToolUse', 'SessionStart', 'AskUserQuestion', 'Skill tool']) expect(doc).not.toContain(term);
|
||||
});
|
||||
|
||||
test('every flag it names is in gstack-autoplan --help, and the gate forms parse', () => {
|
||||
const help = spawnSync('bun', [AUTOPLAN, '--help'], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 30_000 }).stdout;
|
||||
const commands = AUTOPLAN_DRIVER_COMMANDS.map(c => c.cmd).join(' ');
|
||||
for (const flag of new Set(commands.match(/--[a-z][a-z-]*/g)!)) {
|
||||
expect(help.includes(flag), flag).toBe(true);
|
||||
}
|
||||
const gate: GateList = { schema_version: 1, run: 'r', gate_rev: 1, items: ['d3', 'uc1'].map(id => ({ id, title: id, recommended: 'a', kind: 'auto' as const, options: [{ key: 'a', text: 'keep' }, { key: 'b', text: 'change' }] })) };
|
||||
for (const f of GATE_REPLY_FORMS) expect(parseReply(f.reply, gate, 1).valid, f.reply).toBe(true);
|
||||
expect(parseReply('yes', gate, 1).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('where a parent finds it', () => {
|
||||
test('gstack-autoplan --help prints DRIVER: <root>/autoplan/DRIVER.md', () => {
|
||||
const r = spawnSync('bun', [AUTOPLAN, '--help'], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 30_000 });
|
||||
expect(r.stdout.split('\n')).toContain(`DRIVER: ${DRIVER}`);
|
||||
});
|
||||
|
||||
test('the first next prints the DRIVER line once', () => {
|
||||
const dir = path.join(host.base, 'first-next');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'plan.md'), SMALL_PLAN);
|
||||
const out = path.join(dir, 'run');
|
||||
const first = spawnSync('bun', [AUTOPLAN, 'next', '--out', out, '--plan', path.join(dir, 'plan.md'), '--no-ui'], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 120_000 });
|
||||
expect(first.status, first.stderr).toBe(0);
|
||||
expect(first.stdout.split('\n').filter(l => l.startsWith('DRIVER: '))).toEqual([`DRIVER: ${DRIVER}`]);
|
||||
const again = spawnSync('bun', [AUTOPLAN, 'next', '--out', out], { cwd: ROOT, env: hostEnv(), encoding: 'utf8', timeout: 120_000 });
|
||||
expect(again.stdout).not.toContain('DRIVER: ');
|
||||
});
|
||||
|
||||
test('an installed copy root resolves DRIVER.md through its recorded .source-path; a stale record falls back to the root', () => {
|
||||
const root = path.join(host.base, 'install-root');
|
||||
fs.mkdirSync(path.join(root, 'bin'), { recursive: true });
|
||||
fs.cpSync(path.join(ROOT, 'lib'), path.join(root, 'lib'), { recursive: true });
|
||||
fs.copyFileSync(AUTOPLAN, path.join(root, 'bin', 'gstack-autoplan'));
|
||||
fs.copyFileSync(path.join(ROOT, 'bin', 'gstack-autoplan-snapshot.ts'), path.join(root, 'bin', 'gstack-autoplan-snapshot.ts'));
|
||||
fs.writeFileSync(path.join(root, '.source-path'), `${ROOT}\n`);
|
||||
const help = spawnSync('bun', [path.join(root, 'bin', 'gstack-autoplan'), '--help'], { cwd: host.base, env: hostEnv(), encoding: 'utf8', timeout: 30_000 });
|
||||
expect(help.stdout.split('\n')).toContain(`DRIVER: ${DRIVER}`);
|
||||
expect(resolveSourceRoot(root)).toBe(ROOT);
|
||||
fs.writeFileSync(path.join(root, '.source-path'), `${path.join(host.base, 'gone')}\n`);
|
||||
expect(resolveSourceRoot(root)).toBe(root);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generator inventory', () => {
|
||||
test('normal, out-dir and dry-run generation all carry autoplan/DRIVER.md, and the committed file is fresh', async () => {
|
||||
const outDir = path.join(host.base, 'render');
|
||||
const written = await runGeneration({ host: 'claude', outputRoot: outDir, contentLinkRoot: null });
|
||||
expect(written.artifacts).toContainEqual({ relativePath: 'autoplan/DRIVER.md', kind: 'asset', host: 'claude' });
|
||||
expect(fs.readFileSync(path.join(outDir, 'autoplan', 'DRIVER.md'), 'utf8')).toBe(doc);
|
||||
const dry = await runGeneration({ host: 'claude', dryRun: true });
|
||||
expect(dry.artifacts).toContainEqual({ relativePath: 'autoplan/DRIVER.md', kind: 'asset', host: 'claude' });
|
||||
expect(dry.diagnostics.filter(d => d.relativePath === 'autoplan/DRIVER.md')).toEqual([]);
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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[^)]*\) 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`');
|
||||
});
|
||||
});
|
||||
+4
-1
@@ -107,7 +107,10 @@
|
||||
{ "file": "lib/pregate/checks/secrets.ts", "movedFrom": "(new: pre-gate check, C8)" },
|
||||
{ "file": "lib/pregate/checks/strays.ts", "movedFrom": "(new: pre-gate check, C8)" },
|
||||
{ "file": "lib/pregate/checks/literals.ts", "movedFrom": "(new: pre-gate check, C8)" },
|
||||
{ "file": "lib/pregate/checks/lanes.ts", "movedFrom": "(new: pre-gate check with the pipefail runner and requires-remote obligations, C8)" }
|
||||
{ "file": "lib/pregate/checks/lanes.ts", "movedFrom": "(new: pre-gate check with the pipefail runner and requires-remote obligations, C8)" },
|
||||
{ "file": "lib/autoplan-scope.ts", "movedFrom": "(new: UI evidence from rendered surfaces and the plan scope marker, shared by the runner and the snapshot tool, field wave B2c)" },
|
||||
{ "file": "lib/source-root.ts", "movedFrom": "(new: the source checkout behind an install root, from its recorded .source-path, field wave B2e)" },
|
||||
{ "file": "scripts/resolvers/autoplan-driver.ts", "movedFrom": "(new: renders autoplan/DRIVER.md from the runner's constants, field wave B2e)" }
|
||||
],
|
||||
"residualFiles": [
|
||||
{ "file": "scripts/test-free-shards.ts", "maxLines": 2361, "reason": "free lane policy after the shard engine took spawn, sandbox, logs, seeds and flags" },
|
||||
|
||||
@@ -46,6 +46,7 @@ describe('generator artifact and dry-run contract', () => {
|
||||
expect(generated.artifacts.filter(a => a.kind === 'asset').sort((a, b) => a.relativePath.localeCompare(b.relativePath))).toEqual([
|
||||
{ relativePath: 'review/design-checklist.md', kind: 'asset', host: 'claude' },
|
||||
{ relativePath: 'lib/dom-dump.js', kind: 'asset', host: 'claude' },
|
||||
{ relativePath: 'autoplan/DRIVER.md', kind: 'asset', host: 'claude' },
|
||||
...ALL_HOST_NAMES.filter(host => includesSkill(getHostConfig(host), 'qa')).map(host => ({
|
||||
relativePath: host === 'claude' ? 'qa/templates/functional-report-template.md'
|
||||
: `${getHostConfig(host).hostSubdir}/skills/gstack-qa/templates/functional-report-template.md`,
|
||||
|
||||
@@ -13,7 +13,8 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { REALITY_ROWS, REALITY_SCOPES, TIER1_ROW_IDS, checkRealityRows, parseRealityLines, parseScopes, renderIncidentTable, renderRows, requiredRows } from '../lib/plan-reality';
|
||||
import { REALITY_ROWS, REALITY_SCOPES, TIER1_ROW_IDS, checkPlanReality, checkRealityRows, parseRealityLines, parseScopes, renderIncidentTable, renderRows, requiredRows } from '../lib/plan-reality';
|
||||
import { extractHeading } from '../lib/autoplan-prompts';
|
||||
import { RESULT_CODES } from '../lib/result-codes';
|
||||
import { generatePlanRealityRows } from '../scripts/resolvers/plan-reality';
|
||||
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||
@@ -175,6 +176,8 @@ describe('gstack-plan-reality bin', () => {
|
||||
const parsed = JSON.parse(unreceipted.out);
|
||||
expect(parsed.unreceipted).toEqual(['premise-table']);
|
||||
expect(unreceipted.err).toContain('(REALITY_RECEIPT_MISSING)');
|
||||
const unreceiptedText = run(['check', '--phase', 'ceo', '-'], `${partial}\n${FIXTURE.rows['premise-table']!.decoy}`);
|
||||
expect(unreceiptedText.out).toMatch(/^PLAN_REALITY: phase=ceo .*\bunreceipted=premise-table\b.* verdict=repair$/m);
|
||||
expect(run(['check', '--phase', 'ceo', path.join(tmp, 'absent.md')]).code).toBe(EXIT.fail);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
@@ -233,3 +236,78 @@ describe('rendered templates carry the contract', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('field wave item 12: a receipt per measured number, the rule text for a rule claim', () => {
|
||||
const others = (id: string) => Object.entries(FIXTURE.rows).filter(([k]) => k !== id).map(([, r]) => r.positive).join('\n');
|
||||
const FIVE = 'REALITY: numbers pass p50 120ms, p95 340ms, 13% faster, 28k tokens, cost $41 [measured: test/bench.ts:10]';
|
||||
|
||||
test('five measured numbers sharing one receipt leave four unreceipted, each printed with its clause', () => {
|
||||
const r = checkRealityRows(`${others('numbers')}\n${FIVE}`, { phase: 'eng', sessionKind: 'unattended' });
|
||||
expect(r.missing).toEqual([]);
|
||||
expect(r.unreceipted).toEqual([]);
|
||||
expect(r.unreceipted_numbers).toEqual(['p50 120ms', 'p95 340ms', '13% faster', '28k tokens']);
|
||||
expect(r.numberLines).toEqual(['NUMBER_UNRECEIPTED: p50 120ms', 'NUMBER_UNRECEIPTED: p95 340ms', 'NUMBER_UNRECEIPTED: 13% faster', 'NUMBER_UNRECEIPTED: 28k tokens']);
|
||||
expect(r.verdict).toBe('incomplete');
|
||||
expect(r.line).toContain(' unreceipted_numbers=4 ');
|
||||
expect(checkRealityRows(`${others('numbers')}\n${FIVE}`, { phase: 'eng' }).verdict).toBe('repair');
|
||||
});
|
||||
|
||||
test('each clause with its own receipt (file:line, script path or a command in backticks) passes; unmeasured counts, versions and ids need none', () => {
|
||||
const own = 'REALITY: numbers pass p95 340ms lib/x.ts:4, 13% faster `bun run bench --json`, 28k tokens scripts/count-tokens.ts, recall 0.82 vs 0.79 on gstack 1.91.95.0 per F2 and #3160 [estimated]';
|
||||
const r = checkRealityRows(`${others('numbers')}\n${own}`, { phase: 'eng' });
|
||||
expect(r.unreceipted_numbers).toEqual([]);
|
||||
expect(r.verdict).toBe('complete');
|
||||
});
|
||||
|
||||
test('a rule claim without the rule text is unquoted; the quoted rule or a file:line link to it passes', () => {
|
||||
const bare = 'REALITY: binding-decisions pass the decision rule was written first 91bbd9e';
|
||||
const r = checkRealityRows(`${others('binding-decisions')}\n${bare}`, { phase: 'eng', sessionKind: 'unattended' });
|
||||
expect(r.unreceipted).toEqual([]);
|
||||
expect(r.unquoted).toEqual([bare]);
|
||||
expect(r.line).toContain(' unquoted=1 ');
|
||||
expect(r.verdict).toBe('incomplete');
|
||||
const quoted = 'REALITY: binding-decisions pass the decision rule was written first: "stop if recall drops below the base on any bucket" 91bbd9e';
|
||||
expect(checkRealityRows(`${others('binding-decisions')}\n${quoted}`, { phase: 'eng' }).verdict).toBe('complete');
|
||||
const linked = 'REALITY: binding-decisions pass the decision rule was written first, see docs/plan.md:4';
|
||||
expect(checkRealityRows(`${others('binding-decisions')}\n${linked}`, { phase: 'eng' }).unquoted).toEqual([]);
|
||||
});
|
||||
|
||||
test('receipts on dot paths count; a sha receipt needs a hex letter or a commit/backtick context', () => {
|
||||
expect(parseRealityLines('REALITY: pr-count pass one PR .gstack/ship-policy.json:1')[0]!.receipt).toBe('.gstack/ship-policy.json:1');
|
||||
expect(parseRealityLines('REALITY: pr-count pass ci lane .github/workflows/ci.yml:12-20')[0]!.receipt).toBe('.github/workflows/ci.yml:12-20');
|
||||
expect(parseRealityLines('REALITY: numbers pass run took 20261012 cycles')[0]!.receipt).toBeNull();
|
||||
expect(parseRealityLines('REALITY: already-done pass base clean at commit 1234567')[0]!.receipt).toBe('commit 1234567');
|
||||
expect(parseRealityLines('REALITY: already-done pass base clean at `7654321` today')[0]!.receipt).toBe('`7654321`');
|
||||
});
|
||||
|
||||
test('bin: NUMBER_UNRECEIPTED lines follow the PLAN_REALITY line and the verdict exits 1 with a fix', () => {
|
||||
const r = run(['check', '--phase', 'eng', '-', '--session-kind', 'unattended'], `${others('numbers')}\n${FIVE}`);
|
||||
expect(r.code).toBe(EXIT.fail);
|
||||
expect(r.out.split('\n').filter(Boolean)).toEqual([
|
||||
expect.stringMatching(/^PLAN_REALITY: phase=eng rows=7\/7 missing=none unreceipted_numbers=4 .*verdict=incomplete$/),
|
||||
'NUMBER_UNRECEIPTED: p50 120ms', 'NUMBER_UNRECEIPTED: p95 340ms', 'NUMBER_UNRECEIPTED: 13% faster', 'NUMBER_UNRECEIPTED: 28k tokens',
|
||||
]);
|
||||
expect(r.err).toContain('(REALITY_RECEIPT_MISSING)');
|
||||
expect(r.err).toContain('fix:');
|
||||
});
|
||||
|
||||
test('checkPlanReality(planText): plan prose gets the same number and rule checks, pure and session-kind mapped', () => {
|
||||
const plan = 'Cold install measured 32.5 s; the warm rerun takes 13 s (bin/gstack-capy-install:91).\n```\nlatency 900 ms in a fenced example\n```\nThe decision rule was written first.\nThe preregistered rule is below.\n> stop when recall drops below the base\n';
|
||||
const r = checkPlanReality(plan, { sessionKind: 'unattended' });
|
||||
expect(r.unreceipted_numbers).toEqual(['Cold install measured 32.5 s']);
|
||||
expect(r.unquoted).toEqual(['The decision rule was written first.']);
|
||||
expect(r.verdict).toBe('incomplete');
|
||||
expect(r.line).toBe('PLAN_REALITY: scope=plan unreceipted_numbers=1 unquoted=1 verdict=incomplete');
|
||||
expect(r.numberLines).toEqual(['NUMBER_UNRECEIPTED: Cold install measured 32.5 s']);
|
||||
expect(checkPlanReality('Add a retry with a 30 s cap (lib/worker.ts:12).\n').line).toBe('PLAN_REALITY: scope=plan unreceipted_numbers=0 unquoted=0 verdict=complete');
|
||||
expect(checkPlanReality(plan).verdict).toBe('repair');
|
||||
});
|
||||
|
||||
test('false-positive bound: the field-wave plan itself raises only a small, pinned count', () => {
|
||||
const text = fs.readFileSync(path.join(ROOT, 'docs', 'designs', 'GSTACK_FIELD_WAVE_2026_10_12.md'), 'utf8');
|
||||
const r = checkPlanReality(extractHeading(text, 'Implementation plan')!);
|
||||
expect(r.unquoted).toEqual([]);
|
||||
expect(r.unreceipted_numbers.length).toBeGreaterThan(0);
|
||||
expect(r.unreceipted_numbers.length).toBeLessThanOrEqual(6);
|
||||
});
|
||||
});
|
||||
@@ -89,20 +89,23 @@ describe('review checklists (plan B4)', () => {
|
||||
});
|
||||
|
||||
test('the runner loads the checklist in light mode, keeps always sections, and falls back to the deep section when scope is uncertain', () => {
|
||||
const detectedNoUi: Scope = { ui: false, dx: true, ui_matches: 0, dx_matches: 4, developer_tool: false, agent_primary: false, source: 'detected' };
|
||||
const detectedNoUi: Scope = { ui: false, dx: true, ui_matches: 0, dx_matches: 4, developer_tool: false, agent_primary: false, sources: { ui: 'detected', dx: 'detected' } };
|
||||
const light = sectionLoadFor('ceo', detectedNoUi, 'light');
|
||||
expect(light.checklist).toContain('## Checklist (runner profile)');
|
||||
expect(light.loaded).toEqual(['review-sections']);
|
||||
expect(light.skipped).toEqual([]);
|
||||
expect(light.line).toBe('Skipped sections: none (scope: ui=no,dx=yes,source=detected)');
|
||||
expect(light.line).toBe('Skipped sections: none (scope: ui=no,dx=yes,source=ui:detected,dx:detected)');
|
||||
const full = sectionLoadFor('ceo', detectedNoUi, 'full');
|
||||
expect(full.checklist).toBeNull();
|
||||
expect(full.loaded).toEqual(['review-sections']);
|
||||
// a scoped section is skipped only when its scope is off by flags or detected with zero matches; weak detection keeps it
|
||||
expect(sectionScopeOff('ui', detectedNoUi)).toBe(true);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, ui_matches: 1 })).toBe(false);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, source: 'flags' })).toBe(true);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, source: 'flags', ui: true })).toBe(false);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, sources: { ui: 'flag', dx: 'detected' } })).toBe(true);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, sources: { ui: 'flag', dx: 'detected' }, ui: true })).toBe(false);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, ui_matches: 1, sources: { ui: 'marker', dx: 'detected' } })).toBe(true);
|
||||
expect(sectionScopeOff('dx', { ...detectedNoUi, dx: false, dx_matches: 2, sources: { ui: 'detected', dx: 'flag' } })).toBe(true);
|
||||
expect(sectionScopeOff('ui', { ...detectedNoUi, sources: undefined as never, source: 'flags', ui_matches: 1 })).toBe(true);
|
||||
expect(sectionScopeOff('dx', { ...detectedNoUi, dx: false, dx_matches: 0 })).toBe(true);
|
||||
expect(sectionScopeOff('dx', { ...detectedNoUi, dx: false, dx_matches: 2 })).toBe(false);
|
||||
for (const s of ['always', 'db', 'perf', 'security', 'incident'] as const) expect(sectionScopeOff(s, { ...detectedNoUi, ui: false, dx: false, dx_matches: 0 })).toBe(false);
|
||||
|
||||
Reference in new issue
Block a user