From 987e847ea17afdac6a800d9f99e748a575f79909 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 19 Jun 2026 13:33:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(voyage):=20S18=20=E2=80=94=20framing-harde?= =?UTF-8?q?ning=20(min-version=20gate=20+=20memory=20status=20+=20pre-2.2?= =?UTF-8?q?=20doc=20+=20flagship=20hedge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes devils-advocate audit #4–#6 (MAJOR×MED). Four additive, non-breaking changes to the v5.5 framing-alignment machinery: 1. --min-brief-version gate (audit #4). Opt-in version floor on /trekplan + /trekresearch, forwarded to brief-validator as --min-version. New opts.minBriefVersion warns BRIEF_VERSION_BELOW_MINIMUM (never blocks) when a brief declares a version below the floor; trekreview exempt; absent opt = no check. CLI shim parses --min-version and skips its value token in filePath detection. 2. memory_alignment.status field (audit #5). brief-reviewer now emits status: verified | n_a | contradictions so a score-5 N/A (no memory) is distinguishable from a score-5 verified-aligned brief — the score≥4 gate passes in both, status reveals whether the wrong-premise defense ran. 3. Document pre-2.2 = zero framing enforcement (audit #4). HANDOVER-CONTRACTS §Handover 1 now states the producer-elective hole + two remedies. Also fixes a stale "current is 2.1" line (current is 2.2). 4. Soften flagship overselling (audit #6). CLAUDE.md Context-Engineering principle now hedges that main-context relief is asserted-by-design, not measured (T1 PoC found Δ≈0); README carried no false claim to fix. TDD: 8 new tests written failing-first (5 validator, 1 trekbrief status pin, 2 doc-consistency cross-file pins). Suite 691→699 (697 pass / 2 skip / 0 fail). No flagship prose-pin added (deliberate, per S19 anti-bloat). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB --- CHANGELOG.md | 3 +- CLAUDE.md | 2 +- agents/brief-reviewer.md | 15 ++++++++-- commands/trekplan.md | 18 ++++++++++-- commands/trekresearch.md | 11 ++++++- docs/HANDOVER-CONTRACTS.md | 4 ++- docs/command-modes.md | 2 ++ lib/validators/brief-validator.mjs | 35 +++++++++++++++++++++-- tests/commands/trekbrief.test.mjs | 6 ++++ tests/lib/doc-consistency.test.mjs | 27 +++++++++++++++++ tests/validators/brief-validator.test.mjs | 35 +++++++++++++++++++++++ 11 files changed, 145 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 883553b..28cc703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ The coordinated release held since the framing work. Supersedes the unreleased i Three version-gated layers ship at `brief_version ≥ 2.2`, implementing the `CLAUDE.md` cross-cutting invariant "brief framing must match operator intent": - **`framing` enum** (`preserve | refine | replace | new-direction`) — required in frontmatter, collected in `/trekbrief` Phase 2.5 *before* any prose is written, non-skippable even in `--quick`. Enum-checked on any version (`BRIEF_INVALID_FRAMING`); missing at ≥ 2.2 → `BRIEF_MISSING_FRAMING`. -- **Memory-alignment dimension** — new `brief-reviewer` dimension 6 compares brief Intent/Goal + framing against operator memory, flagging *explicit* contradictions only (scores N/A when no memory is supplied); wired to the Phase 4e gate (`memory_alignment.score ≥ 4`). +- **Memory-alignment dimension** — new `brief-reviewer` dimension 6 compares brief Intent/Goal + framing against operator memory, flagging *explicit* contradictions only (scores N/A when no memory is supplied); wired to the Phase 4e gate (`memory_alignment.score ≥ 4`). Emits a `status` field (`verified | n_a | contradictions`) so a score-5 N/A (no memory available) is distinguishable from a score-5 verified-aligned brief — the gate passes in both cases, and `status` is what tells the operator whether the wrong-premise defense actually ran. - **Obligatory `## TL;DR`** — ≤ 5 content lines at the top of every 2.2 brief (soft cap → `BRIEF_TLDR_TOO_LONG`). +- **Opt-in version floor** — `--min-brief-version ` on `/trekplan` + `/trekresearch` (forwarded to the validator as `--min-version`) raises a `BRIEF_VERSION_BELOW_MINIMUM` *warning* — never a block — when a brief declares a version below the floor. Closes the producer-elective hole where a `2.0`/`2.1` brief silently sidesteps framing enforcement; `docs/HANDOVER-CONTRACTS.md` §Handover 1 now documents pre-2.2 = zero framing enforcement and the two remedies (require `2.2` upstream, or pass the floor flag). Existing `2.0`/`2.1` briefs stay valid (forward + backward compatible), mirroring the `phase_signals ≥ 2.1` precedent. `trekreview` briefs are exempt. diff --git a/CLAUDE.md b/CLAUDE.md index a46a184..ede50c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execute, review, continue. Deep implementation planning and research with specialized agent swarms, external research, adversarial review, session decomposition, disciplined execution, and headless support. -**Design principle: Context Engineering** — build the right context by orchestrating specialized agents. Each step in the pipeline (brief → research → plan → execute) produces a structured artifact that the next step consumes. +**Design principle: Context Engineering** — build the right context by orchestrating specialized agents. Each step in the pipeline (brief → research → plan → execute) produces a structured artifact that the next step consumes. (The claim that fanning out to a swarm *relieves* the main context is asserted-by-design, not measured end-to-end: the one delegation actually measured — the Phase-7 synthesis PoC — found Δ main-context ≈ 0, see `docs/T1-synthesis-poc-results.md`. The structured-artifact handoffs are the load-bearing benefit; main-context relief is not yet demonstrated.) > **v3.0.0 — architect step extracted from this plugin.** The plan command still auto-discovers `architecture/overview.md` if present, so any compatible producer (architect plugin no longer publicly distributed; the architecture/overview.md slot remains available for any compatible producer) plugs into the same slot. See [CHANGELOG.md](CHANGELOG.md) for migration history. diff --git a/agents/brief-reviewer.md b/agents/brief-reviewer.md index 2d988c6..1eead8d 100644 --- a/agents/brief-reviewer.md +++ b/agents/brief-reviewer.md @@ -164,9 +164,17 @@ The caller MAY supply operator memory excerpts or file paths in your prompt. - **If memory context IS supplied:** compare the brief — especially `## Intent`, `## Goal`, and the frontmatter `framing:` value — against those facts. Report **EXPLICIT contradictions only** (a brief claim that a memory fact directly - negates), never vibes or soft mismatches. -- **If NO memory context is supplied:** score `5`, set `contradictions: []`, and - note "no memory context supplied". Do not speculate or invent contradictions. + negates), never vibes or soft mismatches. With no contradiction found, set + `status: "verified"`; with one or more, set `status: "contradictions"`. +- **If NO memory context is supplied:** score `5`, set `contradictions: []`, + `status: "n_a"`, and note "no memory context supplied". Do not speculate or + invent contradictions. + +The `status` field is load-bearing: a `score: 5` alone is ambiguous because it is +emitted both when memory was checked and aligned (`verified`) and when no memory +was available to check (`n_a`). Downstream gates that read `memory_alignment.score +≥ 4` therefore pass in *both* cases — `status` is what tells the operator whether +the wrong-premise defense actually ran or was simply absent. Flag as **memory-misaligned** if: - The frontmatter `framing:` value contradicts memory (e.g. `framing: preserve` @@ -256,6 +264,7 @@ information that would strengthen the brief. List only if actionable.} }, "memory_alignment": { "score": 1-5, + "status": "verified | n_a | contradictions", "contradictions": [ { "brief_claim": "{quoted brief text}", "memory_fact": "{quoted memory fact}", "file": "{source file}" } ] diff --git a/commands/trekplan.md b/commands/trekplan.md index 86effa8..c058d80 100644 --- a/commands/trekplan.md +++ b/commands/trekplan.md @@ -63,8 +63,11 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence: - Set **project_dir = {dir}**, **brief_path = {dir}/brief.md**. - **Validate inputs** (soft mode — warnings do not block, errors do): ```bash - # Brief schema sanity check (frontmatter + state machine, soft on body sections) - node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md" + # Brief schema sanity check (frontmatter + state machine, soft on body sections). + # When --min-brief-version was passed, append --min-version {min_brief_version} + # so the validator emits BRIEF_VERSION_BELOW_MINIMUM (warn, never block) for an + # older brief that sidesteps framing enforcement. + node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json [--min-version {min_brief_version}] "{dir}/brief.md" # v5.1.1 — resolve per-phase brief-signal for plan phase. Result is # captured as phase_signal_result and used at Agent-spawn sites below @@ -120,7 +123,15 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence: autonomy-gate state machine via the CLI shim: `node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`. -9. If neither `--brief` nor `--project` is present after flag parsing, +9. **`--min-brief-version `** — (optional) version floor. Set + **min_brief_version = {ver}** (e.g. `2.2`). Forwarded to the brief-validator + as `--min-version {ver}`; when the brief declares an older version it emits a + `BRIEF_VERSION_BELOW_MINIMUM` **warning** (never blocks) — framing enforcement + only fires at `≥ 2.2`, so an older brief sidesteps the framing-alignment + defense silently. Absent → no version check. See `docs/HANDOVER-CONTRACTS.md` + §Handover 1 for the pre-2.2 enforcement hole this guards. + +10. If neither `--brief` nor `--project` is present after flag parsing, output usage and stop: ``` @@ -140,6 +151,7 @@ Modes: --research Add up to 3 extra research briefs as planning context --fg No-op alias (foreground is the only mode as of v2.4.0) --quick Skip exploration agent swarm; plan directly + --min-brief-version Warn (never block) if the brief is older than (e.g. 2.2) --export Generate shareable output from an existing plan (no new planning) --decompose Split an existing plan into self-contained headless sessions diff --git a/commands/trekresearch.md b/commands/trekresearch.md index 6e3897f..08bc064 100644 --- a/commands/trekresearch.md +++ b/commands/trekresearch.md @@ -60,7 +60,9 @@ Supported flags: at Agent-spawn sites in Phase 4 to inject the brief-resolved model: ```bash - node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md" + # When --min-brief-version was passed, append --min-version {min_brief_version} + # so an older brief raises BRIEF_VERSION_BELOW_MINIMUM (warn, never block). + node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json [--min-version {min_brief_version}] "{dir}/brief.md" node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/phase-signal-resolver.mjs --brief "{dir}/brief.md" --phase research --json ``` @@ -71,6 +73,13 @@ Supported flags: state machine via the CLI shim: `node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`. +7. `--min-brief-version ` — (optional) version floor for an attached + `--project` brief. Set **min_brief_version = {ver}** (e.g. `2.2`). Forwarded + to the brief-validator below as `--min-version {ver}`; an older brief emits a + `BRIEF_VERSION_BELOW_MINIMUM` **warning** (never blocks) because framing + enforcement only fires at `≥ 2.2`. Absent → no version check. See + `docs/HANDOVER-CONTRACTS.md` §Handover 1 for the pre-2.2 enforcement hole. + Flags can be combined: - `--local` — local-only research - `--external --quick` — external-only, lightweight diff --git a/docs/HANDOVER-CONTRACTS.md b/docs/HANDOVER-CONTRACTS.md index 653c474..f15642d 100644 --- a/docs/HANDOVER-CONTRACTS.md +++ b/docs/HANDOVER-CONTRACTS.md @@ -96,10 +96,12 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference **State machine** detail: a brief that says it has research topics but skipped them must explicitly admit it (via `brief_quality: partial`). This is the most common failure mode the validator catches. -**Versioning:** current is `2.1` (v5.1 — adds optional `phase_signals` + `phase_signals_partial`). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.5.0 contract formalization **established `2.1` as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change under the protocol above — explicitly *not* part of the v5.5.0 formalization. +**Versioning:** the `2.1` schema axis (v5.1 — adds optional `phase_signals` + `phase_signals_partial`; current is `2.2`, see the next paragraph). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.5.0 contract formalization **established `2.1` as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change under the protocol above — explicitly *not* part of the v5.5.0 formalization. **v5.5 → `2.2` (framing enforcement).** `2.2` adds two **required-at-2.2** elements — the `framing` enum field and the `## TL;DR` body section — gated identically to the v2.1 mechanism: the new requirements fire only on `brief_version ≥ 2.2`, so every existing `2.0` / `2.1` brief still validates (forward- and backward-compatible). Because adding required elements is a breaking change for any producer that declares `2.2`, this is a controlled version bump under the breaking-change protocol: downstream producers (e.g. a Tier-2 per-app producer) that emit `brief_version: "2.2"` MUST also emit `framing` + `## TL;DR`. The framing enum is additionally enforced on *any* version when the field is present (`BRIEF_INVALID_FRAMING`). The three framing-alignment layers (framing field, `brief-reviewer` memory-alignment dimension, obligatory TL;DR) implement the `CLAUDE.md` cross-cutting invariant. Note: the *plugin* version bump + CHANGELOG entry for this schema change land at the coordinated release (matrix §S10), separate from this schema axis. +**Pre-2.2 briefs receive zero framing enforcement (producer-elective defense).** The framing-alignment defense is gated at `brief_version ≥ 2.2`, so a brief declaring `2.0` or `2.1` gets **none** of it: `framing` is optional and a missing value is not an error, the `## TL;DR` section is not required, and the `brief-reviewer` memory-alignment dimension scores N/A. The backward-compatibility this buys is deliberate, but it also means the framing defense is **producer-elective**: any producer can sidestep the entire defense — deliberately or by shipping a legacy template — simply by declaring `brief_version: "2.1"`, and the pipeline raises no signal by default. This is a documented property of the contract, not a bug. Two remedies close it for a given consumer: (1) require upstream producers to emit `brief_version: "2.2"` (which forces `framing` + `## TL;DR`); or (2) pass the opt-in **`--min-brief-version `** flag to `/trekplan` or `/trekresearch` (forwarded to the validator as `--min-version`), which raises a `BRIEF_VERSION_BELOW_MINIMUM` **warning** — never a block — when a brief declares a version below the floor. The flag defaults to off, so the N-1 compatibility window for `2.0`/`2.1` briefs is preserved unless a consumer explicitly opts into the stricter floor. + **Failure modes:** - `BRIEF_NOT_FOUND` → consumer halts with a usage message - `FM_MISSING` → file has no frontmatter; halt diff --git a/docs/command-modes.md b/docs/command-modes.md index 2ff0c7e..6e64ec7 100644 --- a/docs/command-modes.md +++ b/docs/command-modes.md @@ -24,6 +24,7 @@ Always interactive. Phase 3 is a section-driven completeness loop (no hard cap o | `--external` | Only external research agents (skip codebase analysis) | | `--fg` | No-op alias (foreground is default since v2.4.0) | | `--gates {open\|closed\|adaptive}` | (v3.4.0) Autonomy-checkpoint policy. Default `adaptive` | +| `--min-brief-version ` | (S18) Warn — never block — if an attached `--project` brief declares a version below `` (e.g. `2.2`), i.e. sidesteps framing enforcement | | `--profile ` | (v4.1.0) Model profile for the research phase. | Flags combine: `--project --local`, `--external --quick`. @@ -37,6 +38,7 @@ Flags combine: `--project --local`, `--external --quick`. | `--research [brief2]` | Enrich with extra research briefs beyond what is in `{project_dir}/research/` | | `--fg` | No-op alias (foreground is default since v2.4.0) | | `--quick` | Plan directly (no agent swarm) | +| `--min-brief-version ` | (S18) Warn — never block — if the brief declares a version below `` (e.g. `2.2`), i.e. sidesteps framing enforcement | | `--export ` | Generate shareable output from existing plan | | `--decompose ` | Split plan into self-contained headless sessions | | `--gates {open\|closed\|adaptive}` | (v3.4.0) Autonomy-checkpoint policy. Default `adaptive` | diff --git a/lib/validators/brief-validator.mjs b/lib/validators/brief-validator.mjs index 118be50..f93d2e2 100644 --- a/lib/validators/brief-validator.mjs +++ b/lib/validators/brief-validator.mjs @@ -155,6 +155,32 @@ export function validateBriefContent(text, opts = {}) { } } + // S18 — opt-in version floor. When the caller passes minBriefVersion (commands + // forward --min-brief-version), WARN (never block) if the brief declares a + // version below it: framing enforcement only fires at ≥ 2.2, so an older brief + // sidesteps the framing-alignment defense silently. Absent opt → no check. + // trekreview briefs have no framing and are exempt. + if (opts.minBriefVersion && fm.type !== 'trekreview') { + const mm = String(opts.minBriefVersion).match(/^(\d+)\.(\d+)$/); + if (mm) { + const minMajor = Number(mm[1]); + const minMinor = Number(mm[2]); + const bm = fm.brief_version === undefined + ? null + : String(fm.brief_version).match(/^(\d+)\.(\d+)$/); + const below = bm + ? (Number(bm[1]) < minMajor || (Number(bm[1]) === minMajor && Number(bm[2]) < minMinor)) + : true; // absent or unparseable version is below any floor + if (below) { + warnings.push(issue( + 'BRIEF_VERSION_BELOW_MINIMUM', + `brief_version ${fm.brief_version ?? '(absent)'} is below the requested minimum ${opts.minBriefVersion} — framing enforcement (≥ 2.2) is bypassed`, + 'Re-run /trekbrief to produce a brief_version 2.2 brief, or drop --min-brief-version to accept the older brief as-is.', + )); + } + } + } + if (fm.type !== undefined && !BRIEF_TYPE_VALUES.includes(fm.type)) { errors.push(issue( 'BRIEF_WRONG_TYPE', @@ -223,12 +249,15 @@ export function validateBrief(filePath, opts = {}) { if (import.meta.url === `file://${process.argv[1]}`) { const args = process.argv.slice(2); const strict = !args.includes('--soft'); - const filePath = args.find(a => !a.startsWith('--')); + const minIdx = args.indexOf('--min-version'); + const minBriefVersion = minIdx >= 0 ? args[minIdx + 1] : undefined; + // filePath is the first positional, skipping the --min-version value token. + const filePath = args.find((a, i) => !a.startsWith('--') && i !== minIdx + 1); if (!filePath) { - process.stderr.write('Usage: brief-validator.mjs [--soft] \n'); + process.stderr.write('Usage: brief-validator.mjs [--soft] [--min-version ] \n'); process.exit(2); } - const r = validateBrief(filePath, { strict }); + const r = validateBrief(filePath, { strict, minBriefVersion }); if (args.includes('--json')) { process.stdout.write(JSON.stringify({ valid: r.valid, errors: r.errors, warnings: r.warnings }, null, 2) + '\n'); } else { diff --git a/tests/commands/trekbrief.test.mjs b/tests/commands/trekbrief.test.mjs index f06be06..3badca3 100644 --- a/tests/commands/trekbrief.test.mjs +++ b/tests/commands/trekbrief.test.mjs @@ -147,6 +147,12 @@ test('trekbrief — v5.5 brief-reviewer declares the memory-alignment dimension' 'brief-reviewer must define the no-memory-context N/A fallback'); }); +test('trekbrief — S18 brief-reviewer memory_alignment carries a status field', () => { + const reviewer = readFileSync(REVIEWER_FILE, 'utf8'); + assert.ok(reviewer.includes('"status": "verified | n_a | contradictions"'), + 'memory_alignment must emit a status enum (verified | n_a | contradictions) so a score-5 N/A (no memory) is distinguishable from a score-5 verified-aligned brief'); +}); + test('trekbrief — v5.5 template carries framing field, 2.2, and TL;DR section', () => { const tpl = readFileSync(TEMPLATE_FILE, 'utf8'); assert.ok(/brief_version: "2\.2"/.test(tpl), 'template must declare brief_version 2.2'); diff --git a/tests/lib/doc-consistency.test.mjs b/tests/lib/doc-consistency.test.mjs index 4f175ea..c6374b1 100644 --- a/tests/lib/doc-consistency.test.mjs +++ b/tests/lib/doc-consistency.test.mjs @@ -869,3 +869,30 @@ test('S17: NW2 bake-off verdict is not labeled unqualified POSITIVE (raw data un 'T2-bakeoff-results.md must disclose the raw per-run data is un-archived / un-reproducible.', ); }); + +// ── S18: framing-hardening cross-file invariants ─────────────────────────── + +test('S18: --min-brief-version is documented at the trekplan + trekresearch boundary', () => { + // The opt-in version floor must be discoverable wherever flags live: both + // consuming commands and the central flag reference. + for (const f of ['commands/trekplan.md', 'commands/trekresearch.md', 'docs/command-modes.md']) { + assert.ok( + read(f).includes('--min-brief-version'), + `${f} must document the --min-brief-version flag (S18 framing-hardening)`, + ); + } +}); + +test('S18: HANDOVER-CONTRACTS documents the pre-2.2 zero-framing-enforcement hole', () => { + // The framing defense is producer-elective: a brief declaring ≤ 2.1 sidesteps + // it entirely. Handover 1 (PUBLIC CONTRACT) must disclose this and name the remedy. + const h = read('docs/HANDOVER-CONTRACTS.md'); + assert.ok( + /BRIEF_VERSION_BELOW_MINIMUM/.test(h), + 'HANDOVER-CONTRACTS.md must name the BRIEF_VERSION_BELOW_MINIMUM gate as the pre-2.2 remedy', + ); + assert.ok( + /zero framing enforcement|no framing enforcement|sidestep/i.test(h), + 'HANDOVER-CONTRACTS.md must state that pre-2.2 briefs receive zero framing enforcement', + ); +}); diff --git a/tests/validators/brief-validator.test.mjs b/tests/validators/brief-validator.test.mjs index 2732cd1..a24d9d4 100644 --- a/tests/validators/brief-validator.test.mjs +++ b/tests/validators/brief-validator.test.mjs @@ -358,3 +358,38 @@ test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate' 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))); }); + +// 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'); + +test('validateBrief — S18 min-version: older brief below floor warns but does not block', () => { + const r = validateBriefContent(BRIEF_21, { strict: true, minBriefVersion: '2.2' }); + assert.equal(r.valid, true, `must warn, not block: ${JSON.stringify(r.errors)}`); + assert.ok( + r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'), + `expected BRIEF_VERSION_BELOW_MINIMUM; warnings=${JSON.stringify(r.warnings)}`, + ); +}); + +test('validateBrief — S18 min-version: absent opt means no version-floor check', () => { + const r = validateBriefContent(BRIEF_21, { strict: true }); + assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); +}); + +test('validateBrief — S18 min-version: brief meeting the floor does not warn', () => { + const r = validateBriefContent(GOOD_BRIEF_22, { strict: true, minBriefVersion: '2.2' }); + assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); +}); + +test('validateBrief — S18 min-version: 2.0 brief below 2.2 floor warns', () => { + const r = validateBriefContent(GOOD_BRIEF, { minBriefVersion: '2.2' }); + assert.ok(r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); +}); + +test('validateBrief — S18 min-version: trekreview brief is exempt (no framing to bypass)', () => { + const r = validateBriefContent(REVIEW_AS_BRIEF, { minBriefVersion: '2.2' }); + assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); +});