Files
msd-core/tests/adr-612-bracket-selection.property.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

402 lines
18 KiB
JavaScript

'use strict';
/**
* PR-2 (#2761 / epic #612) — generative properties for the SELECTION/TOLERANCE
* layer.
*
* Scope, and why this is not a second copy of PR-1's grammar properties:
* PR-1 (#2258) owns the bracket grammar itself — `parsePhaseId`/`renderPhaseId`/
* `toDir` and their round-trip/bijection properties, which live in
* tests/adr-612-bracket-grammar.test.cjs. PR-2 owns a different contract: WHICH
* pattern each reader compiles, decided by the project's resolved
* `phase_id_convention`. That contract is what this file generates against.
*
* Four properties, one per claim the PR actually makes:
* P1 a repo that OPTED IN reads ADR-canonical bracket headings/dirs; a repo
* that did not is byte-blind to exactly the same input.
* P2 under a non-bracket convention every selected reader agrees with the
* hand-transcribed BASE source on generated content — including hostile
* legacy content that merely LOOKS bracketed (`[RFC.2119] 5:`).
* P3 a mutated bracket token is not silently accepted; a case-varied one is
* accepted and FOLDS to the same key.
* P4 both sides of a phase comparison derive the same key under the same
* convention, and convention-less call sites are byte-identical.
*
* Generator discipline (the lessons #2258 rounds 1-2 were burned by, applied):
* - Every input is TEMPLATED FROM RAW PRIMITIVES. Nothing is seeded through
* `renderPhaseId`/`toDir`, so the generator's domain is not defined by the
* code under test — the `p2()` tautology that made round 1's property test
* structurally unable to find B1.
* - The generator domain is >= the code domain: the milestone/token
* arbitraries reach past 99 into the 3+-digit branch (round 2's
* `numArb`-capped-at-99 miss) and hit both sentinels (`00`, `999`).
* - Sub-phases are FORCED IN at high weight rather than left to chance
* (round 2's missing sub-phase mutation).
* - P3 is a NEGATIVE property: canonical input is mutated per-field and
* rejection is asserted. Deterministic boundary literals for the same
* region live alongside, in adr-612-bracket-heading-selection.test.cjs.
*
* Deterministic per CONTRIBUTING.md: seed and numRuns are pinned by
* tests/helpers/fast-check-setup.cjs (seed 42, numRuns 200); set MSD_FC_SEED to
* explore locally.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fc = require('./helpers/fast-check-setup.cjs');
const core = require('../msd-core/bin/lib/phase-id.cjs');
const B = core.PHASE_HEADING_BASELINE;
const {
phaseHeadingPrefixSrcFor,
extractPhaseToken,
phaseKeyFromDir,
phaseKeyFromToken,
bracketQualifiedKey,
foldBracketId,
isSentinelPhaseId,
phaseTokenMatches,
} = core;
// ─── Base spellings, transcribed by hand (NOT read from the selector) ────────
// These are what each call site's regex contained before PR-2. P2 compares the
// selector's non-bracket output against these over GENERATED CONTENT, which is
// what makes it a property rather than a restatement of the 14-site structural
// identity test in adr-612-bracket-heading-selection.test.cjs.
const BASE_SRC = {
[B.ANY_BRACKET]: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+',
[B.LABEL_ONLY]: 'Phase\\s+',
};
const NON_BRACKET_CONVENTIONS = [null, undefined, 'milestone-prefixed', 'legacy', ''];
/**
* Compile a reader the way the shipped call sites do. The leading group is the
* markdown FURNITURE those sites carry (`#{2,4}\s*` on the heading scanners,
* `[\s*_]*` plus a checkbox on the checklist scanners); without it every
* generated line that looks like real ROADMAP content would fail to match on
* BOTH sides and the differential property below would hold vacuously.
*/
const compile = introSrc =>
new RegExp(
`^(?:#{2,4}[ \t]*|[-*+][ \t]*\\[[ xX]\\][ \t]*)?[*_]*${introSrc}([\\w][\\w.-]*)\\s*:`,
'i',
);
const heading = (baseline, convention) => compile(phaseHeadingPrefixSrcFor(baseline, convention));
// ─── Generators: raw primitives, hand-templated ──────────────────────────────
const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const CODE_TAIL = UPPER + '0123456789_';
/** `MSD`, `A`, `CK2`, `PROJ_X` — the emit grammar's project-code shape. */
const codeArb = fc
.tuple(
fc.constantFrom(...UPPER),
fc.string({ unit: fc.constantFrom(...CODE_TAIL), maxLength: 5 }),
)
.map(([head, tail]) => head + tail);
/**
* Milestone integers the bracket grammar admits: EXACTLY two digits, or three
* or more with no leading zero. Deliberately reaches past 99 and pins both
* sentinel milestones (`00` pre-milestone, `999` icebox).
*/
const milestoneArb = fc.oneof(
{ arbitrary: fc.integer({ min: 0, max: 99 }).map(n => String(n).padStart(2, '0')), weight: 4 },
{ arbitrary: fc.integer({ min: 100, max: 99999 }).map(String), weight: 3 },
{ arbitrary: fc.constantFrom('00', '01', '99', '100', '101', '998', '999', '1000'), weight: 2 },
);
/** Phase tokens: 2-digit, 3+-digit, and sub-phases FORCED IN at high weight. */
const tokenArb = fc.oneof(
{ arbitrary: fc.integer({ min: 0, max: 99 }).map(n => String(n).padStart(2, '0')), weight: 4 },
{ arbitrary: fc.integer({ min: 100, max: 9999 }).map(String), weight: 2 },
{
arbitrary: fc
.tuple(fc.integer({ min: 0, max: 99 }), fc.integer({ min: 0, max: 99 }))
.map(([p, s]) => `${String(p).padStart(2, '0')}.${String(s).padStart(2, '0')}`),
weight: 3,
},
{ arbitrary: fc.constantFrom('00', '01', '99', '100', '999', '01.01', '100.100'), weight: 1 },
);
const nameArb = fc
.string({ unit: fc.constantFrom(...(UPPER + 'abcdefghijklmnopqrstuvwxyz ')), minLength: 1, maxLength: 20 })
.map(s => s.trim() || 'Name');
/** Markdown furniture the real call sites tolerate ahead of the intro. */
const furnitureArb = fc.constantFrom('', '## ', '### ', '#### ', '- [ ] ', '- [x] ', '- [x] **');
/** The ADR-canonical, LABEL-LESS bracket heading — the shape only opt-in reads. */
const bracketHeadingArb = fc
.record({ code: codeArb, milestone: milestoneArb, token: tokenArb, name: nameArb, furniture: furnitureArb })
.map(r => ({ ...r, text: `${r.furniture}[${r.code}.${r.milestone}] ${r.token}: ${r.name}` }));
/** The bracket DIRECTORY shape `{CODE}.{MM}-{PP}[.{SS}]-slug`. */
const bracketDirArb = fc
.record({ code: codeArb, milestone: milestoneArb, token: tokenArb, slug: fc.constantFrom('alpha', 'beta-two', 'x', 'a-b-c') })
.map(r => {
const dir = `${r.code}.${r.milestone}-${r.token}-${r.slug}`;
// A short letters+digit project-code prefix is string-indistinguishable
// from the shipped letter-prefixed-decimal family (for example A1.00-00).
// Derive that legacy oracle from generator primitives, not the parser.
const firstDigit = [...r.code].findIndex(ch => ch >= '0' && ch <= '9');
const hasLegacyLetterDecimalPrefix = firstDigit >= 1
&& firstDigit <= 3
&& [...r.code.slice(0, firstDigit)].every(ch => UPPER.includes(ch));
const legacyReading = hasLegacyLetterDecimalPrefix
? `${r.code}.${r.milestone}-${r.token}`
: dir;
return { ...r, dir, legacyReading };
});
// ─── P1 — the selection gate ─────────────────────────────────────────────────
describe('#612 PR-2 property: the convention selects, and only the convention', () => {
test('P1a a label-less bracket heading is read IFF the convention is bracket', () => {
fc.assert(
fc.property(bracketHeadingArb, fc.constantFrom(B.ANY_BRACKET, B.LABEL_ONLY), (h, baseline) => {
const opted = heading(baseline, 'bracket').exec(h.text);
assert.ok(opted, `opted-in repo failed to read ${JSON.stringify(h.text)}`);
assert.equal(opted[1], h.token, 'the captured token must be the phase token, not the bracket');
for (const convention of NON_BRACKET_CONVENTIONS) {
assert.equal(
heading(baseline, convention).test(h.text),
false,
`convention ${JSON.stringify(convention)} must stay blind to ${JSON.stringify(h.text)}`,
);
}
}),
);
});
test('P1b a bracket DIRECTORY yields its phase token IFF the convention is bracket', () => {
fc.assert(
fc.property(bracketDirArb, d => {
assert.equal(extractPhaseToken(d.dir, 'bracket'), d.token);
for (const convention of NON_BRACKET_CONVENTIONS) {
assert.equal(
extractPhaseToken(d.dir, convention),
d.legacyReading,
`convention ${JSON.stringify(convention)} must preserve the exact legacy reading of ${JSON.stringify(d.dir)}`,
);
}
}),
);
});
test('P1c the bracket MILESTONE carries the sentinel, independent of the token', () => {
// READING-B: `### [MSD.999] 01:` is an icebox item even though its token is
// an ordinary `01`. The token alone can never decide this.
fc.assert(
fc.property(bracketHeadingArb, h => {
const milestoneInt = parseInt(h.milestone, 10);
const expected = milestoneInt === 0 || milestoneInt === 999;
assert.equal(
isSentinelPhaseId(`${h.code}.${h.milestone}-${h.token}`, 'bracket'),
expected,
`milestone ${h.milestone} sentinel classification`,
);
}),
);
});
});
// ─── P2 — differential invariance for every non-bracket convention ───────────
/** Legacy and hostile-but-plausible legacy lines. No bracket phase headings. */
const legacyLineArb = fc.oneof(
{
arbitrary: fc.record({ t: tokenArb, n: nameArb }).map(r => `### Phase ${r.t}: ${r.n}`),
weight: 4,
},
{
arbitrary: fc.record({ t: tokenArb, n: nameArb }).map(r => `- [x] **Phase ${r.t}: ${r.n}**`),
weight: 3,
},
{
// The refutation corpus: bracket-DOTTED prose headings that predate this
// convention and must never be claimed as phases by a non-bracket repo.
arbitrary: fc
.record({
tag: fc.constantFrom('RFC.2119', 'v1.0', 'ADR.612', 'SPEC.1', 'ISO.8601', 'MSD.02'),
t: tokenArb,
n: nameArb,
})
.map(r => `### [${r.tag}] ${r.t}: ${r.n}`),
weight: 3,
},
{
arbitrary: fc.record({ tag: fc.constantFrom('MSD.02', 'x'), t: tokenArb, n: nameArb })
.map(r => `### [${r.tag}] Phase ${r.t}: ${r.n}`),
weight: 2,
},
{ arbitrary: nameArb.map(n => `Some prose about ${n}.`), weight: 1 },
);
describe('#612 PR-2 property: a repo that did not opt in compiles the base reader', () => {
test('P2 every non-bracket convention agrees with the BASE source on generated content', () => {
fc.assert(
fc.property(
fc.array(legacyLineArb, { minLength: 1, maxLength: 12 }),
fc.constantFrom(B.ANY_BRACKET, B.LABEL_ONLY),
(lines, baseline) => {
const base = compile(BASE_SRC[baseline]);
for (const convention of NON_BRACKET_CONVENTIONS) {
const selected = heading(baseline, convention);
for (const line of lines) {
const a = base.exec(line);
const b = selected.exec(line);
assert.equal(
a === null,
b === null,
`recognition diverged from base on ${JSON.stringify(line)} (convention ${JSON.stringify(convention)})`,
);
if (a && b) {
assert.equal(b[1], a[1], `captured token diverged from base on ${JSON.stringify(line)}`);
}
}
}
},
),
);
});
});
// ─── P3 — negative properties: mutate every field, assert non-acceptance ─────
/**
* Each mutation breaks ONE field of a canonical bracket heading. `folds` marks
* the one mutation that must still be ACCEPTED (case is folded, not rejected) —
* asserting rejection there would pin the wrong contract.
*/
const MUTATIONS = [
{ name: 'unpadded milestone', folds: false, apply: h => h.milestone.length === 2 && h.milestone[0] === '0' ? `${h.furniture}[${h.code}.${h.milestone.slice(1)}] ${h.token}: ${h.name}` : null },
{ name: 'overpadded milestone', folds: false, apply: h => `${h.furniture}[${h.code}.0${h.milestone}] ${h.token}: ${h.name}` },
{ name: 'non-numeric milestone', folds: false, apply: h => `${h.furniture}[${h.code}.AB] ${h.token}: ${h.name}` },
{ name: 'unclosed bracket', folds: false, apply: h => `${h.furniture}[${h.code}.${h.milestone} ${h.token}: ${h.name}` },
{ name: 'nested bracket', folds: false, apply: h => `${h.furniture}[${h.code}.[${h.milestone}]] ${h.token}: ${h.name}` },
{ name: 'hyphen for dot', folds: false, apply: h => `${h.furniture}[${h.code}-${h.milestone}] ${h.token}: ${h.name}` },
{ name: 'colon for dot', folds: false, apply: h => `${h.furniture}[${h.code}:${h.milestone}] ${h.token}: ${h.name}` },
{ name: 'paren for close bracket', folds: false, apply: h => `${h.furniture}[${h.code}.${h.milestone}) ${h.token}: ${h.name}` },
{ name: 'lowercased code', folds: true, apply: h => `${h.furniture}[${h.code.toLowerCase()}.${h.milestone}] ${h.token}: ${h.name}` },
];
describe('#612 PR-2 property: a broken bracket token is not silently accepted', () => {
for (const mutation of MUTATIONS) {
test(`P3 ${mutation.name} — ${mutation.folds ? 'folds to the same key' : 'is not read as a phase'}`, () => {
fc.assert(
fc.property(bracketHeadingArb, fc.constantFrom(B.ANY_BRACKET, B.LABEL_ONLY), (h, baseline) => {
const mutated = mutation.apply(h);
if (mutated === null) return; // mutation not applicable to this sample
const reader = heading(baseline, 'bracket');
if (mutation.folds) {
assert.ok(reader.test(mutated), `case variation must still be read: ${JSON.stringify(mutated)}`);
assert.equal(
bracketQualifiedKey(`${h.code.toLowerCase()}.${h.milestone}-${h.token}`, 'bracket'),
bracketQualifiedKey(`${h.code}.${h.milestone}-${h.token}`, 'bracket'),
'a case variation must fold to the identical qualified key',
);
return;
}
assert.equal(
reader.test(mutated),
false,
`${mutation.name} must not be read as a phase: ${JSON.stringify(mutated)}`,
);
}),
);
});
}
test('P3z a malformed bracket forms no qualified key, on any convention', () => {
fc.assert(
fc.property(bracketHeadingArb, fc.constantFrom(...MUTATIONS.filter(m => !m.folds).map(m => m.name)), (h, name) => {
const mutation = MUTATIONS.find(m => m.name === name);
const mutated = mutation.apply(h);
if (mutated === null) return;
// The id form of the same breakage: strip the heading furniture.
const id = mutated.replace(/^(?:#{2,4}[ \t]*|[-*+][ \t]*\[[ xX]\][ \t]*)?[*_]*/, '').replace(/^\[/, '').replace(/\]\s*/, '-').replace(/:.*$/, '').trim();
assert.equal(bracketQualifiedKey(id, 'bracket'), null, `${name} must form no key: ${JSON.stringify(id)}`);
for (const convention of NON_BRACKET_CONVENTIONS) {
assert.equal(bracketQualifiedKey(id, convention), null);
}
}),
);
});
});
// ─── P4 — both sides of a comparison, one convention ─────────────────────────
describe('#612 PR-2 property: comparison sides derive under the same convention', () => {
test('P4a a bracket DIR and its ROADMAP TOKEN produce the identical key', () => {
// The #2562 defect class reached from the other side: the promoted
// `phaseKeyFromDir` derives both sides with the same FUNCTION, but a bracket
// dir still needs the same CONVENTION or it keys to its own whole name.
fc.assert(
fc.property(bracketDirArb, d => {
assert.equal(phaseKeyFromDir(d.dir, 'bracket'), phaseKeyFromToken(d.token));
}),
);
});
test('P4b a convention-less call site is byte-identical across all its spellings', () => {
fc.assert(
fc.property(
fc.oneof(
bracketDirArb.map(d => d.dir),
fc.record({ t: tokenArb, s: fc.constantFrom('alpha', 'beta-two', 'x') }).map(r => `${r.t}-${r.s}`),
fc.record({ c: codeArb, t: tokenArb }).map(r => `${r.c}-${r.t}-slug`),
),
dir => {
const bare = phaseKeyFromDir(dir);
assert.equal(phaseKeyFromDir(dir, undefined), bare);
assert.equal(phaseKeyFromDir(dir, null), bare);
assert.equal(phaseKeyFromDir(dir, 'milestone-prefixed'), bare);
},
),
);
});
test('P4c the qualified key separates same-token phases of different milestones', () => {
fc.assert(
fc.property(
fc.record({ code: codeArb, a: milestoneArb, b: milestoneArb, token: tokenArb, slug: fc.constantFrom('one', 'two') }),
r => {
fc.pre(parseInt(r.a, 10) !== parseInt(r.b, 10));
const dirA = `${r.code}.${r.a}-${r.token}-${r.slug}`;
const qualifiedB = `${r.code}.${r.b}-${r.token}`;
assert.equal(phaseTokenMatches(dirA, `${r.code}.${r.a}-${r.token}`, 'bracket'), true);
assert.equal(
phaseTokenMatches(dirA, qualifiedB, 'bracket'),
false,
`${dirA} must not answer to milestone ${r.b}`,
);
assert.notEqual(
bracketQualifiedKey(`${r.code}.${r.a}-${r.token}`, 'bracket'),
bracketQualifiedKey(qualifiedB, 'bracket'),
);
},
),
);
});
test('P4d foldBracketId is idempotent and case-collapsing', () => {
fc.assert(
fc.property(bracketDirArb, d => {
const id = `${d.code}.${d.milestone}-${d.token}`;
const folded = foldBracketId(id);
assert.equal(foldBracketId(folded), folded, 'fold must be idempotent');
assert.equal(foldBracketId(id.toLowerCase()), folded);
assert.equal(foldBracketId(id.toUpperCase()), folded);
}),
);
});
});