Implements the CLAUDE.md cross-cutting invariant "brief framing must match operator intent" as a controlled brief_version 2.1->2.2 bump (operator option A1). Three defense layers, version-gated at >=2.2 so existing 2.0/2.1 briefs stay valid (forward + backward compatible), mirroring the phase_signals >=2.1 gate: - L1 framing: enum field (preserve|refine|replace|new-direction). Enum-checked on any version when present (BRIEF_INVALID_FRAMING); missing at >=2.2 -> BRIEF_MISSING_FRAMING. /trekbrief Phase 2.5 collects it BEFORE any brief prose (non-skippable, even in --quick). - L2 memory alignment: new brief-reviewer dimension 6 comparing brief Intent/Goal + framing against operator memory for explicit contradictions; degrades to score 5 (N/A) when no memory context is supplied. Wired into Phase 4e gate (memory_alignment.score >= 4). - L3 obligatory ## TL;DR (<=5 content lines) at >=2.2; soft cap -> BRIEF_TLDR_TOO_LONG warning. trekreview briefs are exempt from the framing/TL;DR gate. Handover 1 PUBLIC CONTRACT doc, README "What's new", and the CLAUDE.md invariant + agents table (brief-reviewer 5->6 dimensions) updated to 2.2 (schema axis only; plugin version badge + CHANGELOG remain S10). Iron Law followed: validator tests red->green first. Tests 586 -> 606 (+20, 604 pass / 2 skip). claude plugin validate passes (pre-existing CLAUDE.md root-context warning unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
360 lines
13 KiB
JavaScript
360 lines
13 KiB
JavaScript
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
|
|
|
const GOOD_BRIEF = `---
|
|
type: trekbrief
|
|
brief_version: "2.0"
|
|
created: 2026-04-30
|
|
task: "Add JWT auth to API"
|
|
slug: jwt-auth
|
|
project_dir: .claude/projects/2026-04-30-jwt-auth/
|
|
research_topics: 2
|
|
research_status: pending
|
|
auto_research: false
|
|
interview_turns: 5
|
|
source: interview
|
|
---
|
|
|
|
# Task: JWT auth
|
|
|
|
## Intent
|
|
|
|
Why this matters.
|
|
|
|
## Goal
|
|
|
|
What success looks like.
|
|
|
|
## Success Criteria
|
|
|
|
- All tests pass.
|
|
`;
|
|
|
|
test('validateBrief — happy path', () => {
|
|
const r = validateBriefContent(GOOD_BRIEF, { strict: true });
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
});
|
|
|
|
test('validateBrief — wrong type rejected', () => {
|
|
const t = GOOD_BRIEF.replace('type: trekbrief', 'type: notabrief');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_WRONG_TYPE'));
|
|
});
|
|
|
|
test('validateBrief — missing required field', () => {
|
|
const t = GOOD_BRIEF.replace(/^research_topics: 2\n/m, '');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_FIELD' && /research_topics/.test(e.message)));
|
|
});
|
|
|
|
test('validateBrief — bad research_status value', () => {
|
|
const t = GOOD_BRIEF.replace('research_status: pending', 'research_status: maybe');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_BAD_STATUS'));
|
|
});
|
|
|
|
test('validateBrief — state machine: research_topics > 0 + skipped without partial = error', () => {
|
|
const t = GOOD_BRIEF.replace('research_status: pending', 'research_status: skipped');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_STATE_INCOHERENT'));
|
|
});
|
|
|
|
test('validateBrief — state machine: skipped + brief_quality: partial = warning only', () => {
|
|
const t = GOOD_BRIEF
|
|
.replace('research_status: pending', 'research_status: skipped')
|
|
.replace('source: interview', 'source: interview\nbrief_quality: partial');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.ok(r.warnings.find(w => w.code === 'BRIEF_PARTIAL_SKIPPED'));
|
|
});
|
|
|
|
test('validateBrief — strict requires body sections', () => {
|
|
const t = GOOD_BRIEF.replace(/## Intent\n\nWhy this matters\.\n\n/, '');
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION'));
|
|
});
|
|
|
|
test('validateBrief — soft demotes section errors to warnings', () => {
|
|
const t = GOOD_BRIEF.replace(/## Goal\n\nWhat success looks like\.\n\n/, '');
|
|
const r = validateBriefContent(t, { strict: false });
|
|
assert.equal(r.valid, true);
|
|
assert.ok(r.warnings.find(w => w.code === 'BRIEF_MISSING_SECTION'));
|
|
});
|
|
|
|
test('validateBrief — missing frontmatter is hard error', () => {
|
|
const r = validateBriefContent('# just markdown\n\nno frontmatter\n');
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'FM_MISSING'));
|
|
});
|
|
|
|
const REVIEW_AS_BRIEF = `---
|
|
type: trekreview
|
|
task: "Review delivered trekreview v1.0"
|
|
slug: trekreview
|
|
project_dir: .claude/projects/2026-05-01-trekreview/
|
|
findings:
|
|
- 0123456789abcdef0123456789abcdef01234567
|
|
- fedcba9876543210fedcba9876543210fedcba98
|
|
---
|
|
|
|
# Review brief
|
|
|
|
## Intent
|
|
|
|
Adversarial review of delivered trekreview v1.0.
|
|
|
|
## Goal
|
|
|
|
Find what was missed.
|
|
|
|
## Success Criteria
|
|
|
|
- All BLOCKER findings get a fix-plan.
|
|
`;
|
|
|
|
test('validateBrief — trekreview type accepted with findings array', () => {
|
|
const r = validateBriefContent(REVIEW_AS_BRIEF, { strict: true });
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
});
|
|
|
|
test('validateBrief — trekreview without findings rejected (BRIEF_MISSING_FIELD)', () => {
|
|
const t = REVIEW_AS_BRIEF.replace(/findings:\n - 0123[\s\S]*?- fedcba[0-9a-f]+\n/, '');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(
|
|
r.errors.find(e => e.code === 'BRIEF_MISSING_FIELD' && /findings/.test(e.message)),
|
|
`expected BRIEF_MISSING_FIELD for findings; got ${JSON.stringify(r.errors)}`,
|
|
);
|
|
});
|
|
|
|
test('validateBrief — trekreview with findings as scalar (not array) rejected (BRIEF_BAD_FINDINGS_TYPE)', () => {
|
|
const t = REVIEW_AS_BRIEF.replace(
|
|
/findings:\n - 0123[\s\S]*?- fedcba[0-9a-f]+/,
|
|
'findings: not-an-array',
|
|
);
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_BAD_FINDINGS_TYPE'));
|
|
});
|
|
|
|
test('validateBrief — wrong-type error message includes accepted set', () => {
|
|
const t = REVIEW_AS_BRIEF.replace('type: trekreview', 'type: somethingelse');
|
|
const r = validateBriefContent(t);
|
|
assert.equal(r.valid, false);
|
|
const wrongType = r.errors.find(e => e.code === 'BRIEF_WRONG_TYPE');
|
|
assert.ok(wrongType);
|
|
assert.ok(/trekbrief/.test(wrongType.message));
|
|
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.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
|
|
// version when the field is present but malformed.
|
|
|
|
const GOOD_BRIEF_22 = `---
|
|
type: trekbrief
|
|
brief_version: "2.2"
|
|
created: 2026-06-18
|
|
task: "Add JWT auth to API"
|
|
slug: jwt-auth
|
|
project_dir: .claude/projects/2026-06-18-jwt-auth/
|
|
research_topics: 0
|
|
research_status: complete
|
|
auto_research: false
|
|
interview_turns: 5
|
|
source: interview
|
|
framing: new-direction
|
|
phase_signals_partial: true
|
|
---
|
|
|
|
# Task: JWT auth
|
|
|
|
## TL;DR
|
|
|
|
Net-new JWT auth; no prior brief to anchor against.
|
|
|
|
## Intent
|
|
|
|
Why this matters.
|
|
|
|
## Goal
|
|
|
|
What success looks like.
|
|
|
|
## Success Criteria
|
|
|
|
- All tests pass.
|
|
`;
|
|
|
|
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));
|
|
});
|
|
|
|
test('validateBrief — v5.5 framing enum: invalid value rejected on any version', () => {
|
|
const t = GOOD_BRIEF.replace('source: interview\n', 'source: interview\nframing: sideways\n');
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_FRAMING'));
|
|
});
|
|
|
|
test('validateBrief — v5.5 framing enum: all four canonical values accepted', () => {
|
|
for (const v of ['preserve', 'refine', 'replace', 'new-direction']) {
|
|
const t = GOOD_BRIEF_22.replace('framing: new-direction', `framing: ${v}`);
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, true, `framing=${v}: ${JSON.stringify(r.errors)}`);
|
|
}
|
|
});
|
|
|
|
test('validateBrief — v5.5 brief_version 2.2 missing framing rejected (BRIEF_MISSING_FRAMING)', () => {
|
|
const t = GOOD_BRIEF_22.replace('framing: new-direction\n', '');
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING'));
|
|
});
|
|
|
|
test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR rejected (strict)', () => {
|
|
const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, '');
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message)));
|
|
});
|
|
|
|
test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR demoted to warning (soft)', () => {
|
|
const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, '');
|
|
const r = validateBriefContent(t, { strict: false });
|
|
assert.ok(r.warnings.find(w => w.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(w.message)));
|
|
});
|
|
|
|
test('validateBrief — v5.5 TL;DR exceeding 5 lines emits BRIEF_TLDR_TOO_LONG warning', () => {
|
|
const longTldr = ['l1', 'l2', 'l3', 'l4', 'l5', 'l6'].join('\n');
|
|
const t = GOOD_BRIEF_22.replace('Net-new JWT auth; no prior brief to anchor against.', longTldr);
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.ok(
|
|
r.warnings.find(w => w.code === 'BRIEF_TLDR_TOO_LONG'),
|
|
`expected TL;DR-too-long warning; warnings=${JSON.stringify(r.warnings)}`,
|
|
);
|
|
});
|
|
|
|
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');
|
|
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'));
|
|
assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message)));
|
|
});
|
|
|
|
test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate', () => {
|
|
const r = validateBriefContent(REVIEW_AS_BRIEF, { strict: true });
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING'));
|
|
assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message)));
|
|
});
|