voyage/tests/lib/doc-consistency.test.mjs
Kjell Tore Guttormsen 056720e7f3 feat(voyage): S13 — RELEASE v5.5.0 (framing 2.2 badge + NW1–NW3 + W2/W3 roll-up)
The coordinated release held since S6. Bumps 5.1.1 → 5.5.0 across plugin.json,
package.json, package-lock.json, README badge, and CHANGELOG (operator-confirmed
version; matches the codebase-wide v5.4/v5.5 milestone labels). Additive — no
breaking change for existing consumers; new brief requirements gate only on
briefs that declare brief_version 2.2.

Lands:
- brief_version 2.2 framing enforcement (framing enum + memory-alignment dim +
  obligatory TL;DR), held since S6 — badge now bumped
- Handover 1 PUBLIC CONTRACT formalization (the unreleased "v5.4")
- W2: Opus 4.8 baseline + native effort: on 8 agents + resolver model-gate fix
- W3: exec-form hooks (CC-14) + disallowed-tools on trekexecute (CC-11)
- NW1 reviewer-output schema contract; NW2 --workflow opt-in (bake-off POSITIVE);
  NW3 synthesis-agent dormant (declined per measurement)

CC 2.1.130→181 dispositions (docs/cc-upgrade-2.1.181-decision-matrix.md §S13):
- CC-08: GH#36071 (hooks in headless) CLOSED AS NOT PLANNED, not fixed →
  in-prompt safety preamble retained
- CC-29/31: verified clean / aligned, no code change
- CC-07/12/13: DEFER confirmed (recorded, no code)
- CLAUDE.md root warning: ACCEPTED BY DESIGN (universal across marketplace,
  advisory only; repo/maintainer context, not shipped consumer context)

TDD: version-consistency + v5.5.0-entry tests added first (RED→GREEN).
Tests 695 → 697 (695 pass / 2 skip / 0 fail). `claude plugin validate` passes
(one accepted advisory warning).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
2026-06-18 18:12:51 +02:00

