enhance(#2721): regenerating merge driver, regen:derived, and a name for the emitted-artifact family (#2730)
* test(#2721): failing-first suite for the gsd-regen driver and CONTEXT.md parity Tests precede the implementation per the TDD gate. The driver module does not exist yet, so tests/git-merge-regen-driver.test.cjs fails at require time; the contributor-standards parity assertions fail against next as it stands today, where the standards doc names two CONTEXT.md headings that have never existed. Refs #2721 * feat(#2721): add the gsd-regen merge driver and regen:derived The golden parity manifests and the two size baselines are pure functions of the source tree, so their only correct merge is "recompute" -- something git's ours/theirs interface cannot express. 140 of 143 conflicted-file instances across the open PR queue are these files. The driver deliberately does NOT regenerate. Four probes established that at merge-driver time neither the working tree nor the index reflects the merge: both hold the ours side, a file added by theirs does not exist yet, and MERGE_HEAD is unwritten. Git also invokes the driver once per conflicted path (20 here). A regenerating driver would therefore read the ours-side tree and emit a plausible-but-wrong hash manifest -- worse than a conflict, because a conflict is visible. So it accepts %A, runs zero subprocesses, records the resolved paths, and prints one notice pointing at npm run regen:derived. Staleness stays caught where it already was, by golden-install-parity in CI. Every failure path degrades toward today's behaviour (a normal conflict). install-tree is deliberately excluded per ADR-2719 section 7. Also folded in, per the no-defer rule: workflow-size.cjs claimed .md files have no eol=lf in .gitattributes; git check-attr shows eol: lf, set by .gitattributes line 2 since #1088. Refs #2721 * docs(#2721): document regen:derived and the gsd-regen merge driver Adds the how-to a contributor actually reaches for when the generated parity manifests or size baselines conflict, in both places they would look: the merge-conflict path in CONTRIBUTING.md and the full guide in TESTING-SUITES.md, including what the driver deliberately does not do (it does not clear GitHub's CONFLICTING label, and it does not regenerate mid-merge). Also scopes the new contributor-standards parity assertion to the doc's own CONTEXT.md section. Its first run flagged `## Decision`, `## Consequences` and `## Standards followed`, which the doc attributes to an ADR body and a PR body rather than to CONTEXT.md -- a doc-wide extractor would have demanded CONTEXT.md grow headings that do not belong to it. Refs #2721 * fix(#2721): stop passing %P to the merge driver — shell injection The isolated adversarial review found, and I independently reproduced, local arbitrary command execution. Git does not invoke a merge driver with an argv array. It substitutes %O %A %B %L %P textually into the configured string and runs the whole thing through a shell, and $(...) executes inside POSIX double quotes -- so quoting the placeholder does not neutralise it. %O/%A/%B are git-generated temp names and %L is an integer, but %P is the file's own path, chosen freely by any contributor. A branch renaming a covered fixture to evil$(touch PWNED_SENTINEL).json executed that command on the machine of every maintainer who merged it, and the merge still reported success. Fix removes the input rather than filtering it: %P is no longer registered, so the driver receives no attacker-controlled argument at all. The marker records a count instead of path names. A metacharacter filter would have been a guess about shell grammar; passing nothing is a property. Re-ran the identical exploit against the fixed command: nothing executed, conflict still resolved. Two regressions guard it -- a platform-independent assertion that the registered command carries no %P, and a real merge driven by the actual planInstall output with a $(...) filename. Also from review: CLI dispatch had no coverage at all (CONTRIBUTING's "CLI and command routing" matrix), which is why runInstall/runStatus now take {repoRoot} -- hardcoding REPO_ROOT was what made them untestable. Renamed planResolution to resolveAndRecord since the plan* prefix promised purity it did not have. Reconciled the eleven-vs-twelve generator count across CONTEXT.md, CONTRIBUTING.md and the changeset. Refs #2721 * test(#2721): scope safe.directory for the check-attr helper The 66f4d85a run failed 11 assertions, all in the .gitattributes scoping block, with "fatal: detected dubious ownership in repository at '/work'". The test container checks the repo out at a path its user does not own, so git refuses check-attr outright. Everything else passed (27,185). `check-attr` is a pure read of .gitattributes -- no hooks, no filters -- so the exemption is scoped to that one invocation. It is deliberately NOT applied to the driver's own production `git config` calls, which run in the user's own clone and should keep the protection. Refs #2721 * test(#2721): delete the stale assertion that the driver command carries %P The plex2 run on bdfd0856 left exactly two failures, both this test: it still asserted the pre-fix command string, i.e. the vulnerable behaviour. Deleted rather than relaxed, per RULESET.TESTS.delete-bad-tests -- its useful half is already covered, in both directions, by registeredDriverCommandNeverPassesThePlaceholderForTheFilePath. Refs #2721 * test(#2721): drive the end-to-end merges from the real planInstall output The e2e helper hand-rolled its own driver registration, and still carried %P. That meant the five real-git tests were not exercising the production command string at all -- planInstall could drift and they would keep passing. They now register exactly what a contributor gets from npm run setup:merge-driver. Refs #2721 * chore(#2721): backfill changeset pr number to 2730
This commit is contained in:
@@ -81,6 +81,139 @@ describe('docs/contributor-standards.md', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Parity: docs/contributor-standards.md tells contributors which CONTEXT.md sections to
|
||||
* write into. If it names a heading CONTEXT.md does not have, the instruction is
|
||||
* unfollowable — and that is not hypothetical: on 2026-07-27 it named `## Domain terms`
|
||||
* and `## AI Ops Memory`, neither of which has ever existed (#2721). Two surfaces, one
|
||||
* truth; this asserts they cannot diverge again.
|
||||
*/
|
||||
const CONTEXT_MD = path.join(REPO_ROOT, 'CONTEXT.md');
|
||||
|
||||
/**
|
||||
* The body of one `## ` section, exclusive of the next `## `. Split on /\r?\n/ so a CRLF
|
||||
* checkout parses identically. Shared by every assertion below — two copies of the same
|
||||
* section parser is exactly the silent divergence RULESET.SHARED-HELPERS-LINT-VS-TEST warns of.
|
||||
*/
|
||||
function sectionBody(content, heading) {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const start = lines.findIndex((l) => l.trim() === heading);
|
||||
if (start === -1) return null;
|
||||
const rest = lines.slice(start + 1);
|
||||
const end = rest.findIndex((l) => /^##\s/.test(l));
|
||||
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Backticked heading references that the standards doc attributes to CONTEXT.md.
|
||||
*
|
||||
* Scoped to the doc's own `## CONTEXT.md` section on purpose. The doc also names headings
|
||||
* belonging to *other* documents — `## Decision` and `## Consequences` describe an ADR
|
||||
* body, `## Standards followed` describes an issue/PR body. Extracting doc-wide would
|
||||
* demand CONTEXT.md grow headings that have nothing to do with it.
|
||||
*
|
||||
* `<Placeholder>` templates like `### <Module Name>` are skipped: they are shapes to
|
||||
* follow, not headings to find.
|
||||
*/
|
||||
function extractContextHeadingRefs(standardsContent) {
|
||||
const scope = sectionBody(standardsContent, '## CONTEXT.md');
|
||||
if (scope === null) return [];
|
||||
const refs = new Set();
|
||||
for (const m of scope.matchAll(/`(#{2,6}\s+[^`]+)`/g)) {
|
||||
const heading = m[1].trim();
|
||||
if (heading.includes('<')) continue;
|
||||
refs.add(heading);
|
||||
}
|
||||
return [...refs];
|
||||
}
|
||||
|
||||
/** Headings actually present in CONTEXT.md. Anchored /m — CRLF-safe without a `\n` split. */
|
||||
function actualHeadings(contextContent) {
|
||||
return new Set([...contextContent.matchAll(/^#{2,6}\s+.*$/gm)].map((m) => m[0].trim()));
|
||||
}
|
||||
|
||||
describe('docs/contributor-standards.md ↔ CONTEXT.md heading parity', () => {
|
||||
test('everyContextHeadingNamedByTheStandardsDocExistsInContextMd', () => {
|
||||
const refs = extractContextHeadingRefs(readStandardsDoc());
|
||||
const actual = actualHeadings(fs.readFileSync(CONTEXT_MD, 'utf-8'));
|
||||
|
||||
assert.ok(refs.length > 0, 'the standards doc must name at least one CONTEXT.md heading');
|
||||
const missing = refs.filter((r) => !actual.has(r));
|
||||
assert.deepEqual(
|
||||
missing,
|
||||
[],
|
||||
`docs/contributor-standards.md directs contributors to heading(s) that do not exist in ` +
|
||||
`CONTEXT.md: ${JSON.stringify(missing)}. Fix the standards doc (or add the heading).`
|
||||
);
|
||||
});
|
||||
|
||||
test('contextMdStillHasTheGlossaryHeadingTheStandardsDocNamed', () => {
|
||||
const actual = actualHeadings(fs.readFileSync(CONTEXT_MD, 'utf-8'));
|
||||
assert.ok(
|
||||
actual.has('## Glossary — Domain modules and seams'),
|
||||
'the glossary heading is the one the standards doc points Module authors at'
|
||||
);
|
||||
});
|
||||
|
||||
// Negative space for the extractor itself. The RED run of this suite flagged
|
||||
// `## Decision`, `## Consequences` and `## Standards followed` — all headings the doc
|
||||
// attributes to an ADR body or a PR body, not to CONTEXT.md. A doc-wide extractor would
|
||||
// demand CONTEXT.md sprout headings that do not belong to it.
|
||||
test('doesNotTreatAdrOrPrBodyHeadingsAsContextMdClaims', () => {
|
||||
const refs = extractContextHeadingRefs(readStandardsDoc());
|
||||
for (const foreign of ['## Decision', '## Consequences', '## Standards followed']) {
|
||||
assert.ok(
|
||||
!refs.includes(foreign),
|
||||
`${foreign} describes another document's structure and must not be read as a CONTEXT.md claim`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('matchesAHeadingReferenceRegardlessOfLineEndingStyle', () => {
|
||||
const crlf = '## Test rules and lint\r\n\r\n### Emitted Artifact Provenance\r\n';
|
||||
const found = actualHeadings(crlf);
|
||||
assert.ok(found.has('## Test rules and lint'), 'a CRLF checkout must not defeat the match');
|
||||
assert.ok(found.has('### Emitted Artifact Provenance'));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The Emitted Artifact Provenance naming deliverable (#2721). Without these, the artifact
|
||||
* family that half the open PR queue collides on still has no name a contributor can look
|
||||
* up — which ADR-2719 identifies as a direct cause of the problem.
|
||||
*/
|
||||
describe('CONTEXT.md names the emitted-artifact family', () => {
|
||||
test('emittedAttributionRulesetIsUnderTheTestRulesAndLintSection', () => {
|
||||
const body = sectionBody(fs.readFileSync(CONTEXT_MD, 'utf-8'), '## Test rules and lint');
|
||||
assert.ok(body, 'CONTEXT.md must have a `## Test rules and lint` section');
|
||||
assert.ok(
|
||||
body.includes('RULESET.EMITTED_ATTRIBUTION='),
|
||||
'RULESET.EMITTED_ATTRIBUTION must be a sibling of the other test rules, not floating elsewhere'
|
||||
);
|
||||
});
|
||||
|
||||
test('emittedArtifactProvenanceIsRegisteredInTheGlossary', () => {
|
||||
const body = sectionBody(
|
||||
fs.readFileSync(CONTEXT_MD, 'utf-8'),
|
||||
'## Glossary — Domain modules and seams'
|
||||
);
|
||||
assert.ok(body, 'CONTEXT.md must have the glossary section');
|
||||
assert.ok(
|
||||
body.includes('### Emitted Artifact Provenance'),
|
||||
'the emitted-artifact family must be registered in the glossary'
|
||||
);
|
||||
});
|
||||
|
||||
test('emittedArtifactProvenanceIsAConceptNotAModule', () => {
|
||||
const headings = actualHeadings(fs.readFileSync(CONTEXT_MD, 'utf-8'));
|
||||
assert.ok(headings.has('### Emitted Artifact Provenance'));
|
||||
assert.ok(
|
||||
!headings.has('### Emitted Artifact Provenance Module'),
|
||||
'it ships nothing, so it takes no `Module` suffix — follows the `### Resolution Provenance` precedent'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CONTRIBUTING.md links contributor-standards.md', () => {
|
||||
test('CONTRIBUTING.md contains link to contributor-standards.md', () => {
|
||||
let contributing;
|
||||
|
||||
Reference in New Issue
Block a user