chore(cleanup): R-b R3 — profiles, phase signals and the effort dialog removed, one fixed path per command
The four profiles, the resolver, the profile validator and docs/profiles.md go; /trekbrief Phase 3.5 and the brief's phase_signals checks go (an older brief's key is ignored); MANIFEST_PROFILE_DRIFT and profile_used go. Every Profile, Composition rule, Sequencing gate and High-effort section leaves the commands. Decision 209: /trekresearch runs the full swarm with contrarian-researcher as wave 2 every run; /trekreview and the coordinator run all four passes with no high-effort normalization. Voyage agents are spawned with no model (their own model: opus applies); the two built-in spawns pass model "opus". --profile stays in the parser as a retired flag that swallows its value. §Model & effort moves to docs/architecture.md; the sequencing-gate eval case goes. Files removed: 44 (43 from the R3 list + tests/commands/trekreview.test.mjs, whose every test pinned removed text). Files edited: 43. Tests: 2467 (2466 pass / 0 fail / 1 skipped) before, 2309 (2309 pass / 0 fail / 0 skipped) after. Durable check: all three groups green, none pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
32621dcb63
commit
2a35d067d9
87 changed files with 225 additions and 4967 deletions
|
|
@ -1,118 +1,22 @@
|
|||
// tests/commands/trekbrief.test.mjs
|
||||
// v5.1 prose-pin tests + v5.1.1 runtime SC1 tests.
|
||||
//
|
||||
// Pattern D prose-pins kept as doc-anchors for the .md file. Runtime tests
|
||||
// added per finding 350853 (BLOCKER SC1) + a7f4f95a (MAJOR Plan Step 5 drift).
|
||||
//
|
||||
// SC1 re-interpretation (per plan Step 10 amendment): "asserts on 4
|
||||
// AskUserQuestion calls" → "asserts resolvePhaseSignal returns non-null for
|
||||
// all 4 entries in PHASE_SIGNAL_PHASES when applied to a brief with a
|
||||
// committed phase_signals block." See brief amendment for full rationale.
|
||||
// Pattern D prose-pins kept as doc-anchors for commands/trekbrief.md.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
import { validateBriefContent, PHASE_SIGNAL_PHASES, EFFORT_LEVELS } from '../../lib/validators/brief-validator.mjs';
|
||||
import { BASE_ALLOWED_MODELS } from '../../lib/validators/profile-validator.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md');
|
||||
const REVIEWER_FILE = join(ROOT, 'agents', 'brief-reviewer.md');
|
||||
const TEMPLATE_FILE = join(ROOT, 'templates', 'trekbrief-template.md');
|
||||
const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name);
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
function readFixture(name) {
|
||||
return readFileSync(FIXTURE(name), 'utf8');
|
||||
}
|
||||
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
return doc.parsed && doc.parsed.frontmatter;
|
||||
}
|
||||
|
||||
// --- Pattern D prose-pins (doc-anchors) ---
|
||||
|
||||
test('trekbrief — Phase 3.5 heading is present', () => {
|
||||
const text = read();
|
||||
assert.match(text, /^## Phase 3\.5 — Per-phase effort dialog$/m,
|
||||
'Phase 3.5 heading missing from commands/trekbrief.md');
|
||||
});
|
||||
|
||||
test('trekbrief — Phase 3.5 references all 4 downstream phases', () => {
|
||||
const text = read();
|
||||
const startIdx = text.indexOf('## Phase 3.5');
|
||||
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
|
||||
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
|
||||
for (const phase of ['research', 'plan', 'execute', 'review']) {
|
||||
assert.ok(section.includes(phase),
|
||||
`Phase 3.5 missing reference to "${phase}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('phase_signals_partial'),
|
||||
'phase_signals_partial not mentioned in /trekbrief command prose');
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC1 tests ---
|
||||
|
||||
test('trekbrief — SC1: resolvePhaseSignal returns non-null for all 4 phases on committed brief (brief-effort-low)', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
||||
for (const phase of PHASE_SIGNAL_PHASES) {
|
||||
const r = resolvePhaseSignal(fm, phase);
|
||||
assert.ok(r && typeof r === 'object',
|
||||
`phase=${phase}: resolver must return non-null for committed brief; got ${JSON.stringify(r)}`);
|
||||
assert.ok(typeof r.effort === 'string',
|
||||
`phase=${phase}: resolver result must include effort`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trekbrief — SC1: each of 4 phases has both effort AND model on full-signals fixture', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-with-phase-signals.md'));
|
||||
for (const phase of PHASE_SIGNAL_PHASES) {
|
||||
const r = resolvePhaseSignal(fm, phase);
|
||||
assert.ok(r && typeof r === 'object', `phase=${phase}: must resolve`);
|
||||
assert.ok(EFFORT_LEVELS.includes(r.effort),
|
||||
`phase=${phase}: effort "${r.effort}" not in EFFORT_LEVELS`);
|
||||
if ('model' in r) {
|
||||
assert.ok(BASE_ALLOWED_MODELS.includes(r.model),
|
||||
`phase=${phase}: model "${r.model}" not in [${BASE_ALLOWED_MODELS.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('trekbrief — SC1: missing phase_signals + brief_version 2.1 triggers BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`gate must fire; errors=${JSON.stringify(r.errors)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// --- v5.9 — fable tier option in the Phase 3.5 loop ---
|
||||
|
||||
test('trekbrief — v5.9 Phase 3.5 canonical mapping contains the fable row and offers 4 options', () => {
|
||||
const text = read();
|
||||
const startIdx = text.indexOf('## Phase 3.5');
|
||||
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
|
||||
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
|
||||
assert.ok(section.includes('fable → {effort: high, model: fable}'),
|
||||
'Phase 3.5 canonical mapping must contain the fable tier row');
|
||||
assert.ok(section.includes('with 4 options'),
|
||||
'Phase 3.5 loop must offer 4 options (AskUserQuestion maxItems: 4)');
|
||||
});
|
||||
|
||||
// --- v5.5 — framing enforcement + TL;DR + memory-alignment prose-pins ---
|
||||
|
||||
test('trekbrief — v5.5 Phase 2.5 framing declaration heading present', () => {
|
||||
|
|
@ -174,35 +78,3 @@ test('trekbrief — v5.5 template carries framing field, 2.2, and TL;DR section'
|
|||
'template frontmatter must include the framing field');
|
||||
assert.match(tpl, /^## TL;DR$/m, 'template must include the ## TL;DR section');
|
||||
});
|
||||
|
||||
test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => {
|
||||
const partial = `---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-14
|
||||
task: "Partial brief"
|
||||
slug: partial-brief
|
||||
project_dir: .claude/projects/2026-05-14-partial-brief/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 2
|
||||
source: fixture
|
||||
phase_signals_partial: true
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
## Intent
|
||||
Stop early.
|
||||
|
||||
## Goal
|
||||
Test partial mode.
|
||||
|
||||
## Success Criteria
|
||||
- gate does not fire.
|
||||
`;
|
||||
const r = validateBriefContent(partial, { strict: true });
|
||||
assert.equal(r.valid, true, `errors=${JSON.stringify(r.errors)}`);
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,84 +1,21 @@
|
|||
// tests/commands/trekexecute.test.mjs
|
||||
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekexecute.
|
||||
// Low effort runs the plan as one session, step after step; it sets no --gates
|
||||
// (fix round 4-5a, #33).
|
||||
// Prose-pin and runtime tests for /trekexecute.
|
||||
|
||||
import { after, test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
import { sectionText } from '../helpers/md-section.mjs';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md');
|
||||
const PHASE = 'execute';
|
||||
|
||||
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
||||
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
return doc.parsed && doc.parsed.frontmatter;
|
||||
}
|
||||
|
||||
// --- Pattern D prose-pins ---
|
||||
|
||||
test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekexecute must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekexecute must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekexecute — low-effort path runs one session, step after step, and sets no --gates open', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.doesNotMatch(section, /--gates open/, 'Low effort must not set --gates open');
|
||||
assert.match(section.replace(/\s+/g, ' '), /`effort == 'low'` runs the plan as one session, step after step \(Phase 2\.5\)/,
|
||||
'Low-effort path must run the plan as one session, step after step');
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC4 + SC7 ---
|
||||
|
||||
test('trekexecute — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'low');
|
||||
assert.equal(r.model, 'sonnet');
|
||||
});
|
||||
|
||||
test('trekexecute — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'standard');
|
||||
assert.equal(r.model, undefined);
|
||||
});
|
||||
|
||||
test('trekexecute — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'high');
|
||||
assert.equal(r.model, 'opus');
|
||||
});
|
||||
|
||||
test('trekexecute — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// --- S38 loop-discipline hardening: completion gate (Step 2) ---
|
||||
|
||||
|
|
@ -201,9 +138,9 @@ test('trekexecute Phase 8: the state-file block imports through the plugin root
|
|||
}
|
||||
});
|
||||
|
||||
// --- Phase 1 --project: the brief check and the execute resolver -------------
|
||||
// Phase 1's --project branch runs the two lines /trekplan runs, for the execute
|
||||
// phase, and halts only on BRIEF_V51_MISSING_SIGNALS.
|
||||
// --- Phase 1 --project: the brief check -------------------------------------
|
||||
// Phase 1's --project branch runs the brief-validator line /trekplan runs, and
|
||||
// never halts on what it reports.
|
||||
|
||||
/** Phase 1's rule 1 (the --project branch), as lines. */
|
||||
function projectRule() {
|
||||
|
|
@ -215,35 +152,25 @@ function projectRule() {
|
|||
return sec.slice(start, end).join('\n');
|
||||
}
|
||||
|
||||
test('trekexecute Phase 1 --project runs brief-validator --soft --json and the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
test('trekexecute Phase 1 --project runs brief-validator --soft --json and never halts on its errors', () => {
|
||||
const rule = projectRule();
|
||||
const validator = libLine(rule, 'validators/brief-validator.mjs', '--soft --json');
|
||||
const resolver = libLine(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
|
||||
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
|
||||
assert.equal(resolver,
|
||||
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
|
||||
const f = flat(rule);
|
||||
for (const s of [
|
||||
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
|
||||
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
|
||||
'Any other error is printed in one line and the run continues.',
|
||||
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
|
||||
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
|
||||
'No `{dir}/brief.md`: skip both lines.',
|
||||
'Any error is printed in one line and the run continues.',
|
||||
'A validator that cannot run (no JSON) is "not measured" in one line, and the run continues.',
|
||||
'No `{dir}/brief.md`: skip the line.',
|
||||
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
|
||||
|
||||
// The two lines run as printed: the validator names the one halting code for a
|
||||
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
|
||||
// The line runs as printed: a brief with an error exits 1 and still prints JSON.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'trekexecute-project-'));
|
||||
try {
|
||||
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
|
||||
writeFileSync(join(dir, 'brief.md'), '---\ntype: trekbrief\n---\n\n# Task\n');
|
||||
const v = bash(validator.split('{dir}').join(dir), ROOT);
|
||||
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
|
||||
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_V51_MISSING_SIGNALS'), v.stdout);
|
||||
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
|
||||
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
|
||||
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
|
||||
assert.equal(JSON.parse(r.stdout).effort, 'low');
|
||||
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_MISSING_FIELD'), v.stdout);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
|
@ -362,36 +289,9 @@ test('trekexecute merge lock: Phase 7.2 step 4 run on a BLOCK review of an older
|
|||
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
|
||||
});
|
||||
|
||||
// --- The resolver runs in Phase 1; low effort is one session; no gates escalation
|
||||
// (fix round 4-5a, #33).
|
||||
// --- No gates escalation (fix round 4-5a, #33).
|
||||
|
||||
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
|
||||
assert.doesNotMatch(flat(read()), /invoked in Phase 2\.4/);
|
||||
assert.ok(flat(sectionText(read(), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
|
||||
'the Composition rule does not say the resolver is invoked in Phase 1');
|
||||
});
|
||||
|
||||
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
|
||||
const f = flat(sectionText(read(), '## Composition rule (v5.1)'));
|
||||
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
|
||||
assert.ok(f.includes('it is returned for cross-command uniformity.'),
|
||||
'the Composition rule does not say why the model is returned');
|
||||
assert.doesNotMatch(f, /gates auto-escalation/);
|
||||
});
|
||||
|
||||
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
|
||||
const sec = sectionText(read(), '## Phase 2.5 — Execution strategy decision').split('\n');
|
||||
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
|
||||
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
|
||||
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
|
||||
const bullets = sec.slice(start + 1, end);
|
||||
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order, unless a wave is in flight: when "
|
||||
+ '`.claude/trekplan-sessions/{slug}/orders/wave.json` exists in the main checkout (the first entry of `git worktree list`), '
|
||||
+ 'this bullet does not apply and the multi-session path below collects the wave. The check only reads the file and never stops the run.'),
|
||||
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
|
||||
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left', () => {
|
||||
const text = read();
|
||||
const f = flat(text);
|
||||
assert.doesNotMatch(f, /gates_mode = 'closed'/);
|
||||
|
|
@ -399,9 +299,6 @@ test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_o
|
|||
assert.doesNotMatch(f, /halts immediately/);
|
||||
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
|
||||
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
|
||||
assert.equal(flat(sectionText(text, '### High-effort behavior (v5.1.1)')),
|
||||
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
|
||||
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
|
||||
});
|
||||
|
||||
// --- Phase 8 writes the merge evidence file before the merge call (4.5b2, MEV) --
|
||||
|
|
|
|||
|
|
@ -1,76 +1,17 @@
|
|||
// tests/commands/trekplan.test.mjs
|
||||
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekplan.
|
||||
// Prose-pin tests for /trekplan.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekplan.md');
|
||||
const PHASE = 'plan';
|
||||
|
||||
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
||||
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
return doc.parsed && doc.parsed.frontmatter;
|
||||
}
|
||||
|
||||
// --- Pattern D prose-pins (kept) ---
|
||||
|
||||
test('trekplan — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekplan must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekplan must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekplan — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC4 + SC7 tests ---
|
||||
|
||||
test('trekplan — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'low');
|
||||
assert.equal(r.model, 'sonnet');
|
||||
});
|
||||
|
||||
test('trekplan — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'standard');
|
||||
assert.equal(r.model, undefined);
|
||||
});
|
||||
|
||||
test('trekplan — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'high');
|
||||
assert.equal(r.model, 'opus');
|
||||
});
|
||||
|
||||
test('trekplan — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// --- S31 / V15: export trim — drop pr|issue|markdown, keep headless as a --decompose alias ---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,21 @@
|
|||
// tests/commands/trekresearch.test.mjs
|
||||
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekresearch.
|
||||
// Prose-pin tests for /trekresearch.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekresearch.md');
|
||||
const PHASE = 'research';
|
||||
|
||||
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
||||
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
return doc.parsed && doc.parsed.frontmatter;
|
||||
}
|
||||
|
||||
// --- Pattern D prose-pins ---
|
||||
|
||||
test('trekresearch — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekresearch must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekresearch must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekresearch — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
// Bound the Composition rule section by the next `###` heading rather than a
|
||||
// magic 2000-character window: a fixed count silently drops the match as soon
|
||||
// as prose is inserted above it, turning a real pin into a no-op.
|
||||
const sectionOf = (doc) => {
|
||||
const compIdx = doc.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const nextHeading = doc.indexOf('\n### ', compIdx);
|
||||
return nextHeading > compIdx ? doc.slice(compIdx, nextHeading) : doc.slice(compIdx);
|
||||
};
|
||||
|
||||
// (a) positive: the low-effort path is documented inside the bounded section.
|
||||
assert.match(sectionOf(text), /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
|
||||
// (b) negative: an actual removal must still be caught — a bound that can
|
||||
// never fail proves nothing.
|
||||
const mutated = text.replace(/--quick/g, '--removed');
|
||||
assert.doesNotMatch(sectionOf(mutated), /--quick/,
|
||||
'heading-bounded slice must still fail on a genuine removal');
|
||||
});
|
||||
|
||||
// --- Phase 5 slice (heading-bounded) ---
|
||||
|
||||
// Same bounding discipline as the Composition-rule pin above: slice from the
|
||||
// phase heading to the NEXT phase heading, never a fixed character window.
|
||||
// Slice from the phase heading to the NEXT phase heading, never a fixed character window.
|
||||
function phaseSlice(doc, startHeading, endHeading) {
|
||||
const start = doc.indexOf(startHeading);
|
||||
assert.ok(start >= 0, `${startHeading} missing`);
|
||||
|
|
@ -70,52 +28,15 @@ function phase5(doc) {
|
|||
return phaseSlice(doc, '## Phase 5 —', '## Phase 6 —');
|
||||
}
|
||||
|
||||
test('trekresearch — High-effort behavior gives standard effort the wave-2 sentence and keeps the low effort sentence verbatim', () => {
|
||||
const text = read();
|
||||
const flatText = text.replace(/\s+/g, ' ');
|
||||
test('trekresearch — Phase 4 runs the full swarm with contrarian-researcher as wave 2 every time (decision 209)', () => {
|
||||
const flatText = read().replace(/\s+/g, ' ');
|
||||
assert.ok(
|
||||
flatText.includes('Every run launches the FULL swarm for its scope: all 5 local agents and all 4 external agents, and `contrarian-researcher` runs every time, as wave 2 (see Launch rules)'),
|
||||
'Phase 4 must state the one fixed path: the full swarm, contrarian-researcher as wave 2 every run',
|
||||
);
|
||||
assert.ok(
|
||||
!flatText.includes('use the existing conditional triggers'),
|
||||
'standard effort must not point at a conditional trigger that nothing defines',
|
||||
);
|
||||
assert.ok(
|
||||
flatText.includes('Standard effort (or absent): the selected agents run as in Phase 4, and `contrarian-researcher`, when selected, runs as wave 2 after the other agents return (see Launch rules).'),
|
||||
'the standard-effort sentence must say the contrarian runs as wave 2 when selected',
|
||||
);
|
||||
assert.ok(
|
||||
text.includes('Low effort: inline research only, no agent swarm'),
|
||||
'the low-effort sentence must survive the Phase 5 rewrite verbatim',
|
||||
);
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC4 + SC7 ---
|
||||
|
||||
test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'low');
|
||||
assert.equal(r.model, 'sonnet');
|
||||
});
|
||||
|
||||
test('trekresearch — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'standard');
|
||||
assert.equal(r.model, undefined);
|
||||
});
|
||||
|
||||
test('trekresearch — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'high');
|
||||
assert.equal(r.model, 'opus');
|
||||
});
|
||||
|
||||
test('trekresearch — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
|
||||
'no effort must point at a conditional trigger that nothing defines',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -134,17 +55,14 @@ function paragraphAt(text, marker) {
|
|||
return flat(lines.slice(start, end).join(' '));
|
||||
}
|
||||
|
||||
test('trekresearch — Phase 5 follow-ups take phase_signal_result.model, else opus', () => {
|
||||
test('trekresearch — Phase 5 follow-ups: a built-in agent is spawned with model opus, at most 2', () => {
|
||||
const text = read();
|
||||
assert.doesNotMatch(flat(phase5(text)), /model: "opus"/);
|
||||
const part = paragraphAt(phase5(text), 'launch a targeted follow-up agent');
|
||||
assert.match(part, /Maximum 2 follow-ups\./);
|
||||
assert.match(part, /launch a targeted follow-up agent \(model: phase_signal_result\.model, else opus — a follow-up may be one of Claude Code's built-in agents, which have no Voyage frontmatter to fall back to; see Launch rules\)/);
|
||||
assert.match(flat(text), /captured as `phase_signal_result` and used at Agent-spawn sites in Phase 4 and Phase 5 to inject the resolved model/);
|
||||
assert.ok(part.includes("A follow-up may be one of Claude Code's built-in agents: Spawn it with `model: \"opus\"`: a built-in agent has no Voyage pin."), part);
|
||||
});
|
||||
|
||||
test('trekresearch — promises no maxTurns budget', () => {
|
||||
const text = read();
|
||||
assert.doesNotMatch(text, /maxTurns/);
|
||||
assert.match(text, /- convention-scanner: medium\+ codebases only \(50\+ files\)/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
// tests/commands/trekreview.test.mjs
|
||||
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekreview.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md');
|
||||
const PHASE = 'review';
|
||||
|
||||
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
||||
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
return doc.parsed && doc.parsed.frontmatter;
|
||||
}
|
||||
|
||||
// --- Pattern D prose-pins ---
|
||||
|
||||
test('trekreview — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekreview must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekreview must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekreview — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC4 + SC7 ---
|
||||
|
||||
test('trekreview — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'low');
|
||||
assert.equal(r.model, 'sonnet');
|
||||
});
|
||||
|
||||
test('trekreview — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'standard');
|
||||
assert.equal(r.model, undefined);
|
||||
});
|
||||
|
||||
test('trekreview — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
|
||||
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
|
||||
const r = resolvePhaseSignal(fm, PHASE);
|
||||
assert.equal(r.effort, 'high');
|
||||
assert.equal(r.model, 'opus');
|
||||
});
|
||||
|
||||
test('trekreview — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
// Falsification via brief-v21-no-signals fixture: validator must catch missing signals.
|
||||
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
|
||||
);
|
||||
});
|
||||
45
tests/fixtures/brief-effort-fable.md
vendored
45
tests/fixtures/brief-effort-fable.md
vendored
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-07-02
|
||||
task: "Fixture: high-effort all phases on fable (v5.9 allowlist test)"
|
||||
slug: brief-effort-fable
|
||||
project_dir: .claude/projects/2026-07-02-brief-effort-fable/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 4
|
||||
source: fixture
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: high
|
||||
model: fable
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: fable
|
||||
- phase: execute
|
||||
effort: high
|
||||
model: fable
|
||||
- phase: review
|
||||
effort: high
|
||||
model: fable
|
||||
---
|
||||
|
||||
# Task: High-effort fable fixture
|
||||
|
||||
## Intent
|
||||
|
||||
Test fixture for the v5.9 fable model tier — all 4 phases at the
|
||||
high effort tier with explicit fable model overrides. Mirrors
|
||||
brief-effort-high.md with `model: opus` replaced by `model: fable`.
|
||||
|
||||
## Goal
|
||||
|
||||
Resolver returns `{effort: 'high', model: 'fable'}` for each of the 4
|
||||
PHASE_SIGNAL_PHASES.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes with no BRIEF_INVALID_MODEL.
|
||||
- resolvePhaseSignal(fm, phase).effort === 'high' for all 4 phases.
|
||||
- resolvePhaseSignal(fm, phase).model === 'fable' for all 4 phases.
|
||||
45
tests/fixtures/brief-effort-high.md
vendored
45
tests/fixtures/brief-effort-high.md
vendored
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-14
|
||||
task: "Fixture: high-effort all phases (v5.1.1 runtime test)"
|
||||
slug: brief-effort-high
|
||||
project_dir: .claude/projects/2026-05-14-brief-effort-high/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 4
|
||||
source: fixture
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: execute
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: review
|
||||
effort: high
|
||||
model: opus
|
||||
---
|
||||
|
||||
# Task: High-effort fixture
|
||||
|
||||
## Intent
|
||||
|
||||
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the
|
||||
high effort tier with explicit opus model overrides. Mirrors the
|
||||
production-grade premium-profile scenario.
|
||||
|
||||
## Goal
|
||||
|
||||
Resolver returns `{effort: 'high', model: 'opus'}` for each of the 4
|
||||
PHASE_SIGNAL_PHASES.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- resolvePhaseSignal(fm, phase).effort === 'high' for all 4 phases.
|
||||
- resolvePhaseSignal(fm, phase).model === 'opus' for all 4 phases.
|
||||
43
tests/fixtures/brief-effort-low.md
vendored
43
tests/fixtures/brief-effort-low.md
vendored
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-14
|
||||
task: "Fixture: low-effort all phases (v5.1.1 runtime test)"
|
||||
slug: brief-effort-low
|
||||
project_dir: .claude/projects/2026-05-14-brief-effort-low/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 4
|
||||
source: fixture
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: execute
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: review
|
||||
effort: low
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Task: Low-effort fixture
|
||||
|
||||
## Intent
|
||||
|
||||
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the lowest
|
||||
effort tier with explicit sonnet model overrides.
|
||||
|
||||
## Goal
|
||||
|
||||
Resolver returns `{effort: 'low', model: 'sonnet'}` for each of the 4
|
||||
PHASE_SIGNAL_PHASES.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- resolvePhaseSignal(fm, phase) is non-null for all 4 phases.
|
||||
42
tests/fixtures/brief-effort-standard.md
vendored
42
tests/fixtures/brief-effort-standard.md
vendored
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-14
|
||||
task: "Fixture: standard-effort all phases, no model (v5.1.1 runtime test)"
|
||||
slug: brief-effort-standard
|
||||
project_dir: .claude/projects/2026-05-14-brief-effort-standard/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 4
|
||||
source: fixture
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: standard
|
||||
- phase: plan
|
||||
effort: standard
|
||||
- phase: execute
|
||||
effort: standard
|
||||
- phase: review
|
||||
effort: standard
|
||||
---
|
||||
|
||||
# Task: Standard-effort fixture (no model override)
|
||||
|
||||
## Intent
|
||||
|
||||
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the
|
||||
standard tier WITHOUT explicit model fields. This is the operator-skipped
|
||||
model path that should fall through to the profile.
|
||||
|
||||
## Goal
|
||||
|
||||
Resolver returns `{effort: 'standard', model: undefined}` for each of the 4
|
||||
PHASE_SIGNAL_PHASES. The orchestrator-model path then falls through to the
|
||||
active profile's phase_models.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- resolvePhaseSignal(fm, phase).model is undefined.
|
||||
- resolvePhaseSignal(fm, phase).effort is 'standard'.
|
||||
32
tests/fixtures/brief-v21-no-signals.md
vendored
32
tests/fixtures/brief-v21-no-signals.md
vendored
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-14
|
||||
task: "Fixture: v5.1 brief WITHOUT phase_signals or partial (falsification target)"
|
||||
slug: brief-v21-no-signals
|
||||
project_dir: .claude/projects/2026-05-14-brief-v21-no-signals/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 4
|
||||
source: fixture
|
||||
---
|
||||
|
||||
# Task: brief_version 2.1 without phase_signals
|
||||
|
||||
## Intent
|
||||
|
||||
Falsification fixture for the v5.1 sequencing gate. The brief declares
|
||||
`brief_version: "2.1"` but omits BOTH `phase_signals` AND
|
||||
`phase_signals_partial: true`. The brief-validator MUST emit
|
||||
`BRIEF_V51_MISSING_SIGNALS` for this file — the runtime test for the
|
||||
sequencing gate asserts the error code fires.
|
||||
|
||||
## Goal
|
||||
|
||||
Validate that brief-validator catches the missing-signals scenario.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- brief-validator returns valid: false.
|
||||
- errors contains BRIEF_V51_MISSING_SIGNALS.
|
||||
42
tests/fixtures/brief-with-phase-signals.md
vendored
42
tests/fixtures/brief-with-phase-signals.md
vendored
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-13
|
||||
task: "Add per-phase effort dialog to /trekbrief"
|
||||
slug: phase-signals-example
|
||||
project_dir: .claude/projects/2026-05-13-phase-signals-example/
|
||||
research_topics: 2
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 6
|
||||
source: interview
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
effort: standard
|
||||
- phase: execute
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: review
|
||||
effort: standard
|
||||
---
|
||||
|
||||
# Task: Phase-signals example
|
||||
|
||||
## Intent
|
||||
|
||||
A minimal brief that exercises the v5.1 phase_signals additive field with a
|
||||
mix of effort levels and model overrides. Used by tests/validators to confirm
|
||||
the validator accepts well-formed signals across the supported tier matrix.
|
||||
|
||||
## Goal
|
||||
|
||||
Validator returns valid: true. annotate.mjs strips phase_signals from the
|
||||
rendered HTML body (frontmatter stays in source).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- annotate.mjs determinism: re-run produces byte-identical HTML.
|
||||
31
tests/fixtures/brief-without-phase-signals.md
vendored
31
tests/fixtures/brief-without-phase-signals.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.0"
|
||||
created: 2026-05-13
|
||||
task: "Backward-compat fixture for v5.0-style brief"
|
||||
slug: legacy-brief-example
|
||||
project_dir: .claude/projects/2026-05-13-legacy-brief-example/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 3
|
||||
source: interview
|
||||
---
|
||||
|
||||
# Task: Legacy brief example
|
||||
|
||||
## Intent
|
||||
|
||||
A pre-v5.1 brief that pre-dates the phase_signals field. Used by
|
||||
tests/validators to confirm backward-compatibility: the brief is accepted
|
||||
without phase_signals as long as brief_version is < 2.1.
|
||||
|
||||
## Goal
|
||||
|
||||
Validator returns valid: true. The sequencing gate
|
||||
(BRIEF_V51_MISSING_SIGNALS) does NOT fire for brief_version 2.0.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- No BRIEF_V51_MISSING_SIGNALS error in r.errors.
|
||||
57
tests/fixtures/plan-profile-drift.md
vendored
57
tests/fixtures/plan-profile-drift.md
vendored
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
plan_version: "1.7"
|
||||
profile: economy
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: sonnet
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: sonnet
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Test plan — profile drift fixture
|
||||
|
||||
Frontmatter declares `profile: economy`. Step 1 manifest has matching
|
||||
profile_used. Step 2 manifest declares `profile_used: premium` — the
|
||||
drift case Step 20 of v4.1 plan-validator must catch in --strict mode.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: matching profile
|
||||
|
||||
- Files: a.ts
|
||||
- Manifest:
|
||||
```yaml
|
||||
manifest:
|
||||
expected_paths:
|
||||
- a.ts
|
||||
min_file_count: 1
|
||||
commit_message_pattern: "^feat:"
|
||||
bash_syntax_check: []
|
||||
forbidden_paths: []
|
||||
must_contain: []
|
||||
profile_used: economy
|
||||
```
|
||||
|
||||
### Step 2: drift to premium
|
||||
|
||||
- Files: b.ts
|
||||
- Manifest:
|
||||
```yaml
|
||||
manifest:
|
||||
expected_paths:
|
||||
- b.ts
|
||||
min_file_count: 1
|
||||
commit_message_pattern: "^feat:"
|
||||
bash_syntax_check: []
|
||||
forbidden_paths: []
|
||||
must_contain: []
|
||||
profile_used: premium
|
||||
```
|
||||
26
tests/fixtures/plan-with-profile.md
vendored
26
tests/fixtures/plan-with-profile.md
vendored
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
plan_version: "1.7"
|
||||
profile: balanced
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Test plan (with profile)
|
||||
|
||||
This fixture has explicit profile + phase_models in frontmatter.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Stub
|
||||
- Files: src/stub.mjs
|
||||
15
tests/fixtures/plan-without-profile.md
vendored
15
tests/fixtures/plan-without-profile.md
vendored
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
plan_version: "1.6"
|
||||
---
|
||||
|
||||
# Test plan (v4.0-style, no profile field)
|
||||
|
||||
This fixture is a v4.0-style plan WITHOUT the v4.1 profile/phase_models fields.
|
||||
Used by tests/lib/profile-application.test.mjs to verify backward-compat
|
||||
edge-case: resolveTrekcontinueProfile returns {profile: 'premium', profile_source: 'default'}
|
||||
without throwing when the plan has no profile concept.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Stub
|
||||
- Files: src/stub.mjs
|
||||
21
tests/fixtures/profile-invalid-enum.yaml
vendored
21
tests/fixtures/profile-invalid-enum.yaml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
profile_version: "1.0"
|
||||
name: invalid-enum
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 4
|
||||
parallel_agents_max: 6
|
||||
external_research_enabled: "yes"
|
||||
brief_reviewer_iter_cap: 2
|
||||
---
|
||||
21
tests/fixtures/profile-invalid-model.yaml
vendored
21
tests/fixtures/profile-invalid-model.yaml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
profile_version: "1.0"
|
||||
name: invalid-model
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: gpt-4
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 2
|
||||
parallel_agents_max: 4
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 2
|
||||
---
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
// tests/integration/profile-jaccard-smoke.test.mjs
|
||||
// SC #18 — cross-tier Jaccard smoke-test for v4.1 model profiles.
|
||||
//
|
||||
// Pairs the 4 parked-synthetic fixtures from Step 17:
|
||||
// profile-plan-run-economy-{1,2}.md × profile-plan-run-premium-{1,2}.md
|
||||
//
|
||||
// Asserts that every cross-tier pair clears CROSS_TIER_JACCARD_FLOOR
|
||||
// after string-normalisering (lowercase, strip backticks/parens, collapse
|
||||
// whitespace). The pre-gates run BEFORE Jaccard:
|
||||
// 1. Frontmatter parses cleanly on both fixtures
|
||||
// 2. Step-count parity (±20 %) — hard fail independent of Jaccard
|
||||
//
|
||||
// Empirically calibrated, NOT literature-canonical (see
|
||||
// research/02-jaccard-syntese-quality.md). arXiv:2412.12148: there is no
|
||||
// universal threshold; 0.55 is conservative starting point per Step 17
|
||||
// calibration file (tests/synthetic/profile-jaccard-calibration.md).
|
||||
//
|
||||
// Plan-critic-fallback (auto-tighten if Jaccard insufficient) is NOT in
|
||||
// v4.1 — deferred to v4.2 per research/02 Recommendation #5.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve, join } from 'node:path';
|
||||
|
||||
import { jaccardSimilarity } from '../../lib/parsers/jaccard.mjs';
|
||||
import { normalizeSteps, checkStepCountParity } from '../../lib/parsers/profile-jaccard.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, '..', '..');
|
||||
|
||||
// Empirically calibrated, NOT literature-canonical.
|
||||
// See tests/synthetic/profile-jaccard-calibration.md for derivation.
|
||||
const CROSS_TIER_JACCARD_FLOOR = 0.55;
|
||||
|
||||
const ECONOMY_FIXTURES = [
|
||||
'tests/synthetic/profile-plan-run-economy-1.md',
|
||||
'tests/synthetic/profile-plan-run-economy-2.md',
|
||||
];
|
||||
const PREMIUM_FIXTURES = [
|
||||
'tests/synthetic/profile-plan-run-premium-1.md',
|
||||
'tests/synthetic/profile-plan-run-premium-2.md',
|
||||
];
|
||||
|
||||
function loadSteps(rel) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(
|
||||
doc.valid,
|
||||
`frontmatter of ${rel} did not parse: ${(doc.errors || []).map((e) => e.message).join(', ')}`,
|
||||
);
|
||||
const steps = doc.parsed.frontmatter && doc.parsed.frontmatter.steps;
|
||||
assert.ok(
|
||||
Array.isArray(steps) && steps.length > 0,
|
||||
`frontmatter.steps of ${rel} is missing or empty`,
|
||||
);
|
||||
return steps;
|
||||
}
|
||||
|
||||
// --- Pre-gate 1: structural frontmatter integrity (acts as plan-validator
|
||||
// pre-gate for synthetic frontmatter-only fixtures; real plan-md goes
|
||||
// through node lib/validators/plan-validator.mjs --strict separately).
|
||||
test('profile-jaccard-smoke — pre-gate: all 4 fixtures parse cleanly with frontmatter.steps', () => {
|
||||
for (const rel of [...ECONOMY_FIXTURES, ...PREMIUM_FIXTURES]) {
|
||||
const steps = loadSteps(rel);
|
||||
assert.ok(steps.length >= 10, `${rel}: < 10 steps (got ${steps.length})`);
|
||||
// Sanity: all entries are non-empty strings
|
||||
for (const s of steps) {
|
||||
assert.equal(typeof s, 'string', `${rel}: non-string step: ${JSON.stringify(s)}`);
|
||||
assert.ok(s.trim().length > 0, `${rel}: empty step entry`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Pre-gate 2: step-count parity (±20 % cross-tier).
|
||||
test('profile-jaccard-smoke — pre-gate: step-count parity ±20% across cross-tier pairs', () => {
|
||||
for (const eFix of ECONOMY_FIXTURES) {
|
||||
for (const pFix of PREMIUM_FIXTURES) {
|
||||
const eSteps = loadSteps(eFix);
|
||||
const pSteps = loadSteps(pFix);
|
||||
const r = checkStepCountParity(eSteps, pSteps, 0.34);
|
||||
// Note: synthetic economy=30, premium=40 → ratio = 10/40 = 0.25.
|
||||
// We allow 0.34 here because empirical cross-tier may exceed 0.20
|
||||
// when one tier prunes verification steps. Tighten in v4.2 once
|
||||
// empirical data lands.
|
||||
assert.ok(r.ok, `${eFix} × ${pFix}: ${r.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Cross-tier Jaccard: every pair must clear floor (after normalisering).
|
||||
test('profile-jaccard-smoke — cross-tier Jaccard ≥ floor for all 4 economy×premium pairs', () => {
|
||||
const pairs = [];
|
||||
for (const eFix of ECONOMY_FIXTURES) {
|
||||
for (const pFix of PREMIUM_FIXTURES) {
|
||||
const eSteps = normalizeSteps(loadSteps(eFix));
|
||||
const pSteps = normalizeSteps(loadSteps(pFix));
|
||||
const sim = jaccardSimilarity(eSteps, pSteps);
|
||||
pairs.push({ eFix, pFix, sim });
|
||||
}
|
||||
}
|
||||
|
||||
// Report all pairs in failure message for diagnostic clarity.
|
||||
const failures = pairs.filter((p) => p.sim < CROSS_TIER_JACCARD_FLOOR);
|
||||
if (failures.length > 0) {
|
||||
const summary = pairs
|
||||
.map((p) => ` ${p.eFix.split('/').pop()} × ${p.pFix.split('/').pop()}: ${p.sim.toFixed(3)}`)
|
||||
.join('\n');
|
||||
assert.fail(
|
||||
`${failures.length}/${pairs.length} cross-tier pairs below floor ${CROSS_TIER_JACCARD_FLOOR}:\n${summary}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Sanity-floor: at least 4 pairs measured (2×2 cross product).
|
||||
assert.equal(pairs.length, 4, 'expected 4 cross-tier pairs (2 economy × 2 premium)');
|
||||
});
|
||||
|
||||
// --- Intra-tier sanity: same-profile pairs must have HIGHER Jaccard than
|
||||
// cross-tier (otherwise the smoke-test is not actually discriminating).
|
||||
test('profile-jaccard-smoke — intra-tier Jaccard > cross-tier mean (sanity for discriminator)', () => {
|
||||
const intraEconomy = jaccardSimilarity(
|
||||
normalizeSteps(loadSteps(ECONOMY_FIXTURES[0])),
|
||||
normalizeSteps(loadSteps(ECONOMY_FIXTURES[1])),
|
||||
);
|
||||
const intraPremium = jaccardSimilarity(
|
||||
normalizeSteps(loadSteps(PREMIUM_FIXTURES[0])),
|
||||
normalizeSteps(loadSteps(PREMIUM_FIXTURES[1])),
|
||||
);
|
||||
|
||||
let crossSum = 0;
|
||||
let crossN = 0;
|
||||
for (const eFix of ECONOMY_FIXTURES) {
|
||||
for (const pFix of PREMIUM_FIXTURES) {
|
||||
crossSum += jaccardSimilarity(
|
||||
normalizeSteps(loadSteps(eFix)),
|
||||
normalizeSteps(loadSteps(pFix)),
|
||||
);
|
||||
crossN += 1;
|
||||
}
|
||||
}
|
||||
const crossMean = crossSum / crossN;
|
||||
|
||||
assert.ok(
|
||||
intraEconomy > crossMean,
|
||||
`intra-tier Jaccard (economy: ${intraEconomy.toFixed(3)}) must exceed cross-tier mean (${crossMean.toFixed(3)})`,
|
||||
);
|
||||
assert.ok(
|
||||
intraPremium > crossMean,
|
||||
`intra-tier Jaccard (premium: ${intraPremium.toFixed(3)}) must exceed cross-tier mean (${crossMean.toFixed(3)})`,
|
||||
);
|
||||
});
|
||||
|
|
@ -281,3 +281,8 @@ test('trekreview --answers: the parser accepts it as a boolean, and the usage li
|
|||
assert.equal(usage.status, 1, usage.stdout + usage.stderr);
|
||||
assert.match(usage.stderr, /Usage: \/trekreview --project <dir> [^\n]*\[--answers\]/, `the usage line names --answers: ${usage.stderr}`);
|
||||
});
|
||||
|
||||
test('retired --profile swallows its value: --profile economy leaves positional untouched', () => {
|
||||
const r = parseArgs('--profile economy .claude/projects/x', 'trekcontinue');
|
||||
assert.deepEqual(r.positional, ['.claude/projects/x'], `the retired flag's value is no positional: ${JSON.stringify(r)}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,10 +169,9 @@ test('[#1] sibling: commands/trekexecute.md: `gates_mode` changes nothing in `/t
|
|||
'trekexecute does not say gates_mode changes nothing there and the one stop is the merge into main');
|
||||
});
|
||||
|
||||
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
|
||||
test('[#1] sibling: docs/architecture.md does not name --gates open', () => {
|
||||
const text = read('docs/architecture.md');
|
||||
assert.doesNotMatch(text, /--gates open/);
|
||||
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
|
||||
});
|
||||
|
||||
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
|
||||
|
|
|
|||
|
|
@ -124,8 +124,7 @@ test('computeVerdict — counts each severity tier', () => {
|
|||
|
||||
test('reasonablenessFilter — citation-less is REFUTED, unknown rule_key is UNVERIFIED, severity mismatch corrected', () => {
|
||||
// Contract change (fail-closed): only `no-citation` refutes. An ad-hoc
|
||||
// rule_key is a real defect wearing the wrong label — v5.1.1 high-effort mode
|
||||
// already keeps those, normalised to PLAN_EXECUTE_DRIFT.
|
||||
// rule_key is a real defect wearing the wrong label.
|
||||
const r = reasonablenessFilter([
|
||||
{ file: 'x.mjs', line: 1, rule_key: 'NOPE_KEY', severity: 'BLOCKER' }, // unknown → unverified
|
||||
{ file: '', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // no file → drop
|
||||
|
|
|
|||
|
|
@ -833,7 +833,7 @@ test('runCriteria: a nested `node --test` on a failing test is red even when NOD
|
|||
function briefWith(sections) {
|
||||
return [
|
||||
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
|
||||
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
|
||||
...sections,
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
//
|
||||
// Two kinds of pin live here (devil's-advocate audit §Top changes #8, S19):
|
||||
// • STRUCTURAL cross-file invariants — derive one side from a source-of-truth
|
||||
// (file counts, dimension counts from `### N.` headers, profile tables from
|
||||
// the yaml, version-sync) and survive harmless rewording. These are the
|
||||
// (file counts, dimension counts from `### N.` headers, version-sync) and
|
||||
// survive harmless rewording. These are the
|
||||
// load-bearing ones; keep and grow them.
|
||||
// • PROSE / EXISTENCE pins — assert a literal string or file fact. Most still
|
||||
// guard a real wiring (CLI shims, annotate.mjs, anti-stale CHANGELOG,
|
||||
|
|
@ -22,7 +22,6 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
import { resolveProfile, loadProfile } from '../../lib/profiles/resolver.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -310,27 +309,6 @@ test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json fin
|
|||
}
|
||||
});
|
||||
|
||||
// --- S20 — anti-false-claim: operations.md custom-profile prose must match findProfilePath ---
|
||||
//
|
||||
// findProfilePath (lib/profiles/resolver.mjs:57) resolves built-in FIRST, then
|
||||
// repo-root voyage-profiles/, then ~/.claude/voyage-profiles/. Custom profiles
|
||||
// live in voyage-profiles/ (NOT lib/profiles/), must use NEW names, and CANNOT
|
||||
// shadow a built-in. The old prose claimed the opposite (lib/profiles/<custom>;
|
||||
// "override built-ins of the same name"; "alphabetical … precedence") — guard it.
|
||||
test('operations.md custom-profile prose matches findProfilePath resolution order [S20]', () => {
|
||||
const ops = read('docs/operations.md');
|
||||
for (const bad of ['lib/profiles/<custom>', 'alphabetical', 'override built-ins of the same name']) {
|
||||
assert.ok(
|
||||
!ops.includes(bad),
|
||||
`docs/operations.md: false custom-profile claim "${bad}" contradicts findProfilePath — fix the prose, not this test`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
ops.includes('voyage-profiles/'),
|
||||
'docs/operations.md must point custom profiles at voyage-profiles/ (not lib/profiles/)',
|
||||
);
|
||||
});
|
||||
|
||||
// --- S24 — anti-false-claim: operations.md + command-modes.md autonomy-gate truth-pass ---
|
||||
//
|
||||
// autonomy-gate.mjs is the --gates flag parser: --gates [true|false|closed|open]
|
||||
|
|
@ -371,113 +349,6 @@ test('operations.md + command-modes.md describe the real autonomy-gate, not a fa
|
|||
}
|
||||
});
|
||||
|
||||
// --- v4.1 Step 21 — pin --profile + phase_models on the 6 commands ---
|
||||
//
|
||||
// CLAUDE.md / README.md pinning is deferred to Step 22 (post-write of
|
||||
// those documents). Step 21 only verifies command-file content, which
|
||||
// was written in Step 7 (Wave 3).
|
||||
|
||||
const PIPELINE_COMMANDS = [
|
||||
'trekbrief.md',
|
||||
'trekresearch.md',
|
||||
'trekplan.md',
|
||||
'trekexecute.md',
|
||||
'trekreview.md',
|
||||
'trekcontinue.md',
|
||||
];
|
||||
|
||||
test('every pipeline command-file documents the --profile flag (SC #20)', () => {
|
||||
for (const f of PIPELINE_COMMANDS) {
|
||||
const text = read(`commands/${f}`);
|
||||
assert.match(
|
||||
text,
|
||||
/--profile\b/,
|
||||
`commands/${f}: --profile flag is required documentation in v4.1`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('command-files mentioning model profiles use canonical name `phase_models`', () => {
|
||||
// Reject legacy / brainstormed alternatives that would confuse readers.
|
||||
const FORBIDDEN = ['model_per_phase', 'phase_to_model', 'profile_phase_models'];
|
||||
for (const f of PIPELINE_COMMANDS) {
|
||||
const text = read(`commands/${f}`);
|
||||
for (const bad of FORBIDDEN) {
|
||||
assert.ok(
|
||||
!text.includes(bad),
|
||||
`commands/${f}: forbidden alias "${bad}" — canonical name is "phase_models"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('at least one pipeline command-file references `phase_models` canonical name', () => {
|
||||
// Sanity: not every command has to enumerate phase_models inline (e.g.
|
||||
// trekbrief and trekcontinue may only mention --profile), but ≥ 1
|
||||
// command-file must spell out the canonical name so the regression test
|
||||
// pins drift.
|
||||
let mentioned = 0;
|
||||
for (const f of PIPELINE_COMMANDS) {
|
||||
if (read(`commands/${f}`).includes('phase_models')) mentioned += 1;
|
||||
}
|
||||
assert.ok(
|
||||
mentioned >= 1,
|
||||
`expected ≥ 1 command-file to mention canonical name "phase_models", got ${mentioned}`,
|
||||
);
|
||||
});
|
||||
|
||||
// --- v4.1 Step 22 — post-write CLAUDE.md / README.md pinning ---
|
||||
//
|
||||
// Plan-critic Blocker 2 fix: Step 21 only pinned commands/*.md (which
|
||||
// are written in Step 7 / Wave 3). Step 22 writes the top-level docs
|
||||
// and extends pinning here so doc-consistency stays green AFTER Step 22.
|
||||
|
||||
test('CLAUDE.md documents --profile flag', () => {
|
||||
const md = read('CLAUDE.md');
|
||||
assert.match(
|
||||
md,
|
||||
/--profile\b/,
|
||||
'CLAUDE.md must document the --profile flag (v4.1 SC #20)',
|
||||
);
|
||||
});
|
||||
|
||||
test('canonical name `phase_models` documented in docs/profiles.md', () => {
|
||||
// v5.x doc consolidation (67f6dd5) moved profile/phase detail OUT of
|
||||
// CLAUDE.md and into docs/. docs/profiles.md is now the canonical home for
|
||||
// phase_models; CLAUDE.md only links to it via "Reference docs". Assert the
|
||||
// canonical name lives in the canonical doc and no legacy alias crept in.
|
||||
// (was: read('CLAUDE.md') — v4.1 SC #20.)
|
||||
const md = read('docs/profiles.md');
|
||||
assert.match(
|
||||
md,
|
||||
/phase_models/,
|
||||
'docs/profiles.md must use canonical name "phase_models" (v4.1 SC #20)',
|
||||
);
|
||||
for (const bad of ['model_per_phase', 'phase_to_model', 'profile_phase_models']) {
|
||||
assert.ok(
|
||||
!md.includes(bad),
|
||||
`docs/profiles.md must NOT use legacy alias "${bad}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('README.md documents --profile flag for all 6 commands', () => {
|
||||
// SG1: README flag-table coverage is gating for SC #20. README is the
|
||||
// primary discovery surface for new users.
|
||||
const md = read('README.md');
|
||||
// Top-level Profile system section is required so the flag is
|
||||
// discoverable independent of per-command tables.
|
||||
assert.match(md, /## Profile system/, 'README.md missing top-level "## Profile system" section');
|
||||
// Every per-command Modes table must include --profile (count of
|
||||
// --profile occurrences should be ≥ 6 — one per command + Profile
|
||||
// system section).
|
||||
const profileMentions = (md.match(/--profile\b/g) || []).length;
|
||||
assert.ok(
|
||||
profileMentions >= 6,
|
||||
`README.md must mention --profile ≥ 6 times (one per command + section), got ${profileMentions}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v4.1.0 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(
|
||||
|
|
@ -487,19 +358,6 @@ test('CHANGELOG.md has v4.1.0 entry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('docs/profiles.md exists and documents Custom.yaml authoring', () => {
|
||||
const dp = read('docs/profiles.md');
|
||||
assert.ok(dp.length > 1000, 'docs/profiles.md must be substantive (> 1000 chars)');
|
||||
// Must document custom-profile authoring (Step 22 manifest must_contain
|
||||
// pattern: "Custom.yaml" — case-insensitive match handled here as
|
||||
// /[Cc]ustom[. ]/ to allow either "custom.yaml" or "Custom profile" prose).
|
||||
assert.match(
|
||||
dp,
|
||||
/[Cc]ustom\.yaml|[Cc]ustom profile|<custom>\.yaml/,
|
||||
'docs/profiles.md must document custom profile authoring',
|
||||
);
|
||||
});
|
||||
|
||||
test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
|
||||
const cmd = read('commands/trekplan.md');
|
||||
// Locate Phase 8 section
|
||||
|
|
@ -714,7 +572,7 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () =>
|
|||
}
|
||||
});
|
||||
|
||||
// --- v5.1 — phase_signals + brief_version 2.1 ---
|
||||
// --- v5.1 — brief_version 2.1 ---
|
||||
|
||||
test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => {
|
||||
const t = read('templates/trekbrief-template.md');
|
||||
|
|
@ -722,67 +580,11 @@ test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (qu
|
|||
'trekbrief-template.md must declare brief_version: "2.2" (quoted) — unquoted parses as Number and bypasses sequencing gate; v5.5 bumped 2.1→2.2 for framing enforcement');
|
||||
});
|
||||
|
||||
test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => {
|
||||
const t = read('templates/trekbrief-template.md');
|
||||
assert.match(t, /^phase_signals:$/m,
|
||||
'trekbrief-template.md must contain a phase_signals: block in frontmatter');
|
||||
});
|
||||
|
||||
test('v5.1 — HANDOVER-CONTRACTS.md schema row includes phase_signals + phase_signals_partial', () => {
|
||||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||
assert.ok(t.includes('| `phase_signals` |'),
|
||||
'HANDOVER-CONTRACTS must add a phase_signals row to the Handover 1 schema table');
|
||||
assert.ok(t.includes('| `phase_signals_partial` |'),
|
||||
'HANDOVER-CONTRACTS must add a phase_signals_partial row to the Handover 1 schema table');
|
||||
});
|
||||
|
||||
test('v5.1 — phase_signals documented in docs/HANDOVER-CONTRACTS.md', () => {
|
||||
// v5.x doc consolidation (67f6dd5) moved phase_signals out of CLAUDE.md.
|
||||
// phase_signals is a Handover 1 (brief) field, so docs/HANDOVER-CONTRACTS.md
|
||||
// is its canonical home (the schema rows are pinned separately above).
|
||||
// CLAUDE.md only links to it. (was: read('CLAUDE.md') — v5.1.)
|
||||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||
assert.ok(t.includes('phase_signals'),
|
||||
'docs/HANDOVER-CONTRACTS.md must document phase_signals (v5.1)');
|
||||
});
|
||||
|
||||
test('v5.1 — voyage README.md mentions phase_signals', () => {
|
||||
const t = read('README.md');
|
||||
assert.ok(t.includes('phase_signals'),
|
||||
'voyage README.md must mention phase_signals (v5.1 "What\'s new" bullet)');
|
||||
});
|
||||
|
||||
// --- v5.1.1 — High-effort behavior sub-section per command (Step 10) ---
|
||||
|
||||
test('v5.1.1 — commands/trekplan.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||||
const t = read('commands/trekplan.md');
|
||||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||||
'trekplan.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (Adversarial Pass 2, unavailable/skipped since v5.10.1)');
|
||||
});
|
||||
|
||||
test('v5.1.1 — commands/trekresearch.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||||
const t = read('commands/trekresearch.md');
|
||||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||||
'trekresearch.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (contrarian-researcher always-on)');
|
||||
});
|
||||
|
||||
test('v5.1.1 — commands/trekreview.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||||
const t = read('commands/trekreview.md');
|
||||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||||
'trekreview.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (skip Pass 3 + coordinator normalization)');
|
||||
});
|
||||
|
||||
test('v5.1.1 — commands/trekexecute.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||||
const t = read('commands/trekexecute.md');
|
||||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||||
'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (high effort changes nothing in /trekexecute)');
|
||||
});
|
||||
|
||||
// --- v5.4 — brief.md formalized as PUBLIC CONTRACT (Handover 1) ---
|
||||
// S5: the brief schema is the only public producer↔Voyage integration boundary
|
||||
// (Trinity asymmetry invariant). These pin the formalization as enforced doc-truth,
|
||||
// not prose — same pattern as the v5.1 Handover-1 pins above. Freeze, not promote:
|
||||
// phase_signals stays optional; brief_version 2.1 is the frozen baseline (S3 decision).
|
||||
// brief_version 2.1 is the frozen baseline (S3 decision).
|
||||
|
||||
test('v5.4 — HANDOVER-CONTRACTS.md Handover 1 heading is labeled (PUBLIC CONTRACT)', () => {
|
||||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||
|
|
@ -808,10 +610,8 @@ test('v5.5 — brief_version 2.2 is current in the public-contract schema table
|
|||
'Handover 1 schema table must mark brief_version "2.2" as current');
|
||||
});
|
||||
|
||||
test('v5.5 — phase_signals stays optional: not promoted to required', () => {
|
||||
test('v5.5 — the Versioning note cites no phantom v5.4 and records the 2.1 baseline', () => {
|
||||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||
assert.ok(!t.includes('v5.4 may promote'),
|
||||
'the speculative "v5.4 may promote phase_signals to required" line must be resolved (S3 froze the shape)');
|
||||
assert.ok(!/v5\.4/.test(t),
|
||||
'the contract doc must not cite the phantom v5.4 release (v5.2–5.4 never shipped; S16 → v5.5.0)');
|
||||
assert.ok(t.includes('established `2.1` as the public-contract baseline'),
|
||||
|
|
@ -851,103 +651,6 @@ test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2
|
|||
'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)');
|
||||
});
|
||||
|
||||
// --- S15 — default-profile name + per-phase model claims (cost-claim truth-pass) ---
|
||||
// The resolver's hardcoded default (lib/profiles/resolver.mjs) is the single
|
||||
// source of truth for which built-in profile a fresh user gets. README,
|
||||
// docs/profiles.md, and docs/operations.md must all mark THAT profile as the
|
||||
// default and must not mark any other built-in as default. Guards the
|
||||
// code-vs-docs contradiction the S14 audit surfaced (Addendum finding #3).
|
||||
const PROFILE_DOCS = ['README.md', 'docs/profiles.md', 'docs/operations.md'];
|
||||
|
||||
test('S15: default-profile name is consistent across resolver + all profile docs', () => {
|
||||
// Empty env so the operator's VOYAGE_PROFILE does not mask the hardcoded default.
|
||||
const { profile: def, profile_source } = resolveProfile({}, {});
|
||||
assert.equal(profile_source, 'default', 'resolveProfile({}, {}) must report source=default');
|
||||
assert.equal(def, 'premium', 'resolver hardcoded default is premium (operator decision 2026-05-13, commit 40d8742)');
|
||||
|
||||
const OTHERS = ['economy', 'balanced', 'premium', 'fable'].filter((p) => p !== def);
|
||||
for (const doc of PROFILE_DOCS) {
|
||||
const body = read(doc);
|
||||
assert.ok(
|
||||
body.includes(`\`${def}\` (default)`),
|
||||
`${doc}: profile table must mark \`${def}\` as "(default)" to match resolver`,
|
||||
);
|
||||
for (const other of OTHERS) {
|
||||
assert.ok(
|
||||
!body.includes(`\`${other}\` (default)`),
|
||||
`${doc}: must NOT mark \`${other}\` as "(default)" — resolver default is ${def}`,
|
||||
);
|
||||
assert.ok(
|
||||
!body.includes(`Default \`${other}\``),
|
||||
`${doc}: lookup-order must not say "Default \`${other}\`" — resolver default is ${def}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('S15: profile tables encode each built-in yaml phase_models exactly', () => {
|
||||
// Column order in every profile table: Profile | Brief | Research | Plan | Execute | Review | Continue | Use case
|
||||
const PHASES = ['brief', 'research', 'plan', 'execute', 'review', 'continue'];
|
||||
for (const name of ['economy', 'balanced', 'premium', 'fable']) {
|
||||
const pm = loadProfile(name).phase_models; // {brief:'opus', ...}
|
||||
const expected = PHASES.map((ph) => pm[ph]);
|
||||
for (const doc of PROFILE_DOCS) {
|
||||
const row = read(doc)
|
||||
.split('\n')
|
||||
.find((l) => new RegExp(`^\\|\\s*\`${name}\``).test(l));
|
||||
assert.ok(row, `${doc}: profile table is missing a \`${name}\` row`);
|
||||
const cells = row.split('|').map((c) => c.trim());
|
||||
// cells[0] is '' (leading pipe), cells[1] is the name, cells[2..7] are the 6 models
|
||||
const docModels = cells.slice(2, 8);
|
||||
assert.deepEqual(
|
||||
docModels,
|
||||
expected,
|
||||
`${doc}: \`${name}\` row models ${JSON.stringify(docModels)} must equal lib/profiles/${name}.yaml ${JSON.stringify(expected)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- S34 (V30) — economy is self-declared experimental until the cross-tier
|
||||
// Jaccard floor (0.55) is empirically calibrated (Step-17 calibration deferred
|
||||
// to v4.2). The status must be visible in BOTH the profile data
|
||||
// (lib/profiles/economy.yaml `experimental: true`) and every profile doc's
|
||||
// economy row. Fix the SOURCE doc/profile, not these pins.
|
||||
test('S34: economy profile row is flagged experimental across all profile docs', () => {
|
||||
for (const doc of PROFILE_DOCS) {
|
||||
const body = read(doc);
|
||||
const row = body.split('\n').find((l) => /^\|\s*`economy`/.test(l));
|
||||
assert.ok(row, `${doc}: profile table missing an \`economy\` row`);
|
||||
assert.match(
|
||||
row,
|
||||
/experimental/i,
|
||||
`${doc}: the \`economy\` table row must flag it experimental ` +
|
||||
`(uncalibrated Jaccard floor, S34/V30) — matches lib/profiles/economy.yaml experimental: true`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('S34: economy experimental flag agrees with parked-synthetic calibration status', () => {
|
||||
// economy.yaml experimental:true is honest ONLY while calibration is still
|
||||
// parked-synthetic. When empirical calibration lands (status: empirical),
|
||||
// drop experimental:true in the SAME change — this pin enforces the linkage.
|
||||
const eco = parseDocument(read('lib/profiles/economy.yaml')).parsed.frontmatter;
|
||||
const cal = parseDocument(read('tests/synthetic/profile-jaccard-calibration.md')).parsed.frontmatter;
|
||||
if (cal.status === 'parked-synthetic') {
|
||||
assert.equal(
|
||||
eco.experimental,
|
||||
true,
|
||||
'while calibration is parked-synthetic, economy.yaml must stay experimental: true',
|
||||
);
|
||||
} else {
|
||||
assert.notEqual(
|
||||
eco.experimental,
|
||||
true,
|
||||
'calibration is no longer parked-synthetic — drop economy experimental: true in the same change',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', () => {
|
||||
// All 24 sub-agents are model: opus (operator-pinned, commit 40d8742) and the
|
||||
// model is uniform per phase — there is no "Opus orchestrates / Sonnet runs
|
||||
|
|
@ -962,7 +665,7 @@ test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', (
|
|||
for (const phrase of BANNED) {
|
||||
assert.ok(
|
||||
!md.includes(phrase),
|
||||
`README.md must not claim "${phrase}" — sub-agents are opus-pinned; default profile runs Opus on every phase`,
|
||||
`README.md must not claim "${phrase}" — sub-agents are opus-pinned`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1070,7 +773,7 @@ test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01
|
|||
);
|
||||
const p3Start = text.indexOf('## Phase 3 — Completeness loop');
|
||||
assert.ok(p3Start >= 0, 'trekbrief.md must have a "## Phase 3 — Completeness loop" section');
|
||||
const p3End = text.indexOf('## Phase 3.5', p3Start);
|
||||
const p3End = text.indexOf('## Phase 4', p3Start);
|
||||
const p3 = text.slice(p3Start, p3End > p3Start ? p3End : undefined);
|
||||
assert.ok(
|
||||
/Use `AskUserQuestion` for every question/.test(p3),
|
||||
|
|
@ -1125,29 +828,6 @@ test('S38: forward-guard — no product-facing doc asserts main-context relief u
|
|||
);
|
||||
});
|
||||
|
||||
// ── v5.9 (fable-tier step 6) — composed-resolver wiring pin ────────────────
|
||||
// The four pipeline commands must resolve {effort, model} via the composed
|
||||
// CLI (`resolver.mjs --resolve-phase-model`, brief > profile > default). A
|
||||
// direct `phase-signal-resolver.mjs --brief` invocation in a command Bash
|
||||
// block is the brief-only CLI: it silently drops the profile layer (the
|
||||
// AP2-1 footgun — `--profile <x>` would never reach sub-agent spawns again).
|
||||
// If this pin fails, re-wire the command to the composed CLI — do not relax
|
||||
// the pin.
|
||||
|
||||
for (const cmd of ['trekresearch', 'trekplan', 'trekreview', 'trekexecute']) {
|
||||
test(`v5.9: commands/${cmd}.md invokes the composed resolver, not the brief-only CLI`, () => {
|
||||
const text = read(`commands/${cmd}.md`);
|
||||
assert.ok(
|
||||
text.includes('--resolve-phase-model'),
|
||||
`commands/${cmd}.md must invoke resolver.mjs --resolve-phase-model (composed brief > profile > default)`,
|
||||
);
|
||||
assert.ok(
|
||||
!/phase-signal-resolver\.mjs --brief/.test(text),
|
||||
`commands/${cmd}.md must not invoke the brief-only phase-signal-resolver CLI — the profile layer would be silently dropped`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ── v5.9 (fable-tier step 7) — orchestrator model-pin ABSENCE pin ───────────
|
||||
// Command frontmatter deliberately omits `model:` — omission is the only
|
||||
// session-inheritance spelling documented by BOTH official surfaces (AP2-2:
|
||||
|
|
@ -1221,11 +901,7 @@ test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritan
|
|||
// class as bumping a version string inside a measurement doc.
|
||||
//
|
||||
// BLIND SPOT, stated rather than hidden: THIS file cannot be in the live-surface
|
||||
// list, because the S95 block below is necessarily full of the word. So the two
|
||||
// v5.1.1 assertion messages above (which describe the high-effort rules) are not
|
||||
// covered by the scanner and were found stale by review, not by npm test — they
|
||||
// match on the heading regex, so a wrong message never fails. When the high-effort
|
||||
// rules change, re-read those messages by hand.
|
||||
// list, because the S95 block below is necessarily full of the word.
|
||||
|
||||
// Live = anything that can still steer a run. Explicit, so a new file forces a decision.
|
||||
const GEMINI_LIVE_SURFACES = [
|
||||
|
|
@ -1275,20 +951,10 @@ test('S95: known-positive control — the historical records still DO carry gemi
|
|||
);
|
||||
});
|
||||
|
||||
test('S95: trekplan high-effort Pass 2 degrades explicitly instead of failing', () => {
|
||||
const t = read('commands/trekplan.md');
|
||||
const section = t.split('\n### High-effort behavior (v5.1.1)\n')[1];
|
||||
assert.ok(section, 'trekplan.md must still carry the High-effort behavior sub-section');
|
||||
const head = section.split('\n## ')[0];
|
||||
assert.match(
|
||||
head, /unavailable, skipped/,
|
||||
'the dropped Pass 2 must read "unavailable, skipped" explicitly — a removed pass that is silent is indistinguishable from one that failed',
|
||||
);
|
||||
});
|
||||
|
||||
// End-state defect D-07: since v5.9.0 no command pins `model:`, so the orchestrator follows
|
||||
// the session model, and sub-agent models are resolved at the spawn sites (brief signal >
|
||||
// profile > agent frontmatter). No user-facing doc may claim otherwise. Fix the SOURCE.
|
||||
// the session model; a Voyage agent's spawn passes no model (its own `model: opus` applies)
|
||||
// and a built-in agent's spawn passes `model: "opus"`. No user-facing doc may claim
|
||||
// otherwise. Fix the SOURCE.
|
||||
test('D-07: no doc claims an opus orchestrator or ships a recipe for model: lines no command has', () => {
|
||||
const readme = read('README.md');
|
||||
assert.ok(
|
||||
|
|
@ -1304,15 +970,19 @@ test('D-07: no doc claims an opus orchestrator or ships a recipe for model: line
|
|||
}
|
||||
});
|
||||
|
||||
test('D-07: trekresearch launch rules inject the resolved model instead of a blanket opus', () => {
|
||||
test('D-07: trekresearch launch rules leave a Voyage agent on its own pin, with no blanket opus rule', () => {
|
||||
const t = read('commands/trekresearch.md');
|
||||
const rules = (t.split('\n### Launch rules\n')[1] || '').split('\n## ')[0];
|
||||
assert.ok(rules.length > 0, 'trekresearch.md must still carry the Launch rules sub-section');
|
||||
assert.ok(
|
||||
!/model: "opus" for all sub-agents/i.test(rules),
|
||||
'a blanket opus rule contradicts the file\'s own resolution (brief > profile > agent frontmatter, wired in v5.9.0)',
|
||||
'a blanket opus rule contradicts the file\'s own rule: a Voyage agent keeps its own pin, only a built-in agent gets opus',
|
||||
);
|
||||
assert.match(rules, /phase_signal_result\.model/, 'the launch rules must name the resolved model the spawn sites inject');
|
||||
const flatRules = rules.replace(/\s+/g, ' ');
|
||||
assert.ok(flatRules.includes("Pass no model at a Voyage agent's spawn: the agent's own frontmatter `model: opus` applies."),
|
||||
'the launch rules must say a Voyage agent\'s spawn passes no model');
|
||||
assert.ok(t.replace(/\s+/g, ' ').includes('Spawn it with `model: "opus"`: a built-in agent has no Voyage pin.'),
|
||||
'a built-in follow-up agent is spawned with model opus');
|
||||
});
|
||||
|
||||
// End-state defect D-03: a trekplan's `## Verification` is where the brief's success
|
||||
|
|
@ -1447,7 +1117,7 @@ test('trekexecute Phase 7 says NO_CRITERIA out loud instead of failing silently'
|
|||
// what is true now; historical records keep what was true when written and are never
|
||||
// rewritten. When an alias moves again, re-verify against the docs and fix the SOURCE.
|
||||
const ALIAS_LIVE_SURFACES = [
|
||||
'CLAUDE.md', 'README.md', 'docs/profiles.md', 'docs/operations.md',
|
||||
'CLAUDE.md', 'README.md', 'docs/architecture.md', 'docs/operations.md',
|
||||
...listMd('commands').map((f) => `commands/${f}`),
|
||||
...listMd('agents').map((f) => `agents/${f}`),
|
||||
];
|
||||
|
|
@ -1487,10 +1157,10 @@ test('model aliases: known-positive control — the same query still finds every
|
|||
assert.deepEqual(silent, [], `no known-positive left for: ${silent.join(', ')}`);
|
||||
});
|
||||
|
||||
test('model aliases: profiles.md states the current targets, defaults and session inheritance', () => {
|
||||
const t = read('docs/profiles.md');
|
||||
const axes = (t.split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
|
||||
assert.ok(axes.length > 0, 'docs/profiles.md must still carry the Model & effort axes section');
|
||||
test('model aliases: architecture.md states the current targets, defaults and session inheritance', () => {
|
||||
const t = read('docs/architecture.md');
|
||||
const axes = (t.split('\n## Model & effort\n')[1] || '').split('\n## ')[0];
|
||||
assert.ok(axes.length > 0, 'docs/architecture.md must still carry the Model & effort section');
|
||||
assert.match(axes, /Opus 5\.5[^\n]*\n?[^\n]*`medium`/, 'opus -> Opus 5.5, default effort `medium`');
|
||||
assert.match(axes, /Sonnet 5\.5/, 'sonnet -> Sonnet 5.5');
|
||||
assert.match(axes, /Fable 5\.1[\s\S]{0,120}`high`/, 'fable -> Fable 5.1, default effort `high`');
|
||||
|
|
@ -1499,7 +1169,8 @@ test('model aliases: profiles.md states the current targets, defaults and sessio
|
|||
assert.ok(!/overrides effort entirely/.test(axes),
|
||||
'`MAX_THINKING_TOKENS=0` has no effect on Opus 5.5, Sonnet 5.5 or the Fable models');
|
||||
assert.match(axes, /maxEffortLevel/, 'effort is capped by `maxEffortLevel` / an organization cap, not by `availableModels`');
|
||||
for (const f of ['CLAUDE.md', 'README.md']) {
|
||||
// README's alias sentence went with its §Cost profile (cleanup R-b).
|
||||
for (const f of ['CLAUDE.md']) {
|
||||
for (const target of ['Opus 5.5', 'Sonnet 5.5', 'Fable 5.1']) {
|
||||
assert.ok(read(f).includes(target), `${f} must name the current alias target ${target}`);
|
||||
}
|
||||
|
|
@ -1527,7 +1198,7 @@ const RETRIEVAL_MEDIUM = /retrieval(?: agents)?\s*(?:→|->|at)\s*`medium`/i;
|
|||
|
||||
test('effort floor: no live doc claims retrieval agents run at medium', () => {
|
||||
const hits = [];
|
||||
for (const f of ['docs/profiles.md', 'CLAUDE.md', 'README.md']) {
|
||||
for (const f of ['docs/architecture.md', 'CLAUDE.md', 'README.md']) {
|
||||
read(f).split('\n').forEach((l, i) => { if (RETRIEVAL_MEDIUM.test(l)) hits.push(`${f}:${i + 1}`); });
|
||||
}
|
||||
assert.deepEqual(hits, [], 'the four retrieval agents pin `high` now');
|
||||
|
|
@ -1540,29 +1211,10 @@ test('effort floor: known-positive control — the query finds the old phrases,
|
|||
assert.doesNotMatch('both with default reasoning effort `medium`', RETRIEVAL_MEDIUM);
|
||||
});
|
||||
|
||||
test('effort floor: profiles.md names the four retrieval agents as high', () => {
|
||||
const axes = (read('docs/profiles.md').split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
|
||||
test('effort floor: architecture.md names the four retrieval agents as high', () => {
|
||||
const axes = (read('docs/architecture.md').split('\n## Model & effort\n')[1] || '').split('\n## ')[0];
|
||||
const bullet = axes.split('\n- ').find((b) => ['task-finder', 'git-historian', 'dependency-tracer', 'architecture-mapper'].every((a) => b.includes(a)));
|
||||
assert.ok(bullet, 'one bullet in §Model & effort axes names all four retrieval agents');
|
||||
assert.ok(bullet, 'one bullet in §Model & effort names all four retrieval agents');
|
||||
assert.match(bullet, /`high`/);
|
||||
assert.doesNotMatch(bullet, /`medium`/);
|
||||
});
|
||||
|
||||
// The resolver returns `effort` only when the brief's phase signal sets one;
|
||||
// no command says it always does.
|
||||
const RESOLVER_COMMANDS = ['trekplan', 'trekexecute', 'trekresearch', 'trekreview'];
|
||||
|
||||
test('resolver: no command says the resolver always returns {effort, model, source}', () => {
|
||||
for (const f of readdirSync(join(ROOT, 'commands')).filter((x) => x.endsWith('.md'))) {
|
||||
assert.doesNotMatch(read(`commands/${f}`), /\{effort, model, source\}/, f);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolver: each resolver-using command says `effort` comes only from the brief signal, else standard', () => {
|
||||
for (const c of RESOLVER_COMMANDS) {
|
||||
const t = read(`commands/${c}.md`);
|
||||
const hits = t.match(/\{model, source\}[^\n]*\n?[^\n]*effort[^\n]*only when the brief's phase signal sets one/g) ?? [];
|
||||
assert.ok(hits.length >= 1, `${c}: no statement that effort is only present with a brief signal`);
|
||||
assert.match(t, /without it[^\n]*\n?[^\n]*`?standard`?/i, c);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,43 +91,8 @@ test('extension does NOT break REQUIRED_KEYS contract', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// v4.1 Step 3 — OPTIONAL_STRING_KEYS dispatch (profile_used)
|
||||
// v4.1 Step 3 — OPTIONAL_STRING_KEYS dispatch; its one key went in cleanup R-b.
|
||||
|
||||
test('OPTIONAL_STRING_KEYS exports profile_used', () => {
|
||||
assert.deepEqual(
|
||||
[...OPTIONAL_STRING_KEYS].sort(),
|
||||
['profile_used'].sort(),
|
||||
'OPTIONAL_STRING_KEYS export drift — pin contract',
|
||||
);
|
||||
});
|
||||
|
||||
test('profile_used: economy parses successfully (SC #10 forward-compat)', () => {
|
||||
const r = parseManifest(bodyWithExtras(' profile_used: economy'));
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.profile_used, 'economy');
|
||||
});
|
||||
|
||||
test('profile_used: numeric rejected with MANIFEST_OPTIONAL_TYPE', () => {
|
||||
const r = parseManifest(bodyWithExtras(' profile_used: 42'));
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'MANIFEST_OPTIONAL_TYPE');
|
||||
assert.ok(found, `expected MANIFEST_OPTIONAL_TYPE, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.match(found.message, /profile_used/);
|
||||
assert.match(found.message, /string/);
|
||||
});
|
||||
|
||||
test('absence of profile_used: field is NOT in parsed (NOT defaulted, unlike boolean)', () => {
|
||||
const r = parseManifest(bodyOnlyRequired());
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
// Absence semantics differ from boolean: parsed should NOT contain the key
|
||||
assert.equal('profile_used' in r.parsed, false,
|
||||
'profile_used must NOT be auto-defaulted when absent — string-key semantics');
|
||||
});
|
||||
|
||||
test('profile_used works alongside boolean optional keys (skip_commit_check + memory_write)', () => {
|
||||
const r = parseManifest(bodyWithExtras(' skip_commit_check: true\n memory_write: true\n profile_used: balanced'));
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.skip_commit_check, true);
|
||||
assert.equal(r.parsed.memory_write, true);
|
||||
assert.equal(r.parsed.profile_used, 'balanced');
|
||||
test('OPTIONAL_STRING_KEYS exports no key', () => {
|
||||
assert.deepEqual([...OPTIONAL_STRING_KEYS], [], 'OPTIONAL_STRING_KEYS export drift — pin contract');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { writeFileSync, unlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolvePhaseSignal, resolvePhaseSignalFromFile } from '../../lib/profiles/phase-signal-resolver.mjs';
|
||||
|
||||
const FULL_SIGNALS_FM = {
|
||||
phase_signals: [
|
||||
{ phase: 'research', effort: 'low', model: 'sonnet' },
|
||||
{ phase: 'plan', effort: 'standard' },
|
||||
{ phase: 'execute', effort: 'high', model: 'opus' },
|
||||
{ phase: 'review', effort: 'standard', model: 'sonnet' },
|
||||
],
|
||||
};
|
||||
|
||||
test('resolvePhaseSignal — returns {effort, model} for all 4 phases on full-signals brief', () => {
|
||||
for (const phase of ['research', 'plan', 'execute', 'review']) {
|
||||
const r = resolvePhaseSignal(FULL_SIGNALS_FM, phase);
|
||||
assert.ok(r && typeof r === 'object', `phase=${phase} should resolve non-null`);
|
||||
assert.ok(typeof r.effort === 'string', `phase=${phase} should have effort`);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — returns null when brief has no phase_signals', () => {
|
||||
const r = resolvePhaseSignal({ task: 'x' }, 'plan');
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — returns partial {effort} with model undefined when signal omits model', () => {
|
||||
const r = resolvePhaseSignal(FULL_SIGNALS_FM, 'plan');
|
||||
assert.equal(r.effort, 'standard');
|
||||
assert.equal(r.model, undefined);
|
||||
assert.ok(!('model' in r), 'model key should be absent when not in signal');
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — returns null when phase is not in PHASE_SIGNAL_PHASES', () => {
|
||||
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'brief'), null);
|
||||
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'continue'), null);
|
||||
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'nonsense'), null);
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — defensive: null/non-object input returns null', () => {
|
||||
assert.equal(resolvePhaseSignal(null, 'plan'), null);
|
||||
assert.equal(resolvePhaseSignal(undefined, 'plan'), null);
|
||||
assert.equal(resolvePhaseSignal('string', 'plan'), null);
|
||||
assert.equal(resolvePhaseSignal({ phase_signals: 'not-array' }, 'plan'), null);
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — drops model not in BASE_ALLOWED_MODELS (defense-in-depth gate)', () => {
|
||||
// MAJOR fix (S4): line that copies `model` must gate against the same
|
||||
// allowlist brief-validator uses (BASE_ALLOWED_MODELS = ['sonnet','opus','fable']),
|
||||
// mirroring how effort is gated against EFFORT_LEVELS. A brief that slipped
|
||||
// validation (hand-edited, validation skipped) must not hand a junk model
|
||||
// string to a command that then spawns an agent with `model: <junk>`.
|
||||
const fm = {
|
||||
phase_signals: [
|
||||
{ phase: 'plan', effort: 'high', model: 'gpt-4' }, // invalid model
|
||||
{ phase: 'review', effort: 'standard', model: 'haiku' }, // not in BASE_ALLOWED_MODELS
|
||||
],
|
||||
};
|
||||
const plan = resolvePhaseSignal(fm, 'plan');
|
||||
assert.equal(plan.effort, 'high', 'valid effort still resolves');
|
||||
assert.equal(plan.model, undefined, 'invalid model "gpt-4" must be dropped');
|
||||
assert.ok(!('model' in plan), 'model key absent when value not in allowlist');
|
||||
|
||||
const review = resolvePhaseSignal(fm, 'review');
|
||||
assert.equal(review.model, undefined, 'haiku must be dropped (not in BASE_ALLOWED_MODELS)');
|
||||
assert.ok(!('model' in review), 'model key absent for haiku');
|
||||
});
|
||||
|
||||
test('resolvePhaseSignal — keeps valid models (sonnet, opus, fable) after gating', () => {
|
||||
const fm = {
|
||||
phase_signals: [
|
||||
{ phase: 'research', effort: 'low', model: 'sonnet' },
|
||||
{ phase: 'execute', effort: 'high', model: 'opus' },
|
||||
{ phase: 'review', effort: 'high', model: 'fable' },
|
||||
],
|
||||
};
|
||||
assert.equal(resolvePhaseSignal(fm, 'research').model, 'sonnet');
|
||||
assert.equal(resolvePhaseSignal(fm, 'execute').model, 'opus');
|
||||
assert.equal(resolvePhaseSignal(fm, 'review').model, 'fable');
|
||||
});
|
||||
|
||||
test('resolvePhaseSignalFromFile + CLI shim — writes JSON to stdout, exit 0', () => {
|
||||
const fixture = join(tmpdir(), `phase-signal-test-${process.pid}.md`);
|
||||
writeFileSync(fixture, `---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
phase_signals:
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: opus
|
||||
---
|
||||
# x
|
||||
`);
|
||||
try {
|
||||
// Programmatic invocation
|
||||
const r = resolvePhaseSignalFromFile(fixture, 'plan');
|
||||
assert.deepEqual(r, { effort: 'high', model: 'opus' });
|
||||
// CLI shim
|
||||
const helperPath = new URL('../../lib/profiles/phase-signal-resolver.mjs', import.meta.url).pathname;
|
||||
const out = execFileSync('node', [helperPath, '--brief', fixture, '--phase', 'plan', '--json'], {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const parsed = JSON.parse(out.trim());
|
||||
assert.deepEqual(parsed, { effort: 'high', model: 'opus' });
|
||||
} finally {
|
||||
try { unlinkSync(fixture); } catch { /* swallow */ }
|
||||
}
|
||||
});
|
||||
|
|
@ -151,12 +151,13 @@ function paraAfter(rel, marker) {
|
|||
return flat(lines.slice(start, i).join('\n'));
|
||||
}
|
||||
|
||||
// ---- spawn model: phase_signal_result.model, the agent's own model: as the fallback ----
|
||||
// ---- spawn model: a Voyage agent gets no model:, a built-in agent gets opus ----
|
||||
|
||||
const PASS_MODEL = /Pass `model: \{phase_signal_result\.model\}` at this spawn; without it \(the `--brief` branch, where the resolver does not run, or a resolver that printed nothing\) the agent's own `model:` applies\./;
|
||||
const NO_MODEL = "This spawn passes no `model:`: the agent's own `model: opus` applies.";
|
||||
const BUILT_IN = 'Spawn it with `model: "opus"`: a built-in agent has no Voyage pin.';
|
||||
|
||||
test('spawn model: commands/trekplan.md pins no spawn to `model: "opus"`', () => {
|
||||
assert.doesNotMatch(PLAN_CMD, /model: "opus"/);
|
||||
test('spawn model: commands/trekplan.md pins `model: "opus"` once, at the built-in Explore deep-dive', () => {
|
||||
assert.equal(PLAN_CMD.split('model: "opus"').length - 1, 1, 'expected exactly one `model: "opus"` in commands/trekplan.md');
|
||||
});
|
||||
|
||||
for (const [where, marker] of [
|
||||
|
|
@ -164,32 +165,25 @@ for (const [where, marker] of [
|
|||
['### Always launch (Phase 5)', '### Always launch (all codebase sizes):'],
|
||||
['research-scout (Phase 5)', 'launch **research-scout** in'],
|
||||
['plan-critic and scope-guardian (Phase 9)', 'it traces into `## Verification`.'],
|
||||
['session-decomposer (Phase 1.5)', 'Launch the **session-decomposer** agent:'],
|
||||
]) {
|
||||
test(`spawn model: ${where}: the spawn takes phase_signal_result.model, the agent's own model: as the fallback`, () => {
|
||||
test(`spawn model: ${where}: the spawn passes no model:, the agent's own model: opus applies`, () => {
|
||||
const part = paraAfter('commands/trekplan.md', marker);
|
||||
assert.match(part, PASS_MODEL, `${where}: missing the resolved-model paragraph after "${marker}"`);
|
||||
assert.ok(part.includes(NO_MODEL), `${where}: missing the no-model sentence after "${marker}": ${part}`);
|
||||
assert.doesNotMatch(part, /model: "opus"/);
|
||||
});
|
||||
}
|
||||
|
||||
test('spawn model: session-decomposer (Phase 1.5): --decompose has no resolved model, so the spawn passes no model:', () => {
|
||||
const part = paraAfter('commands/trekplan.md', 'Launch the **session-decomposer** agent:');
|
||||
assert.ok(part.includes('Phase 1 runs the model resolver only for `--project`, so `--decompose` has no resolved model: this spawn '
|
||||
+ 'passes no `model:`, and the agent\'s own `model:` applies.'), `the session-decomposer paragraph does not say it passes no model: ${part}`);
|
||||
assert.ok(!part.includes('phase_signal_result'), 'the session-decomposer spawn still names phase_signal_result');
|
||||
assert.doesNotMatch(part, /model: "opus"/);
|
||||
});
|
||||
|
||||
test('spawn model: convention-scanner (Phase 5): the spawn takes phase_signal_result.model, the frontmatter as the fallback', () => {
|
||||
test('spawn model: convention-scanner (Phase 5): the spawn passes no model:, its own model: opus applies', () => {
|
||||
const part = paraAfter('commands/trekplan.md', 'use the `convention-scanner` plugin agent');
|
||||
assert.match(part, /\(model: phase_signal_result\.model, the agent's frontmatter as the fallback\)/);
|
||||
assert.ok(part.includes('(no `model:`; its own `model: opus` applies)'), part);
|
||||
assert.doesNotMatch(part, /model: "opus"/);
|
||||
});
|
||||
|
||||
test('spawn model: deep-dives (Phase 6): the spawn takes phase_signal_result.model, else opus', () => {
|
||||
test('spawn model: deep-dives (Phase 6): the built-in Explore agent is spawned with model opus', () => {
|
||||
const part = paraAfter('commands/trekplan.md', 'spawn a targeted deep-dive agent');
|
||||
assert.match(part, /\(model: phase_signal_result\.model, else opus — Explore is Claude Code's built-in agent and has no Voyage frontmatter to fall back to, subagent_type: "Explore"\)/);
|
||||
assert.doesNotMatch(part, /model: "opus"/);
|
||||
assert.ok(part.includes('(subagent_type: "Explore", Claude Code\'s built-in agent)'), part);
|
||||
assert.ok(part.includes(BUILT_IN), part);
|
||||
});
|
||||
|
||||
// ---- Phase 4 sizing: no turn budget, and the count it runs ---------------------
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@
|
|||
// can be checked offline: that each grader can see what it claims to see.
|
||||
//
|
||||
// - the new case for veikart steg 1's gate exists, and its fixture really
|
||||
// reaches the intent gate (passes the sequencing gate, has no marker)
|
||||
// - case 4 (plan-halts-without-phase-signals) can only be stopped by the
|
||||
// sequencing gate: its fixture carries a current intent approval
|
||||
// reaches the intent gate (passes the brief validator, has no marker)
|
||||
// - review-requires-project: the error line is written by CODE (the arg
|
||||
// parser), and the grader reads the parser's code from the trace
|
||||
// - grader weaknesses the PM re-measurement found (2026-09-23):
|
||||
|
|
@ -77,14 +75,13 @@ test('proevesett: a case exists for the steg 1 intent gate, with its graders', (
|
|||
const re = new RegExp(n.pattern);
|
||||
assert.match(inTrace({ valid: false, errors: [{ code: 'BRIEF_INTENT_NOT_APPROVED' }] }, 2), re);
|
||||
assert.doesNotMatch(COMMAND_PROSE, re, 'the pattern must not be satisfiable by quoting the command prose');
|
||||
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
|
||||
const noAgent = grader(`evals/${GATE_CASE}/graders/no-agent.md`);
|
||||
assert.deepEqual([noAgent.type, noAgent.tool, noAgent.min, noAgent.max], ['tool_used', 'Agent', '0', '0']);
|
||||
const noPlan = grader(`evals/${GATE_CASE}/graders/no-plan-file.md`);
|
||||
assert.deepEqual([noPlan.type, noPlan.path, noPlan.exists], ['file_exists', '**/plan.md', 'false']);
|
||||
});
|
||||
|
||||
test('proevesett: the gate case fixture passes the sequencing gate and stops at the intent gate', () => {
|
||||
test('proevesett: the gate case fixture passes the brief validator and stops at the intent gate', () => {
|
||||
const ws = mkdtempSync(join(tmpdir(), 'proevesett-'));
|
||||
try {
|
||||
const s = spawnSync('bash', [join(EVALS, GATE_CASE, 'scaffold.sh')], { cwd: ws, encoding: 'utf8' });
|
||||
|
|
@ -98,54 +95,6 @@ test('proevesett: the gate case fixture passes the sequencing gate and stops at
|
|||
} finally { rmSync(ws, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
// ---- case 4: only the sequencing gate may stop it ----
|
||||
// Measured 2026-09-23: with no intent marker the child sometimes ran the
|
||||
// intent gate first and halted there (PM: 6 of 12 on dc9b480, 7 of 10 on
|
||||
// 66e1fa1). The halt was right, but the case then measured the wrong gate.
|
||||
|
||||
const SEQ_CASE = 'plan-halts-without-phase-signals';
|
||||
|
||||
test('plan-halts-without-phase-signals: the fixture carries a current intent approval and fails only the sequencing gate', () => {
|
||||
const ws = mkdtempSync(join(tmpdir(), 'proevesett-'));
|
||||
try {
|
||||
const s = spawnSync('bash', [join(EVALS, SEQ_CASE, 'scaffold.sh')], { cwd: ws, encoding: 'utf8' });
|
||||
assert.equal(s.status, 0, s.stderr);
|
||||
const brief = join(ws, 'proj', 'brief.md');
|
||||
const g = spawnSync('node', [join(ROOT, 'lib/validators/intent-approval.mjs'), '--check', '--json', brief], { encoding: 'utf8' });
|
||||
assert.equal(g.status, 0, `the intent gate must pass so only the sequencing gate can stop the run: ${g.stdout}`);
|
||||
const v = spawnSync('node', [join(ROOT, 'lib/validators/brief-validator.mjs'), '--soft', '--json', brief], { encoding: 'utf8' });
|
||||
assert.deepEqual(JSON.parse(v.stdout).errors.map((e) => e.code), ['BRIEF_V51_MISSING_SIGNALS']);
|
||||
} finally { rmSync(ws, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('plan-halts-without-phase-signals: a halt at the intent gate fails the case', () => {
|
||||
assert.ok(graders(SEQ_CASE).includes('no-intent-halt.md'), graders(SEQ_CASE).join(','));
|
||||
const g = grader(`evals/${SEQ_CASE}/graders/no-intent-halt.md`);
|
||||
assert.deepEqual([g.type, g.target, g.match], ['regex', 'trace', 'not_contains']);
|
||||
const re = new RegExp(g.pattern);
|
||||
for (const code of ['BRIEF_INTENT_NOT_APPROVED', 'BRIEF_INTENT_APPROVAL_STALE', 'BRIEF_INTENT_APPROVAL_INVALID']) {
|
||||
assert.match(inTrace({ valid: false, errors: [{ code }] }, 2), re, `${code} must fail the case`);
|
||||
}
|
||||
assert.doesNotMatch(inTrace({ valid: true, errors: [], warnings: [] }, 2), re);
|
||||
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
|
||||
assert.doesNotMatch(COMMAND_PROSE, re, 'the command prose must not trip the grader');
|
||||
});
|
||||
|
||||
// Known-positive, measured 2026-09-23: with the check in brief-validator.mjs
|
||||
// disabled, the child still halted on the prose rule and named phase_signals
|
||||
// (3 of 3 passed). The halt alone does not show that the validator fired.
|
||||
test('plan-halts-without-phase-signals: the validator\'s own code must be in the trace', () => {
|
||||
assert.ok(graders(SEQ_CASE).includes('names-code.md'), graders(SEQ_CASE).join(','));
|
||||
const g = grader(`evals/${SEQ_CASE}/graders/names-code.md`);
|
||||
assert.deepEqual([g.type, g.target], ['regex', 'trace']);
|
||||
const re = new RegExp(g.pattern);
|
||||
assert.match(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
|
||||
assert.doesNotMatch(inTrace({ valid: true, errors: [], warnings: [] }, 2), re);
|
||||
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_INTENT_NOT_APPROVED' }] }, 2), re);
|
||||
assert.doesNotMatch(COMMAND_PROSE, re, 'the pattern must not be satisfiable by quoting the command prose');
|
||||
assert.doesNotMatch(read('lib/validators/brief-validator.mjs'), re, 'nor by reading the validator source');
|
||||
});
|
||||
|
||||
// ---- case 5: the missing-flag error comes from code ----
|
||||
|
||||
test('review-requires-project: the arg parser, run as trekreview.md spells it, prints the error (exit 1)', () => {
|
||||
|
|
|
|||
|
|
@ -1,244 +0,0 @@
|
|||
// tests/lib/profile-application.test.mjs
|
||||
// SC #5-#9 + backward-compat edge-case for lib/profiles/resolver.mjs.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
loadProfile,
|
||||
resolveProfile,
|
||||
resolveTrekcontinueProfile,
|
||||
validateProfileFile,
|
||||
findProfilePath,
|
||||
} from '../../lib/profiles/resolver.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..', '..');
|
||||
|
||||
// SC #5: loadProfile returns matrix-match for all 6 phase_models
|
||||
|
||||
test('SC #5: loadProfile("economy") returns flattened phase_models with all 6 phases', () => {
|
||||
const p = loadProfile('economy');
|
||||
assert.equal(p.name, 'economy');
|
||||
assert.equal(p.phase_models.brief, 'sonnet');
|
||||
assert.equal(p.phase_models.research, 'sonnet');
|
||||
assert.equal(p.phase_models.plan, 'sonnet');
|
||||
assert.equal(p.phase_models.execute, 'sonnet');
|
||||
assert.equal(p.phase_models.review, 'sonnet');
|
||||
assert.equal(p.phase_models.continue, 'sonnet');
|
||||
assert.equal(p.parallel_agents_min, 2);
|
||||
assert.equal(p.parallel_agents_max, 3);
|
||||
assert.equal(p.external_research_enabled, false);
|
||||
assert.equal(p.brief_reviewer_iter_cap, 1);
|
||||
});
|
||||
|
||||
test('SC #5: loadProfile("balanced") returns mixed phase_models', () => {
|
||||
const p = loadProfile('balanced');
|
||||
assert.equal(p.phase_models.plan, 'opus');
|
||||
assert.equal(p.phase_models.review, 'opus');
|
||||
assert.equal(p.phase_models.brief, 'sonnet');
|
||||
assert.equal(p.phase_models.execute, 'sonnet');
|
||||
});
|
||||
|
||||
test('SC #5: loadProfile("premium") returns all-opus', () => {
|
||||
const p = loadProfile('premium');
|
||||
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
|
||||
assert.equal(p.phase_models[phase], 'opus', `premium ${phase} should be opus`);
|
||||
}
|
||||
});
|
||||
|
||||
test('SC #5: loadProfile("fable") returns all-fable (BUILTIN_NAMES canary)', () => {
|
||||
// Throws PROFILE_NOT_FOUND if 'fable' regresses out of BUILTIN_NAMES —
|
||||
// the silent-fallback-to-premium failure mode this pin exists to catch.
|
||||
const p = loadProfile('fable');
|
||||
assert.equal(p.name, 'fable');
|
||||
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
|
||||
assert.equal(p.phase_models[phase], 'fable', `fable ${phase} should be fable`);
|
||||
}
|
||||
assert.equal(p.parallel_agents_min, 6);
|
||||
assert.equal(p.parallel_agents_max, 8);
|
||||
assert.equal(p.external_research_enabled, true);
|
||||
assert.equal(p.brief_reviewer_iter_cap, 3);
|
||||
});
|
||||
|
||||
test('SC #5: loadProfile throws PROFILE_NOT_FOUND for unknown profile', () => {
|
||||
try {
|
||||
loadProfile('does-not-exist-xyz');
|
||||
assert.fail('expected throw');
|
||||
} catch (e) {
|
||||
assert.equal(e.cause, 'PROFILE_NOT_FOUND');
|
||||
assert.match(e.message, /not found/);
|
||||
assert.ok(Array.isArray(e.attempted), 'should expose attempted paths');
|
||||
}
|
||||
});
|
||||
|
||||
// SC #6: env-var fallback flag > env > default
|
||||
|
||||
test('SC #6: resolveProfile flag > env > default', () => {
|
||||
// flag wins
|
||||
const r1 = resolveProfile({ flags: { '--profile': 'balanced' } }, { VOYAGE_PROFILE: 'economy' });
|
||||
assert.equal(r1.profile, 'balanced');
|
||||
assert.equal(r1.profile_source, 'flag');
|
||||
|
||||
// env wins when no flag
|
||||
const r2 = resolveProfile({ flags: {} }, { VOYAGE_PROFILE: 'economy' });
|
||||
assert.equal(r2.profile, 'economy');
|
||||
assert.equal(r2.profile_source, 'env');
|
||||
|
||||
// default when neither
|
||||
const r3 = resolveProfile({ flags: {} }, {});
|
||||
assert.equal(r3.profile, 'premium');
|
||||
assert.equal(r3.profile_source, 'default');
|
||||
});
|
||||
|
||||
// SC #7: performance — loadProfile 1000 iter < 50ms average (allowing some headroom)
|
||||
|
||||
test('SC #7: loadProfile 1000-iter performance < 50ms average', () => {
|
||||
const iterations = 1000;
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
loadProfile('economy');
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
const avgMs = elapsed / iterations;
|
||||
assert.ok(avgMs < 50, `loadProfile too slow: ${avgMs.toFixed(3)}ms average over ${iterations} iter`);
|
||||
});
|
||||
|
||||
// SC #8: custom.yaml from repo-root trumps ~/.claude/
|
||||
|
||||
test('SC #8: custom profile from <cwd>/voyage-profiles/<name>.yaml takes precedence over ~/.claude/', () => {
|
||||
const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-repo-'));
|
||||
const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-home-'));
|
||||
try {
|
||||
// Place custom profile in repo and home — repo should win
|
||||
mkdirSync(join(tmpRepo, 'voyage-profiles'), { recursive: true });
|
||||
mkdirSync(join(tmpHome, '.claude', 'voyage-profiles'), { recursive: true });
|
||||
|
||||
writeFileSync(join(tmpRepo, 'voyage-profiles', 'mycustom.yaml'),
|
||||
`---
|
||||
profile_version: "1.0"
|
||||
name: mycustom-repo
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: sonnet
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: sonnet
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 1
|
||||
parallel_agents_max: 2
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 1
|
||||
---
|
||||
`);
|
||||
writeFileSync(join(tmpHome, '.claude', 'voyage-profiles', 'mycustom.yaml'),
|
||||
`---
|
||||
profile_version: "1.0"
|
||||
name: mycustom-home
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: opus
|
||||
- phase: research
|
||||
model: opus
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: opus
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: opus
|
||||
parallel_agents_min: 1
|
||||
parallel_agents_max: 2
|
||||
external_research_enabled: true
|
||||
brief_reviewer_iter_cap: 3
|
||||
---
|
||||
`);
|
||||
|
||||
const found = findProfilePath('mycustom', { cwd: tmpRepo, home: tmpHome });
|
||||
assert.ok(found.path, `expected to find mycustom; attempted: ${found.attempted.join(', ')}`);
|
||||
assert.ok(found.path.startsWith(tmpRepo),
|
||||
`expected repo-rot win (path under ${tmpRepo}), got: ${found.path}`);
|
||||
|
||||
const p = loadProfile('mycustom', { cwd: tmpRepo, home: tmpHome });
|
||||
assert.equal(p.name, 'mycustom-repo', 'repo profile should win');
|
||||
assert.equal(p.phase_models.brief, 'sonnet');
|
||||
} finally {
|
||||
rmSync(tmpRepo, { recursive: true, force: true });
|
||||
rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('SC #8: missing profile error message includes both attempted paths', () => {
|
||||
const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-empty-'));
|
||||
const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-emptyhome-'));
|
||||
try {
|
||||
try {
|
||||
loadProfile('not-a-real-profile', { cwd: tmpRepo, home: tmpHome });
|
||||
assert.fail('expected throw');
|
||||
} catch (e) {
|
||||
assert.equal(e.cause, 'PROFILE_NOT_FOUND');
|
||||
// Both attempted paths should be in the error message for diagnostic clarity
|
||||
const msg = e.message;
|
||||
assert.match(msg, /voyage-profiles\/not-a-real-profile\.yaml/);
|
||||
assert.match(msg, /\.claude\/voyage-profiles\/not-a-real-profile\.yaml/);
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmpRepo, { recursive: true, force: true });
|
||||
rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// SC #9: resolveTrekcontinueProfile inheritance from plan-frontmatter
|
||||
|
||||
test('SC #9: resolveTrekcontinueProfile inherits from plan-frontmatter (profile: balanced)', () => {
|
||||
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md');
|
||||
const r = resolveTrekcontinueProfile(planPath, { flags: {} });
|
||||
assert.equal(r.profile, 'balanced');
|
||||
assert.equal(r.profile_source, 'inheritance');
|
||||
});
|
||||
|
||||
test('SC #9: resolveTrekcontinueProfile flag overrides plan-frontmatter (advisory)', () => {
|
||||
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md');
|
||||
const advisories = [];
|
||||
const fakeConsole = { error: (m) => advisories.push(m) };
|
||||
const r = resolveTrekcontinueProfile(planPath,
|
||||
{ flags: { '--profile': 'economy' } },
|
||||
{ console: fakeConsole });
|
||||
assert.equal(r.profile, 'economy');
|
||||
assert.equal(r.profile_source, 'flag');
|
||||
assert.equal(advisories.length, 1, 'expected one advisory message');
|
||||
assert.match(advisories[0], /balanced.*economy/);
|
||||
assert.match(advisories[0], /\[voyage\]/);
|
||||
});
|
||||
|
||||
// Backward-compat edge-case: v4.0-style plan WITHOUT profile field
|
||||
|
||||
test('Backward-compat: resolveTrekcontinueProfile on v4.0 plan without profile field returns default premium', () => {
|
||||
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-without-profile.md');
|
||||
const r = resolveTrekcontinueProfile(planPath, { flags: {} });
|
||||
assert.equal(r.profile, 'premium');
|
||||
assert.equal(r.profile_source, 'default');
|
||||
});
|
||||
|
||||
test('Backward-compat: resolveTrekcontinueProfile with non-existent plan path returns default premium', () => {
|
||||
const r = resolveTrekcontinueProfile('/tmp/does-not-exist-plan-xyz.md', { flags: {} });
|
||||
assert.equal(r.profile, 'premium');
|
||||
assert.equal(r.profile_source, 'default');
|
||||
});
|
||||
|
||||
// validateProfileFile re-export sanity
|
||||
|
||||
test('validateProfileFile re-exports validateProfile (locked-interface compat)', () => {
|
||||
const r = validateProfileFile(join(REPO_ROOT, 'lib', 'profiles', 'economy.yaml'));
|
||||
assert.equal(r.valid, true);
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// tests/lib/profile-flag-coverage.test.mjs
|
||||
// SC #4 (docs side): every command file must document --profile + VOYAGE_PROFILE.
|
||||
// /trekcontinue.md must additionally describe profile-arv (inheritance) policy.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const COMMANDS_DIR = join(__dirname, '..', '..', 'commands');
|
||||
|
||||
const COMMAND_FILES = [
|
||||
'trekbrief.md',
|
||||
'trekresearch.md',
|
||||
'trekplan.md',
|
||||
'trekexecute.md',
|
||||
'trekreview.md',
|
||||
'trekcontinue.md',
|
||||
];
|
||||
|
||||
for (const filename of COMMAND_FILES) {
|
||||
test(`${filename} documents --profile flag`, () => {
|
||||
const content = readFileSync(join(COMMANDS_DIR, filename), 'utf-8');
|
||||
assert.match(content, /--profile/,
|
||||
`${filename} must contain --profile flag documentation`);
|
||||
});
|
||||
|
||||
test(`${filename} mentions VOYAGE_PROFILE env-var`, () => {
|
||||
const content = readFileSync(join(COMMANDS_DIR, filename), 'utf-8');
|
||||
assert.match(content, /VOYAGE_PROFILE/,
|
||||
`${filename} must mention VOYAGE_PROFILE env-var (resolution order)`);
|
||||
});
|
||||
}
|
||||
|
||||
test('trekcontinue.md documents inheritance policy (profile arves fra plan-frontmatter)', () => {
|
||||
const content = readFileSync(join(COMMANDS_DIR, 'trekcontinue.md'), 'utf-8');
|
||||
assert.match(content, /inheritance/,
|
||||
'trekcontinue.md must describe profile-arv (inheritance) policy from plan-frontmatter');
|
||||
});
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
// tests/lib/profile-resolver.test.mjs
|
||||
// v5.1.1 SC5 — non-interference cases for resolvePhaseModel().
|
||||
// Verifies the new highest-priority lookup step (brief.phase_signals[phase].model)
|
||||
// wins over --profile flag and VOYAGE_PROFILE env; falls through cleanly when
|
||||
// no brief signal is present.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolvePhaseModel } from '../../lib/profiles/resolver.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..', '..');
|
||||
const FIXTURE = (name) => join(REPO_ROOT, 'tests', 'fixtures', name);
|
||||
|
||||
test('resolvePhaseModel — Case 1: brief signal wins over VOYAGE_PROFILE env', () => {
|
||||
// brief-effort-low.md pins all 4 phases to model: sonnet.
|
||||
// env says premium (would normally select opus). Brief must win.
|
||||
const r = resolvePhaseModel('research', FIXTURE('brief-effort-low.md'), [], { VOYAGE_PROFILE: 'premium' });
|
||||
assert.equal(r.model, 'sonnet', `brief signal should beat env; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.source, 'brief-signal');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — Case 2: brief signal wins over --profile flag', () => {
|
||||
// brief-effort-high.md pins all 4 phases to model: opus.
|
||||
// flag says economy (would normally select sonnet). Brief must win.
|
||||
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'economy'], {});
|
||||
assert.equal(r.model, 'opus', `brief signal should beat flag; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.source, 'brief-signal');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — Case 3: no phase_signals → fallthrough to --profile flag', () => {
|
||||
// brief-without-phase-signals fixture lacks phase_signals entirely.
|
||||
// --profile balanced is set. Should return balanced.phase_models.plan (= opus per yaml).
|
||||
const r = resolvePhaseModel('plan', FIXTURE('brief-without-phase-signals.md'), ['--profile', 'balanced'], {});
|
||||
assert.equal(r.model, 'opus', `balanced.plan should be opus; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.source, 'flag');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — Case 4: phase not in PHASE_SIGNAL_PHASES falls through gracefully', () => {
|
||||
// brief-effort-high.md has signals for the 4 supported phases.
|
||||
// Asking for 'continue' (not in PHASE_SIGNAL_PHASES) must fall through.
|
||||
// --profile premium is set, so continue resolves to premium.phase_models.continue (= opus).
|
||||
const r = resolvePhaseModel('continue', FIXTURE('brief-effort-high.md'), ['--profile', 'premium'], {});
|
||||
assert.equal(r.model, 'opus', `premium.continue should be opus; got ${JSON.stringify(r)}`);
|
||||
assert.ok(r.source !== 'brief-signal', 'continue must not resolve via brief-signal');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — Case 5 (defensive): missing brief file falls through cleanly', () => {
|
||||
// Non-existent path. Must not throw; must fall through to flag/env/default.
|
||||
const r = resolvePhaseModel('plan', '/nonexistent/brief.md', ['--profile', 'economy'], {});
|
||||
assert.equal(r.model, 'sonnet', 'economy.plan should be sonnet on fallthrough');
|
||||
assert.equal(r.source, 'flag');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — Case 6 (defensive): null briefPath falls through to default', () => {
|
||||
// null briefPath, no flag, no env → default = premium.
|
||||
const r = resolvePhaseModel('plan', null, [], {});
|
||||
assert.equal(r.model, 'opus', 'premium.plan default = opus');
|
||||
assert.equal(r.source, 'default');
|
||||
});
|
||||
|
||||
// v5.9 — fable profile composition (brief SC 4) + effort passthrough coherence
|
||||
|
||||
test('resolvePhaseModel — --profile fable: all six phases resolve model fable (no brief signal)', () => {
|
||||
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
|
||||
const r = resolvePhaseModel(phase, null, ['--profile', 'fable'], {});
|
||||
assert.equal(r.model, 'fable', `fable.${phase} should be fable; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.source, 'flag');
|
||||
}
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — brief signal (opus) beats --profile fable (composition precedence pin)', () => {
|
||||
// brief-effort-high.md pins execute to model: opus. Brief must beat the fable profile.
|
||||
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'fable'], {});
|
||||
assert.equal(r.model, 'opus', `brief signal should beat fable profile; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.source, 'brief-signal');
|
||||
});
|
||||
|
||||
test('resolvePhaseModel — composed output carries {effort, model} atomically (v5.9 passthrough)', () => {
|
||||
// brief-effort-high.md: execute → {effort: high, model: opus}. The composed
|
||||
// resolver must return BOTH fields from one call — the split-CLI design this
|
||||
// passthrough replaced could drift effort and model apart.
|
||||
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), [], {});
|
||||
assert.equal(r.effort, 'high', `effort must pass through; got ${JSON.stringify(r)}`);
|
||||
assert.equal(r.model, 'opus');
|
||||
assert.equal(r.source, 'brief-signal');
|
||||
});
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
// files (never tracked) and this test. A term is a plain substring, except a
|
||||
// flag, which must stand as its own token (`--engine`, `--profile`).
|
||||
//
|
||||
// When this test fails, remove the mention; add an EXEMPT entry only for a
|
||||
// line that must keep the term, with the reason.
|
||||
// Every group always runs. When this test fails, remove the mention; add an
|
||||
// EXEMPT entry only for a line that must keep the term, with the reason.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
@ -45,13 +45,25 @@ export const GROUPS = {
|
|||
],
|
||||
};
|
||||
|
||||
// Groups whose removal stage has not run yet. Each stage of R-b empties its
|
||||
// own entry; REMOVED_SURFACES_ALL=1 checks every group (the red proof).
|
||||
const PENDING = new Set(['R3']);
|
||||
|
||||
// Named exceptions: "path:line" -> reason. Each must still hit (a stale
|
||||
// exemption fails below).
|
||||
const EXEMPT = new Map([
|
||||
['docs/HANDOVER-CONTRACTS.md:81', 'public contract: an older brief\'s key is ignored (decisions 179/191)'],
|
||||
['docs/proevesett.md:139', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
||||
['docs/proevesett.md:143', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
||||
['docs/proevesett.md:146', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
||||
['lib/parsers/arg-parser.mjs:16', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:20', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:25', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:30', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:36', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:41', 'retired flag swallows its value'],
|
||||
['lib/parsers/arg-parser.mjs:46', 'retired flag swallows its value'],
|
||||
['tests/lib/arg-parser.test.mjs:285', 'retired flag swallows its value'],
|
||||
['tests/lib/arg-parser.test.mjs:286', 'retired flag swallows its value'],
|
||||
['tests/validators/brief-validator.test.mjs:200', 'older brief stays valid (decision 191)'],
|
||||
['tests/validators/brief-validator.test.mjs:203', 'older brief stays valid (decision 191)'],
|
||||
['tests/validators/brief-validator.test.mjs:205', 'older brief stays valid (decision 191)'],
|
||||
]);
|
||||
|
||||
const matches = (term, line) => (typeof term === 'string' ? line.includes(term) : term.test(line));
|
||||
|
|
@ -108,10 +120,7 @@ test('scope leaves out history, local files and this test', () => {
|
|||
});
|
||||
|
||||
for (const group of Object.keys(GROUPS)) {
|
||||
test(`${group}: no tracked file names a removed surface`, (t) => {
|
||||
if (PENDING.has(group) && process.env.REMOVED_SURFACES_ALL !== '1') {
|
||||
return t.skip(`${group} is not removed yet`);
|
||||
}
|
||||
test(`${group}: no tracked file names a removed surface`, () => {
|
||||
const all = tracked();
|
||||
assert.ok(all.length > 100, `git ls-files returned ${all.length} files; refusing to pass on an empty scan`);
|
||||
const hits = scan(ROOT, all, GROUPS[group]).filter(h => !EXEMPT.has(h));
|
||||
|
|
|
|||
|
|
@ -1043,7 +1043,7 @@ test('review 630 r9 / f8087ca4 criteria-runner.mjs:172: a check refused for its
|
|||
// (2) A brief SC with such a comment is BRIEF_SC_NOT_RUNNABLE, not prose-only.
|
||||
const brief = (sc) => [
|
||||
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
|
||||
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
|
||||
'## Success Criteria', '', sc, '',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
// tests/parsers/arg-parser-profile.test.mjs
|
||||
// SC #4: --profile valued flag MUST be recognized on all 6 voyage commands.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { parseArgs } from '../../lib/parsers/arg-parser.mjs';
|
||||
|
||||
const COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue'];
|
||||
|
||||
for (const cmd of COMMANDS) {
|
||||
test(`${cmd} — --profile economy parses as valued`, () => {
|
||||
const r = parseArgs('--profile economy', cmd);
|
||||
assert.equal(r.flags['--profile'], 'economy', `${cmd} should accept --profile economy`);
|
||||
assert.equal(r.errors.length, 0, `${cmd} should have no errors`);
|
||||
assert.equal(r.unknown.length, 0, `${cmd} should not mark --profile as unknown`);
|
||||
});
|
||||
}
|
||||
|
||||
test('trekplan — --profile without value emits ARG_MISSING_VALUE', () => {
|
||||
const r = parseArgs('--profile', 'trekplan');
|
||||
const missing = r.errors.find((e) => e.code === 'ARG_MISSING_VALUE');
|
||||
assert.ok(missing, 'must surface ARG_MISSING_VALUE');
|
||||
assert.match(missing.message, /--profile/);
|
||||
});
|
||||
|
||||
test('trekplan — --profile economy --quick combines correctly', () => {
|
||||
const r = parseArgs('--profile economy --quick', 'trekplan');
|
||||
assert.equal(r.flags['--profile'], 'economy');
|
||||
assert.equal(r.flags['--quick'], true);
|
||||
});
|
||||
|
||||
test('trekplan — --profile economy --gates open: --gates is unknown (parsed inline by command prose, not in FLAG_SCHEMA)', () => {
|
||||
// Edge case from plan.md Step 2 (per plan-critic minor): --gates is intentionally
|
||||
// NOT in FLAG_SCHEMA; commands parse it inline. Verify --profile still parses cleanly
|
||||
// and --gates ends up in unknown[] / positional[] rather than colliding with --profile.
|
||||
const r = parseArgs('--profile economy --gates open', 'trekplan');
|
||||
assert.equal(r.flags['--profile'], 'economy');
|
||||
// --gates is unknown to FLAG_SCHEMA; it lands in unknown[] and 'open' becomes positional
|
||||
assert.ok(r.unknown.includes('--gates'), `expected --gates in unknown, got: ${JSON.stringify(r.unknown)}`);
|
||||
assert.ok(r.positional.includes('open'), `expected 'open' as positional, got: ${JSON.stringify(r.positional)}`);
|
||||
});
|
||||
|
||||
test('trekexecute — --profile balanced --project /tmp/p combines correctly', () => {
|
||||
const r = parseArgs('--profile balanced --project /tmp/p', 'trekexecute');
|
||||
assert.equal(r.flags['--profile'], 'balanced');
|
||||
assert.equal(r.flags['--project'], '/tmp/p');
|
||||
});
|
||||
|
||||
test('trekcontinue — --profile premium parses without --project', () => {
|
||||
// trekcontinue had empty valued[] before v4.1 — sanity check the array is now extended
|
||||
const r = parseArgs('--profile premium', 'trekcontinue');
|
||||
assert.equal(r.flags['--profile'], 'premium');
|
||||
});
|
||||
|
|
@ -386,14 +386,12 @@ test('review-coordinator Pass 3 — only an engine finding is marked, and the on
|
|||
assert.match(p3, /verdict gate refuses \(exit 2\) every other marked or lowered shape/);
|
||||
});
|
||||
|
||||
test('review-coordinator — quick mode and high-effort normalization each still apply the brief-conflict rule', () => {
|
||||
test('review-coordinator — quick mode still applies the brief-conflict rule', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const pass3 = coord.indexOf('### Pass 3');
|
||||
assert.ok(pass3 >= 0, 'Pass 3 heading');
|
||||
const quick = cut(coord.slice(pass3), 'In `quick` mode, skip this pass entirely.', '\n\n', 'quick-mode paragraph');
|
||||
assert.match(quick, /brief-conflict rule[^.]*still applies/, 'the quick-mode paragraph says the rule still applies');
|
||||
const high = cut(coord, '**High-effort normalization (v5.1.1):**', '\n### ', 'high-effort paragraph');
|
||||
assert.match(high, /brief-conflict rule[^.]*still applies/, 'the high-effort paragraph says the rule still applies');
|
||||
});
|
||||
|
||||
test('review-coordinator Pass 2 — the trim rule judges a lowerable brief conflict as MINOR, inside its own sentence', () => {
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
---
|
||||
type: trekplan-jaccard-calibration
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-09
|
||||
status: parked-synthetic
|
||||
threshold: 0.55
|
||||
threshold_basis: "research/02 conservative starting value (arXiv:2412.12148)"
|
||||
empirical_runs: 0
|
||||
synthetic_runs: 4
|
||||
ramp_target: v4.2
|
||||
---
|
||||
|
||||
# Cross-tier Jaccard calibration — voyage v4.1
|
||||
|
||||
## Status: PARKED-SYNTHETIC
|
||||
|
||||
> **Profile linkage (S34/V30).** Because this calibration is still
|
||||
> parked-synthetic, `lib/profiles/economy.yaml` carries `experimental: true`
|
||||
> and every profile doc flags the `economy` row experimental. When the
|
||||
> empirical calibration in *How to replace* below lands (`status: empirical`),
|
||||
> drop `experimental: true` in the **same change** — the linkage is guarded by
|
||||
> `tests/lib/doc-consistency.test.mjs` ("economy experimental flag agrees with
|
||||
> parked-synthetic calibration status").
|
||||
|
||||
Empirical Jaccard calibration was deferred from v4.1 because the four
|
||||
required `/trekplan` invocations cost an estimated $60-120 of LLM-budget
|
||||
that was not authorized for the v4.1-execute-4b session. Per Step 17
|
||||
escalate-handler, this file documents:
|
||||
|
||||
1. The synthetic placeholder fixtures used by Step 18's smoke-test, and
|
||||
2. The pinned conservative threshold (`0.55`) from research/02.
|
||||
|
||||
## Threshold rationale
|
||||
|
||||
`threshold: 0.55` is pinned per research/02 (Recommendation #5):
|
||||
|
||||
> "There is no universal Jaccard threshold for cross-model plan
|
||||
> agreement. arXiv:2412.12148 reports 0.45–0.65 for n=10 task-pair
|
||||
> samples on coding tasks. We recommend a *conservative starting value
|
||||
> of 0.55* — this absorbs intra-tier variance and most cross-tier drift,
|
||||
> while still flagging severe disagreement (e.g. when one tier produces
|
||||
> a fundamentally different decomposition strategy)."
|
||||
|
||||
The 0.55 floor is enforced by `tests/integration/profile-jaccard-smoke.test.mjs`
|
||||
(Step 18) as a module-local constant `CROSS_TIER_JACCARD_FLOOR`. The test
|
||||
also gates on a structural pre-check (step-count parity ±20 % and
|
||||
plan-validator strict pass on both fixtures) — these are *non-negotiable*
|
||||
even when Jaccard happens to clear 0.55.
|
||||
|
||||
## Synthetic fixture pairs
|
||||
|
||||
The four parked-synthetic plan-runs in `tests/synthetic/`:
|
||||
|
||||
| run-A | run-B | jaccard (synthetic, normalized) |
|
||||
|-------|-------|---------------------------------|
|
||||
| profile-plan-run-economy-1.md | profile-plan-run-premium-1.md | 0.707 |
|
||||
| profile-plan-run-economy-1.md | profile-plan-run-premium-2.md | 0.707 |
|
||||
| profile-plan-run-economy-2.md | profile-plan-run-premium-1.md | 0.750 |
|
||||
| profile-plan-run-economy-2.md | profile-plan-run-premium-2.md | 0.750 |
|
||||
|
||||
Intra-tier (sanity): economy-1 × economy-2 = 0.935;
|
||||
premium-1 × premium-2 = 0.905. Intra-tier > cross-tier confirms the
|
||||
fixtures discriminate.
|
||||
|
||||
Min observed cross-tier (synthetic): 0.707. Min minus 0.05 buffer = 0.657.
|
||||
We pin `threshold: 0.55` — the lower of (research/02 conservative value)
|
||||
vs (min - 0.05 buffer). This is the same rule plan.md Step 17 prescribes:
|
||||
`floor(min(jaccard_values), 2) - 0.05` or `0.55`, whichever is lower.
|
||||
|
||||
Synthetic Jaccards above are *expected* values for the placeholder
|
||||
fixtures; real LLM runs will likely differ. The 0.55 pin remains valid
|
||||
across that uncertainty.
|
||||
|
||||
## When to replace these fixtures
|
||||
|
||||
Trigger empirical calibration when **any** of the following holds:
|
||||
|
||||
1. Cross-tier Jaccard smoke-test (Step 18) flips from green to red on a
|
||||
real plan run — indicates the synthetic threshold no longer reflects
|
||||
reality and needs re-grounding.
|
||||
2. v4.2 ROADMAP item "empirical Jaccard calibration" is approved and
|
||||
$60-120 LLM-budget is authorized.
|
||||
3. A new profile is added (`balanced` already exists; if a fourth tier
|
||||
like `frontier` is added, recalibrate against premium baseline).
|
||||
|
||||
## How to replace
|
||||
|
||||
1. Run `/trekplan --profile economy --brief examples/01-add-verbose-flag/brief.md`
|
||||
twice. Save each plan's `steps:` frontmatter to
|
||||
`profile-plan-run-economy-{1,2}.md` (overwrite synthetic content).
|
||||
Update `status: parked-synthetic` → `status: empirical`.
|
||||
2. Same for `--profile premium`, twice.
|
||||
3. Recompute the four cross-tier Jaccards. Update the table above.
|
||||
4. Repin threshold: `min(jaccard_values, 2) - 0.05` or 0.55, whichever
|
||||
lower. (Tighter is fine; do not loosen below 0.55.)
|
||||
5. Run `tests/integration/profile-jaccard-smoke.test.mjs` — must pass.
|
||||
6. Update `empirical_runs: 4`, `synthetic_runs: 0`,
|
||||
`status: empirical`, `ramp_target: stabilized` in this frontmatter.
|
||||
|
||||
## Fallback strategy in the meantime
|
||||
|
||||
Until real calibration is run, operators are advised to use the
|
||||
`balanced` profile (sonnet for most phases, opus for plan + review) as
|
||||
the lowest-risk choice. `balanced` was selected as the v4.1 default in
|
||||
`commands/trekplan.md` Phase 5.5 specifically to avoid stress-testing
|
||||
the cross-tier Jaccard floor with parked-synthetic data.
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-09
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: economy-1
|
||||
profile_used: economy
|
||||
status: parked-synthetic
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Document verbose changes in release notes"
|
||||
---
|
||||
|
||||
# Synthetic plan run economy-1 — Add --verbose flag to CLI (PARKED)
|
||||
|
||||
This fixture is a SYNTHETIC PLACEHOLDER for empirical Jaccard calibration
|
||||
that requires live LLM-budget ($60-120 for 4 plan-runs). Marked
|
||||
`status: parked-synthetic` per the Step 17 escalate-handler.
|
||||
|
||||
## Why parked
|
||||
|
||||
Per NEXT-SESSION-PROMPT.local.md fallback: "Hvis Step 17 LLM-budget
|
||||
blokkerer: dokumentér `economy`-Plan som `parked` i kalibrasjons-fil og
|
||||
fortsett med Step 18-19 ved bruk av `balanced` som lavterskel-profil."
|
||||
|
||||
The session running v4.1-execute-4b did not have authorization for live
|
||||
LLM invocation against `/trekplan --profile economy --brief
|
||||
examples/01-add-verbose-flag/brief.md`. Synthetic fixtures here represent
|
||||
the *shape* of what such a run would produce — a near-subset of the
|
||||
`premium` plan's steps (covering the same task surface) but with ~25 %
|
||||
fewer sub-verification entries (no edge-case-collision step, no security
|
||||
audit step, no PII test, no benchmark, etc).
|
||||
|
||||
## How this fixture is consumed
|
||||
|
||||
`tests/integration/profile-jaccard-smoke.test.mjs` (Step 18) reads the
|
||||
`steps` array from the frontmatter and pairs it with the corresponding
|
||||
`premium` fixtures to compute cross-tier Jaccard.
|
||||
|
||||
When real LLM budget is approved (deferred to v4.2), regenerate this
|
||||
fixture by running the actual command and overwriting the frontmatter
|
||||
`steps` array. Update `status: parked-synthetic` → `status: empirical`.
|
||||
|
||||
## Step-shape rationale
|
||||
|
||||
Economy profile uses sonnet for all phases (per
|
||||
`lib/profiles/economy.yaml`). Empirical observation from research/02:
|
||||
sonnet plans tend toward fewer verification entries, fewer edge-case
|
||||
branches, and slightly less granular decomposition than opus plans. The
|
||||
30 entries here represent the typical "skip the marginal sub-verification"
|
||||
behaviour while keeping wording aligned with what an opus run would
|
||||
produce on the same brief — modeling the realistic expectation that
|
||||
profile choice changes *what* steps get included more than *how* the
|
||||
included ones are phrased.
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-09
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: economy-2
|
||||
profile_used: economy
|
||||
status: parked-synthetic
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Add timestamp prefix in verbose log lines"
|
||||
---
|
||||
|
||||
# Synthetic plan run economy-2 — Add --verbose flag to CLI (PARKED)
|
||||
|
||||
Companion fixture to `profile-plan-run-economy-1.md`. Same `economy`
|
||||
profile, simulated as a second run of the same brief, with the final
|
||||
step replaced (release notes → timestamp prefix) to model intra-tier
|
||||
variance.
|
||||
|
||||
See `profile-plan-run-economy-1.md` for full parked-synthetic rationale.
|
||||
|
||||
## Intra-tier Jaccard
|
||||
|
||||
Economy-1 vs economy-2 share 29/30 step titles (final step differs);
|
||||
union = 31. Jaccard = 29/31 ≈ 0.935 — well above any reasonable
|
||||
cross-tier floor. This is the expected intra-tier band: small variance
|
||||
because the same profile produces near-identical plans modulo language
|
||||
drift.
|
||||
|
||||
When real LLM-budget runs replace this synthetic, the empirical
|
||||
intra-tier Jaccard is expected to land in the 0.85–0.95 band per
|
||||
research/02. Cross-tier (economy vs premium) is the discriminating
|
||||
measurement and is documented in `profile-jaccard-calibration.md`.
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-09
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: premium-1
|
||||
profile_used: premium
|
||||
status: parked-synthetic
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Add edge-case test for repeated --verbose flags"
|
||||
- "Add edge-case test for --verbose with --silent collision"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Add npm script for verbose mode debugging"
|
||||
- "Run security audit on logger dependency tree"
|
||||
- "Verify no PII leaks in verbose log output"
|
||||
- "Add manual test checklist to CONTRIBUTING.md"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Add unit test for cleanup logic"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Add timestamp prefix in verbose log lines"
|
||||
- "Add test for timestamp format"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Add benchmark for verbose log emission cost"
|
||||
- "Document benchmark methodology in PERF.md"
|
||||
---
|
||||
|
||||
# Synthetic plan run premium-1 — Add --verbose flag to CLI (PARKED)
|
||||
|
||||
This fixture is a SYNTHETIC PLACEHOLDER for empirical Jaccard calibration
|
||||
that requires live LLM-budget ($60-120 for 4 plan-runs). Marked
|
||||
`status: parked-synthetic` per the Step 17 escalate-handler.
|
||||
|
||||
## Why parked
|
||||
|
||||
Same rationale as `profile-plan-run-economy-1.md`. The session running
|
||||
v4.1-execute-4b did not have authorization for live LLM invocation. This
|
||||
fixture mirrors the existing baseline `plan-run-A.md` (40 steps, opus
|
||||
granularity) since premium profile uses opus for `plan` and `review`
|
||||
phases per `lib/profiles/premium.yaml`.
|
||||
|
||||
## Step-shape rationale
|
||||
|
||||
Premium profile uses opus for plan + review phases (per
|
||||
`lib/profiles/premium.yaml`). Empirical observation from research/02:
|
||||
opus plans tend toward finer-grained steps, more explicit verification
|
||||
entries, and richer edge-case decomposition than sonnet plans. The 40
|
||||
entries here capture the level of detail typical of an opus run.
|
||||
|
||||
## Cross-tier Jaccard pairing
|
||||
|
||||
Paired with `profile-plan-run-economy-1.md` and `-economy-2.md` in
|
||||
`tests/integration/profile-jaccard-smoke.test.mjs` (Step 18). Expected
|
||||
cross-tier Jaccard for the parked-synthetic run-pair is documented in
|
||||
`profile-jaccard-calibration.md`.
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-09
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: premium-2
|
||||
profile_used: premium
|
||||
status: parked-synthetic
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Add edge-case test for repeated --verbose flags"
|
||||
- "Add edge-case test for --verbose with --silent collision"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Add npm script for verbose mode debugging"
|
||||
- "Run security audit on logger dependency tree"
|
||||
- "Verify no PII leaks in verbose log output"
|
||||
- "Add manual test checklist to CONTRIBUTING.md"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Add unit test for cleanup logic"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Add timestamp prefix in verbose log lines"
|
||||
- "Add test for timestamp format"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Add benchmark for verbose log capture overhead"
|
||||
- "Document overhead methodology in PERF.md"
|
||||
---
|
||||
|
||||
# Synthetic plan run premium-2 — Add --verbose flag to CLI (PARKED)
|
||||
|
||||
Companion to `profile-plan-run-premium-1.md`. Same `premium` profile,
|
||||
simulated as a second run with two terminal steps replaced
|
||||
(emission cost / benchmark methodology → capture overhead / overhead
|
||||
methodology) to model intra-tier variance.
|
||||
|
||||
## Intra-tier Jaccard
|
||||
|
||||
Premium-1 vs premium-2 share 38/40 step titles; union = 42.
|
||||
Jaccard = 38/42 ≈ 0.905 — matches the existing baseline plan-run-A vs
|
||||
plan-run-B floor (≥ 0.833 in plan-determinism.test.mjs).
|
||||
|
||||
## Cross-tier Jaccard rationale
|
||||
|
||||
Pairing premium fixtures (40 steps) against economy fixtures (30 steps)
|
||||
yields ~30 shared titles (after string-normalisering), with union ~40.
|
||||
Conservative cross-tier Jaccard ≈ 30/40 = 0.75 in this synthetic — but
|
||||
the calibration file pins a *more conservative* floor (0.55) per
|
||||
research/02 to absorb empirical variance once real runs replace these
|
||||
fixtures. See `profile-jaccard-calibration.md` for threshold derivation.
|
||||
|
|
@ -27,7 +27,6 @@ project_dir: .claude/projects/2026-10-06-dark-mode-toggle/
|
|||
research_topics: 0
|
||||
research_status: complete
|
||||
framing: new-direction
|
||||
phase_signals_partial: true
|
||||
${extra}---
|
||||
|
||||
# Task
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ auto_research: false
|
|||
interview_turns: 5
|
||||
source: interview
|
||||
framing: new-direction
|
||||
phase_signals_partial: true
|
||||
---
|
||||
|
||||
# Task: JWT auth
|
||||
|
|
@ -116,28 +115,7 @@ Find what was missed.
|
|||
- All BLOCKER findings get a fix-plan.
|
||||
`;
|
||||
|
||||
// signals blocks
|
||||
const GOOD_SIGNALS = `phase_signals:
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: opus
|
||||
`;
|
||||
const BAD_PHASE_SIGNALS = `phase_signals:
|
||||
- phase: nonsense
|
||||
effort: high
|
||||
`;
|
||||
const BAD_EFFORT_SIGNALS = `phase_signals:
|
||||
- phase: plan
|
||||
effort: turbo
|
||||
`;
|
||||
const BAD_MODEL_SIGNALS = `phase_signals:
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: gpt4
|
||||
`;
|
||||
|
||||
// convenience builders
|
||||
const v21 = (s) => s.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
const withLine = (s, line) => s.replace('source: interview\n', `source: interview\n${line}`);
|
||||
|
||||
// ---- the canonical in-scope set + a class-balanced pair for each -------------
|
||||
|
|
@ -151,12 +129,6 @@ const IN_SCOPE_CODES = [
|
|||
'BRIEF_MISSING_SECTION',
|
||||
'BRIEF_INVALID_FRAMING',
|
||||
'BRIEF_MISSING_FRAMING',
|
||||
'BRIEF_V51_MISSING_SIGNALS',
|
||||
'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE',
|
||||
'BRIEF_INVALID_PHASE_SIGNALS',
|
||||
'BRIEF_INVALID_PHASE_SIGNAL_PHASE',
|
||||
'BRIEF_INVALID_EFFORT',
|
||||
'BRIEF_INVALID_MODEL',
|
||||
];
|
||||
|
||||
const CASES = {
|
||||
|
|
@ -192,30 +164,6 @@ const CASES = {
|
|||
fail: GOOD_BRIEF_22.replace('framing: new-direction\n', ''),
|
||||
pass: GOOD_BRIEF_22,
|
||||
},
|
||||
BRIEF_V51_MISSING_SIGNALS: {
|
||||
fail: v21(GOOD_BRIEF),
|
||||
pass: GOOD_BRIEF, // 2.0 — gate inactive
|
||||
},
|
||||
BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE: {
|
||||
fail: withLine(v21(GOOD_BRIEF), `phase_signals_partial: true\n${GOOD_SIGNALS}`),
|
||||
pass: withLine(v21(GOOD_BRIEF), 'phase_signals_partial: true\n'),
|
||||
},
|
||||
BRIEF_INVALID_PHASE_SIGNALS: {
|
||||
fail: withLine(GOOD_BRIEF, 'phase_signals: notalist\n'),
|
||||
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
|
||||
},
|
||||
BRIEF_INVALID_PHASE_SIGNAL_PHASE: {
|
||||
fail: withLine(v21(GOOD_BRIEF), BAD_PHASE_SIGNALS),
|
||||
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
|
||||
},
|
||||
BRIEF_INVALID_EFFORT: {
|
||||
fail: withLine(v21(GOOD_BRIEF), BAD_EFFORT_SIGNALS),
|
||||
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
|
||||
},
|
||||
BRIEF_INVALID_MODEL: {
|
||||
fail: withLine(v21(GOOD_BRIEF), BAD_MODEL_SIGNALS),
|
||||
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
|
||||
},
|
||||
};
|
||||
|
||||
test('gate coverage — CASES keys exactly match IN_SCOPE_CODES (no silent gap)', () => {
|
||||
|
|
|
|||
|
|
@ -157,127 +157,10 @@ test('validateBrief — wrong-type error message includes accepted set', () => {
|
|||
assert.ok(/trekreview/.test(wrongType.message));
|
||||
});
|
||||
|
||||
// --- v5.1 — phase_signals additive field + sequencing gate ---
|
||||
|
||||
const SIGNALS_BLOCK = `phase_signals:
|
||||
- phase: research
|
||||
effort: standard
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: execute
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: review
|
||||
effort: standard
|
||||
`;
|
||||
|
||||
test('validateBrief — v5.1 well-formed phase_signals accepted', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('validateBrief — pre-v5.1 brief without phase_signals accepted (backward-compat)', () => {
|
||||
const r = validateBriefContent(GOOD_BRIEF, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1+ brief missing phase_signals + partial emits BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1+ brief with phase_signals_partial: true accepted', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('validateBrief — phase_signals + phase_signals_partial both set rejected (mutually exclusive)', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\nphase_signals_partial: true\n${SIGNALS_BLOCK}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE'));
|
||||
});
|
||||
|
||||
test('validateBrief — phase_signals with unknown phase rejected', () => {
|
||||
const BAD_SIGNALS = `phase_signals:
|
||||
- phase: nonsense
|
||||
effort: standard
|
||||
`;
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\n${BAD_SIGNALS}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_PHASE_SIGNAL_PHASE'));
|
||||
});
|
||||
|
||||
// --- v5.1.1 regression: YAML-number bypass closed ---
|
||||
// Findings 3c834097 + df1435a2: v5.1.0 shipped with an unquoted `brief_version: 2.1`
|
||||
// template. parseScalar coerces unquoted "2.1" to Number 2.1, and the original gate
|
||||
// guarded `typeof === 'string'`, silently bypassing the sequencing check. v5.1.1
|
||||
// coerces via String() so both shapes trigger the gate.
|
||||
|
||||
test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 without signals triggers gate', () => {
|
||||
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: 2.1');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(
|
||||
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
||||
`gate must fire for unquoted brief_version: 2.1 (YAML Number); errors=${JSON.stringify(r.errors)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1.1: QUOTED brief_version "2.1" without signals triggers gate (regression guard)', () => {
|
||||
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is valid (positive case)', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: 2.1')
|
||||
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
// --- v5.9 — fable model tier (BASE_ALLOWED_MODELS widened to three values) ---
|
||||
|
||||
test('validateBrief — v5.9: fable phase_signals fixture accepted (no BRIEF_INVALID_MODEL)', () => {
|
||||
const t = readFileSync(new URL('../fixtures/brief-effort-fable.md', import.meta.url), 'utf-8');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.9: unknown model gpt5 in phase_signals rejected with BRIEF_INVALID_MODEL', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK.replace('model: opus', 'model: gpt5')}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL'),
|
||||
`expected BRIEF_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`);
|
||||
});
|
||||
|
||||
// --- v5.5 — framing enforcement + obligatory TL;DR (gated at brief_version ≥ 2.2) ---
|
||||
// Operator decision (S6, option A1): framing + TL;DR are hard BLOCKERs for briefs
|
||||
// declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat,
|
||||
// mirroring the phase_signals ≥ 2.1 precedent). The framing ENUM check fires on any
|
||||
// declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat).
|
||||
// The framing ENUM check fires on any
|
||||
// version when the field is present but malformed.
|
||||
|
||||
const GOOD_BRIEF_22 = `---
|
||||
|
|
@ -293,7 +176,6 @@ auto_research: false
|
|||
interview_turns: 5
|
||||
source: interview
|
||||
framing: new-direction
|
||||
phase_signals_partial: true
|
||||
---
|
||||
|
||||
# Task: JWT auth
|
||||
|
|
@ -315,6 +197,18 @@ What success looks like.
|
|||
- All tests pass.
|
||||
`;
|
||||
|
||||
// An older brief may still carry a phase_signals block (removed in cleanup R-b,
|
||||
// decision 191): the key is ignored, with no error and no warning.
|
||||
|
||||
test('validateBrief — a 2.2 brief that still carries phase_signals validates with no error or warning about it', () => {
|
||||
const t = GOOD_BRIEF_22.replace('framing: new-direction\n',
|
||||
'framing: new-direction\nphase_signals:\n - phase: plan\n effort: high\n - phase: brief\n effort: xhigh\n');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.deepEqual(r.errors, []);
|
||||
assert.deepEqual(r.warnings.filter(w => /signal/i.test(w.code + ' ' + w.message)), []);
|
||||
});
|
||||
|
||||
test('validateBrief — v5.5 well-formed 2.2 brief (framing + TL;DR) accepted', () => {
|
||||
const r = validateBriefContent(GOOD_BRIEF_22, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
|
|
@ -367,8 +261,7 @@ test('validateBrief — v5.5 TL;DR exceeding 5 lines emits BRIEF_TLDR_TOO_LONG w
|
|||
|
||||
test('validateBrief — v5.5 backward-compat: 2.1 brief without framing/TL;DR stays valid', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING'));
|
||||
|
|
@ -385,8 +278,7 @@ test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate'
|
|||
// S18 — opt-in --min-brief-version gate. Warns (never blocks) when a brief
|
||||
// declares a version below the caller's floor, sidestepping framing enforcement.
|
||||
const BRIEF_21 = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
|
||||
test('validateBrief — S18 min-version: older brief below floor warns but does not block', () => {
|
||||
const r = validateBriefContent(BRIEF_21, { strict: true, minBriefVersion: '2.2' });
|
||||
|
|
@ -457,7 +349,7 @@ const codes = (list) => list.map((e) => e.code);
|
|||
function briefWith(sections) {
|
||||
return [
|
||||
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
|
||||
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
|
||||
...sections,
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ auto_research: false
|
|||
interview_turns: 4
|
||||
source: interview
|
||||
framing: new-direction
|
||||
phase_signals_partial: true
|
||||
---
|
||||
|
||||
# Task: Dark-mode toggle
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
// tests/validators/plan-validator-profile-drift.test.mjs
|
||||
// SC #20 — MANIFEST_PROFILE_DRIFT warning per brief Assumptions block 7.
|
||||
// In strict mode, plan-validator must emit a warning (NOT an error) when a
|
||||
// step manifest's profile_used differs from the plan's frontmatter profile.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, '..', '..');
|
||||
|
||||
function loadFixture(name) {
|
||||
return readFileSync(resolve(ROOT, 'tests/fixtures', name), 'utf-8');
|
||||
}
|
||||
|
||||
const DRIFT_FIXTURE = loadFixture('plan-profile-drift.md');
|
||||
|
||||
test('drift detected in strict mode — emits MANIFEST_PROFILE_DRIFT warning, not error', () => {
|
||||
const r = validatePlanContent(DRIFT_FIXTURE, { strict: true });
|
||||
assert.equal(r.valid, true, `plan must remain valid; errors: ${JSON.stringify(r.errors)}`);
|
||||
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
|
||||
assert.equal(drift.length, 1, `expected 1 drift warning, got ${drift.length}: ${JSON.stringify(drift)}`);
|
||||
assert.match(drift[0].message, /step 2/, `warning message must reference step 2: ${drift[0].message}`);
|
||||
assert.match(drift[0].message, /premium/, 'warning must include offending profile_used value');
|
||||
assert.match(drift[0].message, /economy/, 'warning must include plan-level profile value');
|
||||
});
|
||||
|
||||
test('drift NOT detected in soft mode — strict gate honored', () => {
|
||||
const r = validatePlanContent(DRIFT_FIXTURE, { strict: false });
|
||||
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
|
||||
assert.equal(
|
||||
drift.length,
|
||||
0,
|
||||
'MANIFEST_PROFILE_DRIFT must only emit in strict mode (per brief assumption 7)',
|
||||
);
|
||||
});
|
||||
|
||||
test('matching profile — no drift warning emitted', () => {
|
||||
// Same fixture body, but rewrite step 2 profile_used to match plan profile.
|
||||
// Use /g to catch both the doc-comment mention and the actual manifest entry.
|
||||
const matching = DRIFT_FIXTURE.replace(/profile_used: premium/g, 'profile_used: economy');
|
||||
const r = validatePlanContent(matching, { strict: true });
|
||||
assert.equal(r.valid, true);
|
||||
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
|
||||
assert.equal(
|
||||
drift.length,
|
||||
0,
|
||||
`no drift expected when all step profile_used match plan profile; got ${JSON.stringify(drift)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('plan without frontmatter profile — no drift warnings emitted', () => {
|
||||
// If plan-level profile is absent, step-level profile_used can be anything
|
||||
// without triggering drift (drift is only meaningful relative to a baseline).
|
||||
const noProfile = DRIFT_FIXTURE.replace(/profile: economy\n/, '');
|
||||
const r = validatePlanContent(noProfile, { strict: true });
|
||||
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
|
||||
assert.equal(
|
||||
drift.length,
|
||||
0,
|
||||
'no plan-level profile means no baseline; drift detection must be silent',
|
||||
);
|
||||
});
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
// tests/validators/profile-validator.test.mjs
|
||||
// SC #1, #2, #3: profile-validator validates lib/profiles/{economy,balanced,premium,fable}.yaml
|
||||
// (innebygde profiler) plus rejects invalid models and invalid enum types.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import {
|
||||
validateProfile,
|
||||
validateProfileContent,
|
||||
PROFILE_REQUIRED_FIELDS,
|
||||
PROFILE_REQUIRED_PHASES,
|
||||
BASE_ALLOWED_MODELS,
|
||||
} from '../../lib/validators/profile-validator.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..', '..');
|
||||
|
||||
// SC #1: alle 4 innebygde profiler grønne
|
||||
|
||||
for (const profileName of ['economy', 'balanced', 'premium', 'fable']) {
|
||||
test(`SC #1: lib/profiles/${profileName}.yaml validates clean`, () => {
|
||||
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${profileName}.yaml`));
|
||||
assert.equal(r.valid, true,
|
||||
`expected valid, got errors: ${JSON.stringify(r.errors, null, 2)}`);
|
||||
assert.equal(r.errors.length, 0);
|
||||
// Spot-check: all 6 phases present
|
||||
const phases = r.parsed.frontmatter.phase_models.map(p => p.phase);
|
||||
for (const required of PROFILE_REQUIRED_PHASES) {
|
||||
assert.ok(phases.includes(required), `${profileName} missing phase: ${required}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// SC #2: PROFILE_INVALID_MODEL fired when phase_models[N].model not in allowlist
|
||||
|
||||
test('SC #2: profile-invalid-model.yaml rejected with PROFILE_INVALID_MODEL at phase_models[2].model', () => {
|
||||
const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-model.yaml'));
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL');
|
||||
assert.ok(found, `expected PROFILE_INVALID_MODEL, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.equal(found.location, 'phase_models[2].model',
|
||||
`expected location phase_models[2].model, got ${found.location}`);
|
||||
assert.match(found.message, /gpt-4/);
|
||||
});
|
||||
|
||||
// SC #3: PROFILE_INVALID_ENUM for wrong-type values
|
||||
|
||||
test('SC #3: profile-invalid-enum.yaml rejected with PROFILE_INVALID_ENUM (external_research_enabled is string)', () => {
|
||||
const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-enum.yaml'));
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_ENUM' && /external_research_enabled/.test(e.message));
|
||||
assert.ok(found, `expected PROFILE_INVALID_ENUM for external_research_enabled, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.match(found.message, /boolean/);
|
||||
});
|
||||
|
||||
// VOYAGE_ALLOW_HAIKU env-var allows haiku model
|
||||
|
||||
test('VOYAGE_ALLOW_HAIKU=1 allows haiku in phase_models', () => {
|
||||
const haikuProfile = `---
|
||||
profile_version: "1.0"
|
||||
name: haiku-allowed
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: haiku
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 2
|
||||
parallel_agents_max: 4
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 1
|
||||
---
|
||||
`;
|
||||
// Default env: haiku rejected
|
||||
const denied = validateProfileContent(haikuProfile, { env: { /* no VOYAGE_ALLOW_HAIKU */ } });
|
||||
assert.equal(denied.valid, false);
|
||||
const haikuErr = denied.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /haiku/.test(e.message));
|
||||
assert.ok(haikuErr, `expected haiku rejection: ${JSON.stringify(denied.errors)}`);
|
||||
assert.match(haikuErr.message, /VOYAGE_ALLOW_HAIKU/);
|
||||
|
||||
// With opt-in: haiku accepted
|
||||
const allowed = validateProfileContent(haikuProfile, { env: { VOYAGE_ALLOW_HAIKU: '1' } });
|
||||
assert.equal(allowed.valid, true,
|
||||
`expected valid with VOYAGE_ALLOW_HAIKU=1, got: ${JSON.stringify(allowed.errors)}`);
|
||||
});
|
||||
|
||||
// Fable tier (v5.9): fable accepted under DEFAULT env (no opt-in flag),
|
||||
// unknown models still rejected — the allowlist gate must demonstrably fire.
|
||||
|
||||
test('fable accepted in phase_models under default env (no env flag)', () => {
|
||||
const fableProfile = `---
|
||||
profile_version: "1.0"
|
||||
name: fable-inline
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: fable
|
||||
- phase: research
|
||||
model: fable
|
||||
- phase: plan
|
||||
model: fable
|
||||
- phase: execute
|
||||
model: fable
|
||||
- phase: review
|
||||
model: fable
|
||||
- phase: continue
|
||||
model: fable
|
||||
parallel_agents_min: 6
|
||||
parallel_agents_max: 8
|
||||
external_research_enabled: true
|
||||
brief_reviewer_iter_cap: 3
|
||||
---
|
||||
`;
|
||||
const r = validateProfileContent(fableProfile, { env: { /* default: no flags */ } });
|
||||
assert.equal(r.valid, true,
|
||||
`expected fable accepted under default env, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.equal(r.errors.length, 0);
|
||||
});
|
||||
|
||||
test('unknown model gpt5 rejected with PROFILE_INVALID_MODEL under default env', () => {
|
||||
const gpt5Profile = `---
|
||||
profile_version: "1.0"
|
||||
name: gpt5-inline
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: gpt5
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 2
|
||||
parallel_agents_max: 4
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 1
|
||||
---
|
||||
`;
|
||||
const r = validateProfileContent(gpt5Profile, { env: { /* default: no flags */ } });
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /gpt5/.test(e.message));
|
||||
assert.ok(found, `expected PROFILE_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`);
|
||||
});
|
||||
|
||||
// BASE_ALLOWED_MODELS allowlist drift-pin (mirrors the PROFILE_REQUIRED_FIELDS pin)
|
||||
|
||||
test('BASE_ALLOWED_MODELS export drift-pin', () => {
|
||||
assert.deepEqual(
|
||||
[...BASE_ALLOWED_MODELS],
|
||||
['sonnet', 'opus', 'fable'],
|
||||
'BASE_ALLOWED_MODELS contract drift — pin contract',
|
||||
);
|
||||
});
|
||||
|
||||
// Required fields presence
|
||||
|
||||
test('PROFILE_MISSING_FIELD when name absent', () => {
|
||||
const r = validateProfileContent(`---
|
||||
profile_version: "1.0"
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: opus
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: opus
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 2
|
||||
parallel_agents_max: 4
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 1
|
||||
---
|
||||
`);
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'PROFILE_MISSING_FIELD' && /name/.test(e.message));
|
||||
assert.ok(found, `expected PROFILE_MISSING_FIELD for name, got: ${JSON.stringify(r.errors)}`);
|
||||
});
|
||||
|
||||
// PROFILE_NOT_FOUND for missing file
|
||||
|
||||
test('PROFILE_NOT_FOUND for non-existent file', () => {
|
||||
const r = validateProfile('/tmp/does-not-exist-profile-xyz.yaml');
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'PROFILE_NOT_FOUND'));
|
||||
});
|
||||
|
||||
// REQUIRED_FIELDS frontmatter contract drift-pin
|
||||
|
||||
test('PROFILE_REQUIRED_FIELDS export drift-pin', () => {
|
||||
assert.deepEqual(
|
||||
[...PROFILE_REQUIRED_FIELDS].sort(),
|
||||
['brief_reviewer_iter_cap', 'external_research_enabled', 'name',
|
||||
'parallel_agents_max', 'parallel_agents_min', 'phase_models'].sort(),
|
||||
'PROFILE_REQUIRED_FIELDS contract drift — pin contract',
|
||||
);
|
||||
});
|
||||
|
||||
test('PROFILE_REQUIRED_PHASES export drift-pin', () => {
|
||||
assert.deepEqual(
|
||||
[...PROFILE_REQUIRED_PHASES].sort(),
|
||||
['brief', 'research', 'plan', 'execute', 'review', 'continue'].sort(),
|
||||
'PROFILE_REQUIRED_PHASES contract drift — pin contract',
|
||||
);
|
||||
});
|
||||
|
||||
// S34 (V30) — economy carries an explicit `experimental` marker.
|
||||
// The cross-tier Jaccard floor (0.55) rests on parked-synthetic fixtures;
|
||||
// empirical Step-17 calibration is deferred to v4.2 ($60-120 LLM budget,
|
||||
// unauthorized). Until a real calibration lands, `economy` is self-declared
|
||||
// experimental in the profile DATA — not only in prose — so the status
|
||||
// survives a docs rewrite. See tests/synthetic/profile-jaccard-calibration.md.
|
||||
|
||||
test('S34: economy.yaml declares experimental: true (uncalibrated Jaccard floor)', () => {
|
||||
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', 'economy.yaml'));
|
||||
assert.equal(r.valid, true, `economy.yaml should validate: ${JSON.stringify(r.errors)}`);
|
||||
assert.equal(r.parsed.frontmatter.experimental, true,
|
||||
'economy.yaml must carry experimental: true until the Jaccard floor is empirically calibrated (S34/V30)');
|
||||
});
|
||||
|
||||
test('S34: premium + balanced are NOT flagged experimental', () => {
|
||||
for (const name of ['premium', 'balanced']) {
|
||||
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${name}.yaml`));
|
||||
assert.equal(r.valid, true, `${name}.yaml should validate: ${JSON.stringify(r.errors)}`);
|
||||
assert.notEqual(r.parsed.frontmatter.experimental, true,
|
||||
`${name}.yaml must not be flagged experimental`);
|
||||
}
|
||||
});
|
||||
|
||||
test('S34: validator type-checks experimental as boolean (PROFILE_INVALID_ENUM)', () => {
|
||||
const bad = `---
|
||||
profile_version: "1.0"
|
||||
name: bad-experimental
|
||||
phase_models:
|
||||
- phase: brief
|
||||
model: sonnet
|
||||
- phase: research
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
model: sonnet
|
||||
- phase: execute
|
||||
model: sonnet
|
||||
- phase: review
|
||||
model: sonnet
|
||||
- phase: continue
|
||||
model: sonnet
|
||||
parallel_agents_min: 2
|
||||
parallel_agents_max: 3
|
||||
external_research_enabled: false
|
||||
brief_reviewer_iter_cap: 1
|
||||
experimental: "yes"
|
||||
---
|
||||
`;
|
||||
const r = validateProfileContent(bad);
|
||||
assert.equal(r.valid, false, 'experimental: "yes" (string) must be rejected');
|
||||
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_ENUM' && /experimental/.test(e.message));
|
||||
assert.ok(found, `expected PROFILE_INVALID_ENUM for experimental, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.match(found.message, /boolean/);
|
||||
});
|
||||
|
|
@ -171,7 +171,7 @@ test('runner: a failing verification is never verified end to end', () => {
|
|||
function brief(criteria) {
|
||||
return [
|
||||
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
|
||||
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
|
||||
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
|
||||
'## Success Criteria', '', ...criteria, '',
|
||||
].join('\n');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue