* 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
231 lines
9.0 KiB
JavaScript
231 lines
9.0 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// docs/contributor-standards.md is a contributor-facing contract doc — its headings
|
|
// and cross-links ARE what contributors read. Structural assertions on headings and
|
|
// links test the deployed contract, not implementation detail.
|
|
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
const STANDARDS_DOC = path.join(REPO_ROOT, 'docs', 'contributor-standards.md');
|
|
const CONTRIBUTING_MD = path.join(REPO_ROOT, 'CONTRIBUTING.md');
|
|
|
|
function readStandardsDoc() {
|
|
try {
|
|
return fs.readFileSync(STANDARDS_DOC, 'utf-8');
|
|
} catch (err) {
|
|
assert.fail(`docs/contributor-standards.md does not exist: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function parseH2Headings(content) {
|
|
return content
|
|
.split('\n')
|
|
.filter((line) => /^## /.test(line))
|
|
.map((line) => line.replace(/^## /, '').trim());
|
|
}
|
|
|
|
describe('docs/contributor-standards.md', () => {
|
|
test('file exists', () => {
|
|
assert.ok(fs.existsSync(STANDARDS_DOC), 'docs/contributor-standards.md must exist');
|
|
});
|
|
|
|
test('has required CONTEXT.md section', () => {
|
|
const content = readStandardsDoc();
|
|
const headings = parseH2Headings(content);
|
|
const hasContextSection = headings.some((h) => /context/i.test(h));
|
|
assert.ok(
|
|
hasContextSection,
|
|
`Expected an ## heading containing "context" (case-insensitive). Found headings: ${JSON.stringify(headings)}`
|
|
);
|
|
});
|
|
|
|
test('has required ADR section', () => {
|
|
const content = readStandardsDoc();
|
|
const headings = parseH2Headings(content);
|
|
const hasAdrSection = headings.some((h) => /adr/i.test(h));
|
|
assert.ok(
|
|
hasAdrSection,
|
|
`Expected an ## heading containing "ADR" (case-insensitive). Found headings: ${JSON.stringify(headings)}`
|
|
);
|
|
});
|
|
|
|
test('has required AI-agent section', () => {
|
|
const content = readStandardsDoc();
|
|
const headings = parseH2Headings(content);
|
|
const hasAgentSection = headings.some((h) => /ai.?agent|agent.?assist/i.test(h));
|
|
assert.ok(
|
|
hasAgentSection,
|
|
`Expected an ## heading containing "AI-agent" or "agent-assist" (case-insensitive). Found headings: ${JSON.stringify(headings)}`
|
|
);
|
|
});
|
|
|
|
test('references CONTEXT.md', () => {
|
|
const content = readStandardsDoc();
|
|
assert.ok(
|
|
content.includes('CONTEXT.md'),
|
|
'docs/contributor-standards.md must reference CONTEXT.md'
|
|
);
|
|
});
|
|
|
|
test('references docs/adr/', () => {
|
|
const content = readStandardsDoc();
|
|
assert.ok(
|
|
content.includes('docs/adr/'),
|
|
'docs/contributor-standards.md must reference docs/adr/'
|
|
);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
try {
|
|
contributing = fs.readFileSync(CONTRIBUTING_MD, 'utf-8');
|
|
} catch (err) {
|
|
assert.fail(`CONTRIBUTING.md does not exist: ${err.message}`);
|
|
}
|
|
assert.ok(
|
|
contributing.includes('contributor-standards.md'),
|
|
'CONTRIBUTING.md must link to docs/contributor-standards.md'
|
|
);
|
|
});
|
|
});
|