fix(engine): FILE-MISSING named the class for a trait-sourced requirement

security -> SECURITY.md read as "missing required file for class
`standalone`", sending the operator to a class definition that never
listed the requirement. requirementsFor now carries each required
file's origin (class vs. trait) and checkRequiredFiles names whichever
actually required it. Failing test written first per the Iron Law.

No release forced by this alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuWwACwhun35j52AnTxRKV
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 18:51:21 +02:00
commit e5cd1bfaaf
4 changed files with 35 additions and 6 deletions

View file

@ -6,6 +6,15 @@ versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- **`FILE-MISSING` named the class even when a TRAIT required the file.**
`security``SECURITY.md` read as "missing required file for class
`standalone`", sending the operator looking for a requirement the class
never listed. `requirementsFor` now carries the source (`class` vs. `trait`)
alongside each required file, and the message names whichever actually
required it. 245 tests, from 243.
## [0.11.1] — 2026-08-13
### Fixed

View file

@ -344,7 +344,7 @@ would recreate, in data, exactly the drift this plugin exists to remove.
## Commands
```bash
npm test # 243 tests
npm test # 245 tests
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo
node scripts/repo-standard-check.mjs --offline # no network call
node scripts/repo-standard-check.mjs --json # machine output

View file

@ -538,12 +538,19 @@ export function checkInstallPins({ readme, forgeTagsByRepo }, register) {
// outsider who finds a hole, and being solo does not remove them.
function requirementsFor(klass, traits, register) {
const cls = register.classes?.[klass] ?? {};
const files = [...(cls.required_files ?? [])];
const classSource = `class \`${klass}\``;
const files = (cls.required_files ?? []).map((file) => ({ file, source: classSource }));
const headings = [...(cls.required_headings ?? [])];
const seenFiles = new Set(files.map((f) => f.file));
for (const t of traits ?? []) {
const tr = register.trait_requirements?.[t];
if (!tr) continue;
for (const f of tr.required_files ?? []) if (!files.includes(f)) files.push(f);
const traitSource = `trait \`${t}\``;
for (const f of tr.required_files ?? []) {
if (seenFiles.has(f)) continue;
seenFiles.add(f);
files.push({ file: f, source: traitSource });
}
for (const h of tr.required_headings ?? []) if (!headings.includes(h)) headings.push(h);
}
return { files, headings };
@ -553,9 +560,9 @@ export function checkRequiredFiles({ present, klass, traits }, register) {
const { files: required } = requirementsFor(klass, traits, register);
const have = new Set(present ?? []);
const findings = [];
for (const f of required) {
if (!have.has(f)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', bucket: 'missing', msg: `missing required file for class \`${klass}\`: ${f}` });
for (const { file, source } of required) {
if (!have.has(file)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', bucket: 'missing', msg: `missing required file for ${source}: ${file}` });
}
}
if (findings.length === 0 && required.length > 0) {

View file

@ -1480,6 +1480,19 @@ test('a repo without the security trait owes no SECURITY.md', () => {
assert.equal(f.some((x) => x.msg.includes('SECURITY.md')), false);
});
test('a trait-required file is missing, FILE-MISSING names the trait, not the class', () => {
const f = checkRequiredFiles({ present: ['README.md', 'LICENSE'], klass: 'standalone', traits: ['security'] }, REGISTER);
const finding = f.find((x) => x.code === 'FILE-MISSING' && x.msg.includes('SECURITY.md'));
assert.equal(finding.msg.includes('trait `security`'), true);
assert.equal(finding.msg.includes('class `standalone`'), false);
});
test('a class-required file is missing, FILE-MISSING still names the class', () => {
const f = checkRequiredFiles({ present: [], klass: 'standalone' }, REGISTER);
const finding = f.find((x) => x.code === 'FILE-MISSING' && x.msg.includes('LICENSE'));
assert.equal(finding.msg.includes('class `standalone`'), true);
});
test('the security trait requires limitations to be stated', () => {
const f = checkHeadings({ readme: '# x\n## Install\n## Non-goals\n', klass: 'standalone', traits: ['security'] }, REGISTER);
assert.equal(f.some((x) => x.code === 'HEADING-MISSING' && x.msg.includes('Known limitations')), true);