feat(rollup): V6 marker-loss gate + depth-1+2 discovery matching board.sh
Two measured defects in build-rollup-register.mjs, reported by ~/repos and
independently re-measured here with the producer's own parser as instrument.
D1 - discovery saw only depth 1, so 5 markers across 3 marketplace repos were
invisible (11 STATE.md files live at depth 2). discoverRepos now mirrors
board.sh:63-89 EXACTLY: a dir that is itself a git repo contributes itself and
is NOT descended into; a non-repo dir holding git repos is a polyrepo container
contributing its children. Identity stays the RAW BASENAME (board.sh:100) so
this builder, board.sh and the coord mailboxes name a repo alike.
Not descending into a git repo is what makes the known nested-name trap
(claude-code-100x/claude-code-100x, both git repos) structurally unreachable
rather than merely absent - blanket depth-2, as proposed, would have reached it.
A collision gate still fails HIGH on two STATE-bearing dirs resolving to one
name, gated on CONTRIBUTING repos so a same-named dir without STATE.md cannot
stop the build. Measured today: 0 collisions across 25 state-bearing dirs.
D2 - parseMarkerLine returns null for two DIFFERENT facts ("not a marker" /
"a marker that failed to parse"), so a malformed marker vanished with exit 0.
V4/V5 only ever gated the carrier; nothing gated marker loss. New V6 reports a
line whose topic and KNOWN status token are both present but which does not
parse, naming file + line number. A warning, not a failure: a typo in another
repo's LOCAL state file must be visible, but is not this builder's business to
fail on. Unknown tokens stay "not a marker", keeping prose out of the channel.
A repo with only losses is still discovered - skipping it would re-open the
exact silent channel V6 closes.
Acceptance against real ~/repos: 15 markers (was 10, +5 exactly as predicted),
2 V6 warnings naming the two measured "--" lines, 0 collisions, exit 0.
Test fixtures are synthetic: marker prose is Output-A material (LOCAL-ONLY),
so quoting a real one into this public repo would be the same
durability-vs-publicity conflation the builder header warns about.
Suite 87/87 (was 78, +9). check-versions 11 OK / 0 WARN / 0 ERROR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPHvLhn2U3j2XD3Cn8zeYa
This commit is contained in:
parent
5ddc6c77a5
commit
1eabad1d27
2 changed files with 273 additions and 47 deletions
|
|
@ -11,6 +11,9 @@
|
|||
// and to make the V4 "no em-dash in carrier" axis unambiguous.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import {
|
||||
STATUS_VOCAB,
|
||||
parseMarkerLine,
|
||||
|
|
@ -18,6 +21,8 @@ import {
|
|||
validateCarrier,
|
||||
computeDrift,
|
||||
buildRegister,
|
||||
extractMarkers,
|
||||
discoverRepos,
|
||||
} from './build-rollup-register.mjs';
|
||||
|
||||
const EMDASH = '—';
|
||||
|
|
@ -220,3 +225,128 @@ test('buildRegister: Output B is sorted and deterministic across repos', () => {
|
|||
const { outputB } = buildRegister({ repos });
|
||||
assert.deepEqual(outputB, ['alpha: planned', 'zeta: done'], 'stable sort so the public index has no spurious diffs');
|
||||
});
|
||||
|
||||
// --- V6 marker-loss gate: a marker ATTEMPT that fails to parse must be reported ---
|
||||
// parseMarkerLine returns null for two different facts ("not a marker" / "a marker that
|
||||
// failed to parse"), so a malformed marker used to vanish with exit 0. V6 splits them.
|
||||
test('V6: a well-formed marker extracts with no loss reported', () => {
|
||||
const text = `# STATE\n\nsome-topic: planned ${EMDASH} prose after the em-dash\n`;
|
||||
const { markers, losses } = extractMarkers(text);
|
||||
assert.equal(markers.length, 1);
|
||||
assert.deepEqual(losses, [], 'a parseable marker is not a loss');
|
||||
});
|
||||
|
||||
test('V6: an ASCII "--" separator loses the marker today -> reported with line number', () => {
|
||||
// The measured shape: two STATE.md files in ~/repos wrote "--" where the grammar
|
||||
// requires the em-dash, and both vanished silently before this gate existed. The
|
||||
// fixture is SYNTHETIC on purpose — marker prose is Output-A material (LOCAL-ONLY),
|
||||
// so quoting a real one verbatim into this public repo would be the same
|
||||
// durability-vs-publicity conflation the builder header warns about.
|
||||
const text = '# STATE\n\nsome-topic: done -- prose after an ASCII separator\n';
|
||||
const { markers, losses } = extractMarkers(text);
|
||||
assert.equal(markers.length, 0, 'the line still does not parse as a marker');
|
||||
assert.equal(losses.length, 1, 'but it must no longer disappear silently');
|
||||
assert.equal(losses[0].lineNo, 3, 'the operator needs the line number to fix it');
|
||||
assert.match(losses[0].line, /some-topic: done/);
|
||||
});
|
||||
|
||||
test('V6: prose and unknown status tokens are NOT losses (no false positives)', () => {
|
||||
const text = [
|
||||
'just some prose without a marker',
|
||||
`topic: active ${EMDASH} folded away, deliberately not a marker`,
|
||||
`# STATE ${EMDASH} title heading`,
|
||||
'',
|
||||
].join('\n');
|
||||
const { markers, losses } = extractMarkers(text);
|
||||
assert.equal(markers.length, 0);
|
||||
assert.deepEqual(losses, [], 'only a KNOWN status token makes a line a marker attempt');
|
||||
});
|
||||
|
||||
test('V6: buildRegister surfaces marker losses per repo, naming the STATE file', () => {
|
||||
const repos = [{
|
||||
name: 'some-repo',
|
||||
statePath: '/repos/some-repo/STATE.md',
|
||||
markers: [], // losses-only: the repo must still be reported, not skipped
|
||||
losses: [{ lineNo: 78, line: 'some-topic: planned -- prose after an ASCII separator' }],
|
||||
carrier: null,
|
||||
carrierMode: 'absent',
|
||||
}];
|
||||
const { markerLosses } = buildRegister({ repos });
|
||||
assert.equal(markerLosses.length, 1);
|
||||
assert.equal(markerLosses[0].repo, 'some-repo');
|
||||
assert.equal(markerLosses[0].statePath, '/repos/some-repo/STATE.md');
|
||||
assert.equal(markerLosses[0].lineNo, 78);
|
||||
});
|
||||
|
||||
// --- D1 discovery: depth 1 + depth 2 under polyrepo containers (board.sh's rule) ---
|
||||
// Identity is the RAW BASENAME so this builder and board.sh name the same repo the same
|
||||
// way (board.sh:100). Mirrors board.sh:63-89: a dir that is itself a git repo contributes
|
||||
// itself and is NOT descended into; a non-repo dir containing git repos is a container.
|
||||
function tree(spec) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'rollup-discovery-'));
|
||||
for (const [rel, state] of Object.entries(spec)) {
|
||||
const dir = join(root, rel);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
if (state !== null) {
|
||||
mkdirSync(join(dir, '.git'), { recursive: true });
|
||||
writeFileSync(join(dir, 'STATE.md'), state);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const MARKER = `topic-a: planned ${EMDASH} prose\n`;
|
||||
|
||||
test('D1: a depth-1 git repo with markers is discovered (unchanged behaviour)', () => {
|
||||
const root = tree({ 'solo': MARKER });
|
||||
const repos = discoverRepos(root);
|
||||
assert.deepEqual(repos.map((r) => r.name), ['solo']);
|
||||
});
|
||||
|
||||
test('D1: repos under a polyrepo container (depth 2) are discovered', () => {
|
||||
// The measured gap: ktg-plugin-marketplace/{catalog,linkedin-studio,ms-ai-architect}
|
||||
// carried 5 markers that the depth-1 glob could not see.
|
||||
const root = tree({
|
||||
'marketplace/catalog': MARKER,
|
||||
'marketplace/linkedin-studio': MARKER,
|
||||
'solo': MARKER,
|
||||
});
|
||||
const repos = discoverRepos(root);
|
||||
assert.deepEqual(
|
||||
repos.map((r) => r.name).sort(),
|
||||
['catalog', 'linkedin-studio', 'solo'],
|
||||
'container children contribute; the container itself never does',
|
||||
);
|
||||
});
|
||||
|
||||
test('D1: a git repo is NOT descended into (the nested same-name trap stays unreachable)', () => {
|
||||
// claude-code-100x/claude-code-100x: the outer IS a git repo, so board.sh's rule never
|
||||
// reaches the inner one. Matching that rule makes this collision structurally impossible
|
||||
// rather than merely absent today.
|
||||
const root = tree({ 'outer': MARKER, 'outer/outer': MARKER });
|
||||
const repos = discoverRepos(root);
|
||||
assert.deepEqual(repos.map((r) => r.name), ['outer'], 'the inner repo must not be reached');
|
||||
});
|
||||
|
||||
test('D1: a STATE.md in a non-git directory is not a repo', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'rollup-discovery-'));
|
||||
mkdirSync(join(root, 'scratch'), { recursive: true });
|
||||
writeFileSync(join(root, 'scratch', 'STATE.md'), MARKER);
|
||||
assert.deepEqual(discoverRepos(root), [], 'no .git -> not a repo (board.sh:78)');
|
||||
});
|
||||
|
||||
test('D1: a basename collision fails LOUDLY instead of silently overwriting', () => {
|
||||
// Raw basename was chosen so this builder and the coord mailbox name repos alike;
|
||||
// measured 0 collisions across 25 state-bearing dirs today, which is a fact, not a
|
||||
// guarantee -- so the ambiguity must be an error, never a silent pick-one.
|
||||
const root = tree({ 'dup': MARKER, 'container/dup': MARKER });
|
||||
assert.throws(
|
||||
() => discoverRepos(root),
|
||||
(err) => {
|
||||
assert.match(err.message, /collision/i);
|
||||
assert.match(err.message, /dup/);
|
||||
return true;
|
||||
},
|
||||
'two STATE.md files resolving to one name must stop the build',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue