feat(voyage): S6 — v5.5 brief framing enforcement (brief_version 2.2)

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
This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 13:09:50 +02:00
commit 736ae55d66
10 changed files with 409 additions and 31 deletions

View file

@ -250,3 +250,111 @@ test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is
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)));
});