v5.1.0 shipped with an unquoted brief_version: 2.1 in trekbrief-template.md. parseScalar coerced it to Number 2.1, and the sequencing gate guarded on typeof === 'string', silently bypassing BRIEF_V51_MISSING_SIGNALS. Three-part atomic fix: - brief-validator.mjs:87+149 now accepts both string and number forms via String(fm.brief_version) coercion. - trekbrief-template.md quotes the value so new briefs parse as String. - doc-consistency.test.mjs pins the QUOTED form going forward. Three regression tests added in brief-validator.test.mjs.
This commit is contained in:
parent
a67b5717c9
commit
4c85a2c22b
4 changed files with 44 additions and 8 deletions
|
|
@ -84,8 +84,12 @@ export function validateBriefContent(text, opts = {}) {
|
|||
}
|
||||
}
|
||||
// Sequencing gate: brief_version ≥ 2.1 requires phase_signals OR phase_signals_partial.
|
||||
if (typeof fm.brief_version === 'string') {
|
||||
const vm = fm.brief_version.match(/^(\d+)\.(\d+)$/);
|
||||
// Coerce to String so the gate fires regardless of whether YAML parsed the value as
|
||||
// a string ("2.1") or a number (2.1). v5.1.0 shipped with an unquoted-2.1 template
|
||||
// that silently bypassed this gate — fix locked in by quoting the template AND
|
||||
// accepting both shapes here as defense-in-depth (v5.1.1, finding 3c834097/df1435a2).
|
||||
if (typeof fm.brief_version === 'string' || typeof fm.brief_version === 'number') {
|
||||
const vm = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
||||
if (vm) {
|
||||
const major = Number(vm[1]);
|
||||
const minor = Number(vm[2]);
|
||||
|
|
@ -146,8 +150,8 @@ export function validateBriefContent(text, opts = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof fm.brief_version === 'string') {
|
||||
const m = fm.brief_version.match(/^(\d+)\.(\d+)$/);
|
||||
if (typeof fm.brief_version === 'string' || typeof fm.brief_version === 'number') {
|
||||
const m = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
||||
if (!m) {
|
||||
warnings.push(issue('BRIEF_VERSION_FORMAT', `brief_version "${fm.brief_version}" not in N.M form`));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: 2.1
|
||||
brief_version: "2.1"
|
||||
created: {YYYY-MM-DD}
|
||||
task: "{one-line task description}"
|
||||
slug: {slug}
|
||||
|
|
|
|||
|
|
@ -554,10 +554,10 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () =>
|
|||
|
||||
// --- v5.1 — phase_signals + brief_version 2.1 ---
|
||||
|
||||
test('v5.1 — templates/trekbrief-template.md declares brief_version: 2.1', () => {
|
||||
test('v5.1 — templates/trekbrief-template.md declares brief_version: "2.1" (quoted)', () => {
|
||||
const t = read('templates/trekbrief-template.md');
|
||||
assert.match(t, /^brief_version: 2\.1$/m,
|
||||
'trekbrief-template.md must declare brief_version: 2.1 at top of frontmatter');
|
||||
assert.match(t, /^brief_version: "2\.1"$/m,
|
||||
'trekbrief-template.md must declare brief_version: "2.1" (quoted) — unquoted parses as Number and bypasses sequencing gate');
|
||||
});
|
||||
|
||||
test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => {
|
||||
|
|
|
|||
|
|
@ -218,3 +218,35 @@ test('validateBrief — phase_signals with unknown phase rejected', () => {
|
|||
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'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue