diff --git a/plugins/config-audit/tests/lib/forbidden-words-data.test.mjs b/plugins/config-audit/tests/lib/forbidden-words-data.test.mjs new file mode 100644 index 0000000..5acc9a2 --- /dev/null +++ b/plugins/config-audit/tests/lib/forbidden-words-data.test.mjs @@ -0,0 +1,97 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DATA_PATH = resolve(__dirname, '..', 'lint-forbidden-words.json'); + +async function loadData() { + const raw = await readFile(DATA_PATH, 'utf8'); + return JSON.parse(raw); +} + +test('forbidden-words JSON parses successfully', async () => { + const data = await loadData(); + assert.equal(typeof data, 'object'); + assert.ok(data !== null); +}); + +test('top-level keys present (tier1, tier2, tier3)', async () => { + const data = await loadData(); + assert.ok(Array.isArray(data.tier1), 'tier1 must be an array'); + assert.ok(Array.isArray(data.tier2), 'tier2 must be an array'); + assert.ok(Array.isArray(data.tier3), 'tier3 must be an array'); +}); + +test('tier1 has 19 entries (verbatim from research/03 SC-3 list line 200)', async () => { + const data = await loadData(); + assert.equal(data.tier1.length, 19, `expected 19 tier1 entries, got ${data.tier1.length}`); +}); + +test('tier2 has 24 entries (verbatim from research/03 SC-3 list line 201)', async () => { + const data = await loadData(); + assert.equal(data.tier2.length, 24, `expected 24 tier2 entries, got ${data.tier2.length}`); +}); + +test('tier3 has 12 entries (verbatim from research/03 SC-3 list line 202 + hook)', async () => { + const data = await loadData(); + assert.equal(data.tier3.length, 12, `expected 12 tier3 entries, got ${data.tier3.length}`); +}); + +test('every entry has required fields (word, replacement, source, tier)', async () => { + const data = await loadData(); + for (const tierName of ['tier1', 'tier2', 'tier3']) { + for (const entry of data[tierName]) { + assert.ok(typeof entry.word === 'string' && entry.word.length > 0, + `${tierName} entry missing 'word': ${JSON.stringify(entry)}`); + assert.ok(typeof entry.replacement === 'string' && entry.replacement.length > 0, + `${tierName} entry "${entry.word}" missing 'replacement'`); + assert.ok(typeof entry.source === 'string' && entry.source.length > 0, + `${tierName} entry "${entry.word}" missing 'source'`); + assert.ok(entry.tier === Number(tierName.replace('tier', '')), + `${tierName} entry "${entry.word}" has wrong tier: ${entry.tier}`); + } + } +}); + +test('tier1 spot-check — required absolute prohibitions present', async () => { + const data = await loadData(); + const words = data.tier1.map((e) => e.word); + for (const required of ['utilize', 'leverage', 'facilitate', 'terminate', 'abort', 'invalid', 'illegal', 'failed to', 'fatal', 'in order to']) { + assert.ok(words.includes(required), `tier1 missing required word: ${required}`); + } +}); + +test('tier2 spot-check — condescending words present', async () => { + const data = await loadData(); + const words = data.tier2.map((e) => e.word); + for (const required of ['simply', 'just', 'obviously', 'clearly']) { + assert.ok(words.includes(required), `tier2 missing required word: ${required}`); + } +}); + +test('tier3 spot-check — domain-specific jargon present', async () => { + const data = await loadData(); + const words = data.tier3.map((e) => e.word); + for (const required of ['CLAUDE.md', '@import', 'MCP', 'hook', 'frontmatter']) { + assert.ok(words.includes(required), `tier3 missing required word: ${required}`); + } +}); + +test('no duplicate words across tiers', async () => { + const data = await loadData(); + const allWords = [ + ...data.tier1.map((e) => e.word), + ...data.tier2.map((e) => e.word), + ...data.tier3.map((e) => e.word), + ]; + const seen = new Set(); + const dupes = []; + for (const w of allWords) { + if (seen.has(w)) dupes.push(w); + seen.add(w); + } + assert.equal(dupes.length, 0, `duplicate forbidden words across tiers: ${dupes.join(', ')}`); +}); diff --git a/plugins/config-audit/tests/lint-forbidden-words.json b/plugins/config-audit/tests/lint-forbidden-words.json new file mode 100644 index 0000000..452d78f --- /dev/null +++ b/plugins/config-audit/tests/lint-forbidden-words.json @@ -0,0 +1,64 @@ +{ + "$schema_note": "SC-3 forbidden-words list. Tier 1 = failure if matched in default output; Tier 2 = warning; Tier 3 = failure (allowed in --raw and --json paths). Sources cite at least one official style guide per term. Generated for config-audit v5.1.0 humanizer.", + "tier1": [ + { "word": "utilize", "replacement": "use", "source": "Microsoft Writing Style Guide; Federal Plain Language; GOV.UK; 18F", "tier": 1 }, + { "word": "utilization", "replacement": "use", "source": "Microsoft Writing Style Guide; Federal Plain Language; GOV.UK; 18F", "tier": 1 }, + { "word": "leverage", "replacement": "use, build on", "source": "Microsoft; GOV.UK; Google Developer Style; 18F", "tier": 1 }, + { "word": "facilitate", "replacement": "help", "source": "Microsoft; Federal Plain Language; GOV.UK", "tier": 1 }, + { "word": "terminate", "replacement": "stop, end", "source": "Microsoft UX error guide; Federal Plain Language", "tier": 1 }, + { "word": "abort", "replacement": "stop, cancel, exit", "source": "Google Developer Style; Microsoft", "tier": 1 }, + { "word": "invalid", "replacement": "incorrect, or describe the problem", "source": "Microsoft UX error guide (explicit); Apple HIG", "tier": 1 }, + { "word": "illegal", "replacement": "incorrect", "source": "Microsoft UX error guide (explicit)", "tier": 1 }, + { "word": "failed to", "replacement": "couldn't, unable to", "source": "Microsoft UX error guide (explicit); Federal Plain Language", "tier": 1 }, + { "word": "catastrophic", "replacement": "serious", "source": "Microsoft UX error guide (explicit)", "tier": 1 }, + { "word": "fatal", "replacement": "serious", "source": "Microsoft UX error guide (explicit)", "tier": 1 }, + { "word": "in order to", "replacement": "to", "source": "Federal Plain Language; GOV.UK; Microsoft", "tier": 1 }, + { "word": "prior to", "replacement": "before", "source": "Federal Plain Language; GOV.UK", "tier": 1 }, + { "word": "commence", "replacement": "start, begin", "source": "Federal Plain Language; 18F", "tier": 1 }, + { "word": "endeavor", "replacement": "try", "source": "Federal Plain Language; Microsoft", "tier": 1 }, + { "word": "attempt", "replacement": "try", "source": "Federal Plain Language; Microsoft", "tier": 1 }, + { "word": "oops", "replacement": "(omit)", "source": "Microsoft UX; Apple HIG; Dynamics 365", "tier": 1 }, + { "word": "whoops", "replacement": "(omit)", "source": "Microsoft UX; Apple HIG; Dynamics 365", "tier": 1 }, + { "word": "hmm", "replacement": "(omit)", "source": "Microsoft UX; Dynamics 365", "tier": 1 } + ], + "tier2": [ + { "word": "simply", "replacement": "(omit), or 'straightforward'", "source": "Google Developer Style; Microsoft", "tier": 2 }, + { "word": "just", "replacement": "(omit)", "source": "Google Developer Style; Microsoft", "tier": 2 }, + { "word": "obviously", "replacement": "(omit)", "source": "Google Developer Style; Microsoft", "tier": 2 }, + { "word": "clearly", "replacement": "(omit)", "source": "Google Developer Style; Microsoft", "tier": 2 }, + { "word": "please", "replacement": "(omit in routine output; reserve for genuine inconvenience)", "source": "Microsoft UX; Mailchimp", "tier": 2 }, + { "word": "sorry", "replacement": "(omit in routine output; reserve for serious failure)", "source": "Microsoft UX; Mailchimp", "tier": 2 }, + { "word": "actionable", "replacement": "state the action directly", "source": "Microsoft; Federal Plain Language", "tier": 2 }, + { "word": "functionality", "replacement": "features, capabilities", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "currently", "replacement": "(omit when redundant)", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "note that", "replacement": "(omit)", "source": "Federal Plain Language; Mailchimp", "tier": 2 }, + { "word": "at this time", "replacement": "(omit), or specific time", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "allows you to", "replacement": "lets you", "source": "Microsoft; Mailchimp", "tier": 2 }, + { "word": "ensure", "replacement": "make sure", "source": "Federal Plain Language; Mailchimp", "tier": 2 }, + { "word": "impact", "replacement": "affect (when used as a verb)", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "methodology", "replacement": "method", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "parameters", "replacement": "limits, or specific name (in prose)", "source": "Federal Plain Language; GOV.UK", "tier": 2 }, + { "word": "subsequent", "replacement": "next, later", "source": "Federal Plain Language; GOV.UK", "tier": 2 }, + { "word": "sufficient", "replacement": "enough", "source": "Federal Plain Language; GOV.UK", "tier": 2 }, + { "word": "numerous", "replacement": "many", "source": "Federal Plain Language; GOV.UK", "tier": 2 }, + { "word": "assist", "replacement": "help", "source": "Federal Plain Language; GOV.UK", "tier": 2 }, + { "word": "perform", "replacement": "do, or specific verb (when generic)", "source": "Federal Plain Language; Microsoft", "tier": 2 }, + { "word": "quite", "replacement": "(omit)", "source": "GOV.UK; Mailchimp", "tier": 2 }, + { "word": "very", "replacement": "(omit, or use a stronger word)", "source": "GOV.UK; Mailchimp", "tier": 2 }, + { "word": "really", "replacement": "(omit)", "source": "GOV.UK; Mailchimp", "tier": 2 } + ], + "tier3": [ + { "word": "CLAUDE.md", "replacement": "your project's instructions to Claude, or 'the configuration file'", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "@import", "replacement": "links to another file, or 'this file pulls in'", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "prompt cache", "replacement": "Claude's memory of your setup between turns", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "prompt-cache", "replacement": "Claude's memory of your setup between turns", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "allow/deny", "replacement": "can / cannot use", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "severity", "replacement": "how urgent, or 'impact'", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "finding ID", "replacement": "lead with prose; ID at end-of-line for searchability", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "MCP", "replacement": "external tool, or 'Claude's connection to [service]'", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "hook", "replacement": "automation that runs when [event]", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "frontmatter", "replacement": "the settings at the top of the file", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "schema", "replacement": "expected format, or 'structure'", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 }, + { "word": "scanner", "replacement": "check, or 'the part that looks for X' (in user-facing prose)", "source": "config-audit research/03 Tier 3 jargon table", "tier": 3 } + ] +}