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:
parent
f422b63bb3
commit
e5cd1bfaaf
4 changed files with 35 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue