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
|
|
@ -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