711 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/doc-consistency.test.mjs
// Pin invariants between prose (CLAUDE.md, README.md) and source files
// (agents/*.md, commands/*.md, templates/, settings.json).
//
// When this test fails, fix the source-of-truth — do NOT rewrite the test to
// hide drift. Borrowed pattern from llm-security commit 97c5c9d.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
function read(rel) { return readFileSync(join(ROOT, rel), 'utf-8'); }
function listMd(rel) { return readdirSync(join(ROOT, rel)).filter(f => f.endsWith('.md')); }
test('CLAUDE.md agents table row count == agents/*.md file count', () => {
const md = read('CLAUDE.md');
const agentFiles = listMd('agents');
const agentTable = md.split('## Agents')[1] || '';
const tableSection = agentTable.split('\n## ')[0];
const dataRows = tableSection
.split('\n')
.filter(l => l.startsWith('|') && !l.match(/^\|[\s-]+\|/) && !l.match(/^\|\s*Agent\s*\|/));
assert.equal(
dataRows.length,
agentFiles.length,
`Drift: ${agentFiles.length} agent files vs ${dataRows.length} CLAUDE.md table rows. ` +
`Sync agents/ ↔ CLAUDE.md.`,
);
});
test('CLAUDE.md commands table mentions every commands/*.md file', () => {
const md = read('CLAUDE.md');
const commandFiles = listMd('commands');
for (const f of commandFiles) {
const cmdName = `/${f.replace(/\.md$/, '')}`;
assert.ok(
md.includes(cmdName),
`commands/${f} not mentioned in CLAUDE.md (looked for ${cmdName})`,
);
}
});
test('every command frontmatter name matches its filename', () => {
for (const f of listMd('commands')) {
const text = read(`commands/${f}`);
const doc = parseDocument(text);
if (!doc.valid) continue;
const expected = f.replace(/\.md$/, '');
if (doc.parsed.frontmatter && doc.parsed.frontmatter.name !== undefined) {
assert.equal(
doc.parsed.frontmatter.name,
expected,
`commands/${f} frontmatter.name="${doc.parsed.frontmatter.name}" should be "${expected}"`,
);
}
}
});
test('templates/plan-template.md declares plan_version: 1.7', () => {
const tpl = read('templates/plan-template.md');
assert.match(tpl, /plan_version:\s*['"]?1\.7['"]?/);
});
test('commands/trekexecute.md still parses v1.7 plan schema', () => {
const cmd = read('commands/trekexecute.md');
const tpl = read('templates/plan-template.md');
const tplVersion = (tpl.match(/plan_version:\s*['"]?([\d.]+)['"]?/) || [])[1];
assert.ok(tplVersion, 'templates/plan-template.md missing plan_version');
assert.ok(
cmd.includes(`plan_version`) || cmd.includes(`Step N:`) || cmd.includes('### Step '),
'commands/trekexecute.md should reference v1.7 plan-schema parsing',
);
});
test('settings.json has only known top-level scopes after Spor 0 cleanup', () => {
const cfg = JSON.parse(read('settings.json'));
const known = ['trekplan', 'trekresearch'];
for (const k of Object.keys(cfg)) {
assert.ok(known.includes(k), `Unknown top-level scope in settings.json: ${k}`);
}
});
test('settings.json no longer carries vestigial exploration block', () => {
const cfg = JSON.parse(read('settings.json'));
assert.equal(cfg.trekplan?.exploration, undefined,
'exploration block was vestigial — should be deleted in v3.1.0 Spor 0');
assert.equal(cfg.trekplan?.agentTeam, undefined,
'agentTeam block was vestigial — should be deleted in v3.1.0 Spor 0');
});
test('CLAUDE.md mentions all six pipeline commands', () => {
// v4.1 Step 21 — added /trekcontinue to coverage (was 5/6 before).
// v5.0.0 — /trekrevise removed (bespoke playground retired); back to six.
const md = read('CLAUDE.md');
for (const c of [
'/trekbrief',
'/trekresearch',
'/trekplan',
'/trekexecute',
'/trekreview',
'/trekcontinue',
]) {
assert.ok(md.includes(c), `CLAUDE.md missing reference to ${c}`);
}
});
test('HANDOVER-CONTRACTS.md contains Handover 6 section', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
text.includes('## Handover 6'),
'docs/HANDOVER-CONTRACTS.md should document Handover 6 (review → plan)',
);
});
test('HANDOVER-CONTRACTS.md contains Handover 7 section (session-state)', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
text.includes('## Handover 7'),
'docs/HANDOVER-CONTRACTS.md should document Handover 7 (.session-state.local.json) ' +
'consumed by /trekcontinue',
);
assert.ok(
text.includes('.session-state.local.json'),
'Handover 7 section should name the artifact path',
);
});
test('review-validator has CLI shim', () => {
const text = read('lib/validators/review-validator.mjs');
assert.ok(
text.includes('import.meta.url === '),
'lib/validators/review-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) so commands can call it from Bash',
);
});
test('session-state-validator has CLI shim', () => {
const text = read('lib/validators/session-state-validator.mjs');
assert.ok(
text.includes('import.meta.url === '),
'lib/validators/session-state-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) so /trekcontinue can call it from Bash',
);
});
test('next-session-prompt-validator has CLI shim', () => {
const text = read('lib/validators/next-session-prompt-validator.mjs');
assert.ok(
text.includes('import.meta.url === '),
'lib/validators/next-session-prompt-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) so /trekcontinue Phase 1.5 can call it from Bash',
);
});
test('HANDOVER-CONTRACTS.md Handover 7 documents § Lifecycle subsection', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
const h7Start = text.indexOf('## Handover 7');
assert.ok(h7Start >= 0, 'Handover 7 heading missing');
const h7End = text.indexOf('## Stability summary', h7Start);
assert.ok(h7End > h7Start, 'Stability summary heading missing — could not bound Handover 7');
const h7 = text.slice(h7Start, h7End);
assert.ok(
h7.includes('Lifecycle'),
'Handover 7 section should include a § Lifecycle subsection (SC-5 stale-file principle)',
);
});
test('HANDOVER-CONTRACTS.md Handover 7 § Lifecycle names --cleanup and produced_by contract', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
const h7Start = text.indexOf('## Handover 7');
const h7End = text.indexOf('## Stability summary', h7Start);
const h7 = text.slice(h7Start, h7End);
assert.ok(
h7.includes('--cleanup'),
'Handover 7 § Lifecycle should mention --cleanup as the operator-invoked stale-file remover',
);
assert.ok(
h7.includes('produced_by'),
'Handover 7 § Lifecycle should document the produced_by frontmatter contract for NEXT-SESSION-PROMPT.local.md',
);
});
test('CLAUDE.md mentions /trekcontinue command', () => {
const md = read('CLAUDE.md');
assert.ok(
md.includes('/trekcontinue') || md.includes('trekcontinue'),
'CLAUDE.md should document /trekcontinue in the Commands table ' +
'(added in v3.3.0 alongside the new command file)',
);
});
test('rule-catalogue has exactly 12 entries', async () => {
const mod = await import('../../lib/review/rule-catalogue.mjs');
assert.strictEqual(
mod.RULE_CATALOGUE.length,
12,
'lib/review/rule-catalogue.mjs RULE_CATALOGUE size invariant: must be 12 (v1.0 baseline)',
);
});
test('headless-launch-template.md mirrors Phase 2.6 hardenings', () => {
const tpl = read('templates/headless-launch-template.md');
for (const needle of [
'GIT_OPTIONAL_LOCKS',
'--max-turns',
'--max-budget-usd',
'--append-system-prompt-file',
'SHARED_CONTEXT_FILE',
'SAFETY_PREAMBLE',
'git push origin',
'GH #36071',
'push-before-cleanup',
]) {
assert.ok(
tpl.includes(needle),
`templates/headless-launch-template.md should include "${needle}" (Step 10 mirrors Phase 2.6)`,
);
}
});
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
const cmd = read('commands/trekplan.md');
const orch = read('agents/planning-orchestrator.md');
// Single-message reinforcement appears in both (command + orchestrator)
assert.ok(
cmd.includes('single assistant message turn'),
'commands/trekplan.md Phase 9 should reinforce single-message parallel dispatch',
);
assert.ok(
orch.includes('single assistant message turn'),
'agents/planning-orchestrator.md Phase 6 should mirror the single-message parallel-dispatch contract',
);
// Dedup CLI shim is wired in both
assert.ok(
cmd.includes('plan-review-dedup.mjs'),
'commands/trekplan.md Phase 9 should call lib/review/plan-review-dedup.mjs after both reviewers complete',
);
assert.ok(
orch.includes('plan-review-dedup.mjs'),
'agents/planning-orchestrator.md Phase 6 should reference the dedup helper',
);
});
// --- 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(
cl,
/## v4\.1\.0\b/,
'CHANGELOG.md must include "## v4.1.0" entry per Keep-a-Changelog 1.1.0',
);
});
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
const phase8Start = cmd.indexOf('## Phase 8');
assert.ok(phase8Start >= 0, 'Phase 8 heading missing');
const phase8End = cmd.indexOf('## Phase 9', phase8Start);
assert.ok(phase8End > phase8Start, 'Phase 9 heading missing — could not bound Phase 8');
const phase8 = cmd.slice(phase8Start, phase8End);
// Required regex source-of-truth references
assert.ok(
phase8.includes('STEP_HEADING_REGEX'),
'Phase 8 should inline STEP_HEADING_REGEX so format contract survives without orchestrator-doc loading',
);
assert.ok(
phase8.includes('FORBIDDEN_HEADING_REGEX'),
'Phase 8 should inline FORBIDDEN_HEADING_REGEX (Step 7 — schema-drift seal)',
);
// Required validator self-check
assert.ok(
phase8.includes('plan-validator.mjs --strict'),
'Phase 8 should mandate post-write `plan-validator.mjs --strict` self-check',
);
// Forbidden-headings list (literal "FORBIDDEN" appears more than once: in regex const + in human-readable list)
assert.ok(
/FORBIDDEN/.test(phase8),
'Phase 8 should explicitly enumerate FORBIDDEN headings',
);
});
// --- v5.0.0 / v5.0.1 — bespoke playground removed; /playground invocation explicit ---
//
// v5.0.0 removed the bespoke playground SPA, /trekrevise, and Handover 8.
// v5.0.1 dropped the v5.0.0 stop-gap (scripts/render-artifact.mjs) and made
// the producing commands print a literal, copy-paste-ready /playground
// document-critique invocation instead. These pins lock both removals in
// AND pin the new copy-paste invocation as the operator-facing contract.
import { existsSync } from 'node:fs';
test('playground/ directory no longer exists (removed in v5.0.0)', () => {
assert.ok(
!existsSync(join(ROOT, 'playground')),
'plugins/voyage/playground/ should be deleted — the bespoke playground was retired in v5.0.0',
);
});
test('commands/trekrevise.md no longer exists (removed in v5.0.0)', () => {
assert.ok(
!existsSync(join(ROOT, 'commands/trekrevise.md')),
'/trekrevise was removed in v5.0.0 — its command file should be gone',
);
});
test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(!text.includes('## Handover 8'), 'Handover 8 section should be removed in v5.0.0');
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
});
test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => {
assert.ok(
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)',
);
});
test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => {
assert.ok(
existsSync(join(ROOT, 'scripts/annotate.mjs')),
'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML',
);
});
test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => {
// v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't
// afford annotation; v5.0.1 pointed at /playground document-critique (which
// pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2
// ships scripts/annotate.mjs — an operator-driven annotation surface where
// the OPERATOR clicks lines and writes their own notes. Pin the wiring.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
read(`commands/${f}`).includes('scripts/annotate.mjs'),
`commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`,
);
}
});
test('producing commands no longer print the v5.0.1 /playground document-critique line', () => {
// v5.0.1 told operators to copy-paste "/playground build a document-critique
// playground for X" — but that flow pre-generates Claude's suggestions. The
// operator asked for their own annotations, not a critique of Claude's.
// v5.0.2 removes that line from the producing commands' final report.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
!read(`commands/${f}`).includes('/playground build a document-critique'),
`commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`,
);
}
});
test('producing commands tell the operator the flow is THEIR own annotations', () => {
// Pin language: every producing command's prose must mention that the
// OPERATOR drives annotation, not Claude. Phrase variants are allowed
// ("YOUR OWN note", "operator drives", etc.) — we look for the operator-
// ownership signal.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
const text = read(`commands/${f}`);
assert.ok(
/YOUR OWN|operator drives|your own/i.test(text),
`commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`,
);
}
});
test('producing commands emit file:// link in final report (operator-UX contract, 2026-05-13)', () => {
// Operator runs Ghostty / iTerm2 / modern Terminal.app — all support cmd+click
// on file:// URLs. Producing commands MUST emit both forms: (a) plain file://
// line in the report block, (b) `open file://...` copy-pasteable command.
// Both must reference $ANNOT_HTML (absolute path from scripts/annotate.mjs).
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
const text = read(`commands/${f}`);
assert.ok(
/file:\/\/\{\$ANNOT_HTML\}/.test(text),
`commands/${f} must include "file://{$ANNOT_HTML}" plain URL in the final report block`,
);
assert.ok(
/open file:\/\/\{\$ANNOT_HTML\}/.test(text),
`commands/${f} must include "open file://{$ANNOT_HTML}" copy-pasteable command in the final report block`,
);
}
});
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
const pkg = JSON.parse(read('package.json'));
assert.equal(
pkg.scripts && pkg.scripts.render,
undefined,
'package.json scripts.render should remain gone',
);
});
test('CHANGELOG.md has v5.0.0 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry');
});
test('CHANGELOG.md has v5.0.1 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
});
test('CHANGELOG.md has v5.0.2 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry');
});
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');
});
test('CHANGELOG.md has v5.5.0 entry (S13 coordinated release)', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.5\.0\b/, 'CHANGELOG.md must include "## v5.5.0" entry — the coordinated release that lands brief_version 2.2 + NW1NW3');
});
test('version is consistent across plugin.json, package.json, README badge, and CHANGELOG top entry', () => {
// versjonssync invariant (CLAUDE.md rule): every file that names the plugin
// version must agree. Bumping one without the others is a release defect.
const pluginVersion = JSON.parse(read('.claude-plugin/plugin.json')).version;
const pkgVersion = JSON.parse(read('package.json')).version;
const badgeVersion = (read('README.md').match(/badge\/version-([\d.]+)-/) || [])[1];
const changelogTop = (read('CHANGELOG.md').match(/## v(\d+\.\d+\.\d+)\b/) || [])[1];
assert.ok(pluginVersion, 'plugin.json missing version');
assert.equal(pkgVersion, pluginVersion, `package.json version ${pkgVersion} != plugin.json ${pluginVersion}`);
assert.equal(badgeVersion, pluginVersion, `README badge ${badgeVersion} != plugin.json ${pluginVersion}`);
assert.equal(changelogTop, pluginVersion, `CHANGELOG top entry v${changelogTop} != plugin.json ${pluginVersion}`);
});
test('operational files no longer reference trekrevise (v5.0.0 removal)', () => {
// Templates, the touched command/orchestrator files, settings.json, and the
// handover-contracts doc must be fully scrubbed. CLAUDE.md / README.md are
// intentionally allowed to mention /trekrevise in their "removed in v5.0.0"
// prose — those are historical notes, not live references.
const targets = [
'settings.json',
'docs/HANDOVER-CONTRACTS.md',
'templates/plan-template.md', 'templates/trekbrief-template.md', 'templates/trekreview-template.md',
'commands/trekplan.md', 'commands/trekbrief.md', 'commands/trekreview.md',
'agents/planning-orchestrator.md',
];
for (const t of targets) {
assert.ok(
!read(t).includes('trekrevise'),
`${t} still references trekrevise — it was removed in v5.0.0`,
);
}
});
// --- v5.1 — phase_signals + brief_version 2.1 ---
test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => {
const t = read('templates/trekbrief-template.md');
assert.match(t, /^brief_version: "2\.2"$/m,
'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 (Decision B + gemini-bridge)');
});
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 + gemini-bridge 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 (gates_mode = closed)');
});
// --- 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).
test('v5.4 — HANDOVER-CONTRACTS.md Handover 1 heading is labeled (PUBLIC CONTRACT)', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.match(t, /^## Handover 1 —.*\(PUBLIC CONTRACT\)\s*$/m,
'Handover 1 heading must carry the (PUBLIC CONTRACT) label — symmetric with Handover 3 (EXTERNAL CONTRACT)');
});
test('v5.4 — Handover 1 declares the PUBLIC CONTRACT callout + breaking-for-downstream guarantee', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('Stability tier: PUBLIC CONTRACT'),
'Handover 1 must open with a "Stability tier: PUBLIC CONTRACT" callout');
assert.ok(t.includes('breaking change for every downstream consumer'),
'the callout must state that a brief-schema change is a breaking change for every downstream consumer');
});
test('v5.5 — brief_version 2.2 is current in the public-contract schema table (no stale 2.0/2.1)', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(!t.includes('`"2.0"` (current)'),
'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)');
assert.ok(!t.includes('`"2.1"` (current)'),
'Handover 1 schema table must not still mark brief_version "2.1" as current (stale — v5.5 bumped to 2.2)');
assert.ok(t.includes('`"2.2"` (current)'),
'Handover 1 schema table must mark brief_version "2.2" as current');
});
test('v5.4 — phase_signals stays optional: not promoted to required', () => {
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(t.includes('froze `2.1` as the public-contract baseline'),
'the Versioning note must record that v5.4 froze 2.1 as the public-contract baseline');
});
// --- v5.5 — framing enforcement: brief_version 2.2 (Handover 1 contract evolution) ---
// S6: the framing-alignment invariant (CLAUDE.md) ships as a controlled brief_version
// bump 2.1→2.2 adding two required-at-2.2 elements (framing field + ## TL;DR section),
// version-gated so 2.0/2.1 briefs stay valid. These pin the contract-doc evolution.
test('v5.5 — Handover 1 schema table adds the framing field row', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('| `framing` |'),
'HANDOVER-CONTRACTS must add a framing row to the Handover 1 schema table');
assert.ok(t.includes('preserve \\| refine \\| replace \\| new-direction'),
'the framing row must list the four canonical enum values');
});
test('v5.5 — Handover 1 documents the framing gate + required-at-2.2 TL;DR section', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('v5.5 framing gate'),
'validation-strategy table must add a v5.5 framing gate row');
assert.ok(/##\s*TL;DR.*required at `brief_version ≥ 2\.2`/.test(t) || t.includes('`## TL;DR` — **required at `brief_version ≥ 2.2`**'),
'body-invariants must mark ## TL;DR as required at brief_version ≥ 2.2');
});
test('v5.5 — Versioning note records the 2.2 framing-enforcement evolution', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('v5.5 → `2.2` (framing enforcement)'),
'the Versioning note must document the v5.5 → 2.2 framing-enforcement evolution');
});
test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2', () => {
const t = read('README.md');
assert.ok(t.includes('framing') && t.includes('brief_version 2.2'),
'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)');
});