feat(ost): v5.6 C — output-style scanner (CA-OST, count 13→14)
New orchestrated scanner output-style-scanner.mjs — first new family since SKL. Three findings, each pinned to a CONFIRMED V-row of the steering-model plan + re-verified against code.claude.com/docs/en/output-styles: - CA-OST-001 (medium, V10): user/project custom style missing keep-coding-instructions:true (default false) → silently strips built-in software-engineering instructions when active. Scoped to user/project. - CA-OST-002 (low, V11): plugin style with force-for-plugin:true overrides the user's selected outputStyle. Verifiseringsplikt correction — the plan bullet said "project/user style," but force-for-plugin is plugin-styles-only per the docs, so the check keys on source==='plugin'. - CA-OST-003 (medium): settings outputStyle matching no built-in (Default/Explanatory/Learning/Proactive, case-insensitive) nor discovered custom style → dead config. Byte-stability — a scanner addition, not a field addition. Growing the scanners array + scanners_ok cannot be hidden by a field strip, but re-seeding frozen v5.0.0 (the SKL precedent) would now bake in B2's hotspot triple + claudeMd drift. So, per the B2 lesson, frozen v5.0.0 snapshots are PRESERVED and the OST entry is stripped at compare time via new tests/helpers/strip-added-scanner.mjs (wired into json/raw-backcompat + the Step 5/6 humanizer tests); only SC-5 default-output is regenerated (additive OST entry, diff reviewed). OST is fixture-gated (no output styles on marketplace-medium / hermetic HOME → silent). Wiring: orchestrator; humanizer (OST→Configuration mistake) + humanizer-data OST family (title-coupled); scoring (OST→Settings, keeps 10 areas). Suite 1012→1023 (+11). Badges: scanners 14, tests 1023, TRANSLATIONS families 15. Lore swept: README, CLAUDE.md, scanner-internals, humanizer.md. self-audit A/A. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
43d8873339
commit
e3b044a476
20 changed files with 557 additions and 47 deletions
62
tests/helpers/strip-added-scanner.mjs
Normal file
62
tests/helpers/strip-added-scanner.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* v5.6 C added the OST (Output-Style Validation) scanner — a 14th orchestrated
|
||||
* scanner. The frozen v5.0.0 byte-equal baselines predate it (13 scanners), so
|
||||
* this strips the additive OST scanner entry before comparison, the same way
|
||||
* strip-hotspot-load-pattern.mjs handles B2's additive hotspot fields. Stripping
|
||||
* (rather than re-seeding the frozen snapshots) keeps the strongest invariant:
|
||||
* the ORIGINAL 13 scanners still emit byte-identical output, and the frozen
|
||||
* snapshots stay free of post-v5.0.0 drift (the hotspot triple, ancestor token
|
||||
* counts) that re-seeding would bake in.
|
||||
*
|
||||
* Removing a zero-finding scanner entry only moves `aggregate.scanners_ok`
|
||||
* (OST is always status 'ok' and emits nothing on the deterministic fixture, so
|
||||
* total_findings / counts / risk_score / verdict are unchanged). The strip
|
||||
* decrements scanners_ok to match.
|
||||
*
|
||||
* Mutates in place and returns the payload for chaining inside a normalizer:
|
||||
* return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
*/
|
||||
|
||||
const ADDED_SCANNERS = new Set(['OST']);
|
||||
|
||||
function stripFromEnvelope(env) {
|
||||
if (!env || typeof env !== 'object' || !Array.isArray(env.scanners)) return;
|
||||
let removedOk = 0;
|
||||
const kept = [];
|
||||
for (const s of env.scanners) {
|
||||
if (ADDED_SCANNERS.has(s.scanner)) {
|
||||
if (s.status === 'ok') removedOk++;
|
||||
continue;
|
||||
}
|
||||
kept.push(s);
|
||||
}
|
||||
env.scanners = kept;
|
||||
if (env.aggregate && typeof env.aggregate.scanners_ok === 'number') {
|
||||
env.aggregate.scanners_ok -= removedOk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip added (post-v5.0.0) scanner entries from a CLI payload / envelope —
|
||||
* both top-level (`scanners`) and nested (`scannerEnvelope.scanners`).
|
||||
* @template T
|
||||
* @param {T} payload
|
||||
* @returns {T}
|
||||
*/
|
||||
export function stripAddedScanners(payload) {
|
||||
if (!payload || typeof payload !== 'object') return payload;
|
||||
stripFromEnvelope(payload);
|
||||
if (payload.scannerEnvelope) stripFromEnvelope(payload.scannerEnvelope);
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `[OST] …` per-scanner progress lines from a captured stderr scorecard,
|
||||
* so a 14-scanner live run matches the frozen 13-scanner stderr snapshot.
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function stripAddedScannerStderr(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
return text.replace(/^[ \t]*\[OST\][^\n]*\n/gm, '');
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import { readFile, writeFile, access, mkdir } from 'node:fs/promises';
|
|||
import { homedir } from 'node:os';
|
||||
import { hermeticEnv, HERMETIC_HOME } from './helpers/hermetic-home.mjs';
|
||||
import { stripHotspotLoadPattern } from './helpers/strip-hotspot-load-pattern.mjs';
|
||||
import { stripAddedScanners, stripAddedScannerStderr } from './helpers/strip-added-scanner.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -103,7 +104,7 @@ function normalizeScanOrchestrator(env) {
|
|||
}
|
||||
}
|
||||
stripAncestorDerived(out);
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
function normalizePosture(p) {
|
||||
|
|
@ -120,7 +121,7 @@ function normalizePosture(p) {
|
|||
}
|
||||
stripAncestorDerived(out.scannerEnvelope);
|
||||
}
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
function normalizeTokenHotspots(p) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { TRANSLATIONS } from '../../scanners/lib/humanizer-data.mjs';
|
|||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const FORBIDDEN_PATH = resolve(__dirname, '..', 'lint-forbidden-words.json');
|
||||
|
||||
const EXPECTED_SCANNERS = ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'CNF', 'GAP', 'TOK', 'CPS', 'DIS', 'COL', 'PLH', 'SKL'];
|
||||
const EXPECTED_SCANNERS = ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'CNF', 'GAP', 'TOK', 'CPS', 'DIS', 'COL', 'PLH', 'SKL', 'OST'];
|
||||
|
||||
function stripBacktickSpans(s) {
|
||||
return s.replace(/`[^`]*`/g, '');
|
||||
|
|
@ -24,7 +24,7 @@ test('TRANSLATIONS exports an object', () => {
|
|||
assert.ok(TRANSLATIONS !== null);
|
||||
});
|
||||
|
||||
test('TRANSLATIONS covers all 14 expected scanner prefixes', () => {
|
||||
test('TRANSLATIONS covers all 15 expected scanner prefixes', () => {
|
||||
for (const prefix of EXPECTED_SCANNERS) {
|
||||
assert.ok(TRANSLATIONS[prefix], `missing scanner prefix: ${prefix}`);
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ test('no translated string contains tier3 jargon (outside backtick spans)', asyn
|
|||
|
||||
test('CML, SET, HKV, RUL, MCP, IMP, GAP, TOK, PLH have non-empty static maps', () => {
|
||||
// These scanners produce findings with titles we documented. Empty static map suggests missed coverage.
|
||||
for (const prefix of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'GAP', 'TOK', 'PLH']) {
|
||||
for (const prefix of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'GAP', 'TOK', 'PLH', 'OST']) {
|
||||
const count = Object.keys(TRANSLATIONS[prefix].static).length;
|
||||
assert.ok(count > 0, `${prefix}.static is empty — expected at least 1 translated title`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,8 +157,8 @@ test('humanizeFinding falls back to "FYI" for unknown severity', () => {
|
|||
|
||||
// ─── userImpactCategory ────────────────────────────────────────────────
|
||||
|
||||
test('humanizeFinding sets category Configuration mistake for CML/SET/HKV/RUL/MCP/IMP/PLH', () => {
|
||||
for (const s of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'PLH']) {
|
||||
test('humanizeFinding sets category Configuration mistake for CML/SET/HKV/RUL/MCP/IMP/PLH/OST', () => {
|
||||
for (const s of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'PLH', 'OST']) {
|
||||
const out = humanizeFinding(makeFinding({ scanner: s }));
|
||||
assert.equal(out.userImpactCategory, 'Configuration mistake', `${s} should map to Configuration mistake`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { readFile, writeFile, access, mkdir } from 'node:fs/promises';
|
|||
import { homedir } from 'node:os';
|
||||
import { hermeticEnv, HERMETIC_HOME } from './helpers/hermetic-home.mjs';
|
||||
import { stripHotspotLoadPattern } from './helpers/strip-hotspot-load-pattern.mjs';
|
||||
import { stripAddedScanners, stripAddedScannerStderr } from './helpers/strip-added-scanner.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -98,7 +99,7 @@ function normalizeScanOrchestrator(env) {
|
|||
}
|
||||
}
|
||||
stripAncestorDerived(out);
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
function normalizePosture(p) {
|
||||
|
|
@ -115,7 +116,7 @@ function normalizePosture(p) {
|
|||
}
|
||||
stripAncestorDerived(out.scannerEnvelope);
|
||||
}
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
function normalizeTokenHotspots(p) {
|
||||
|
|
@ -275,8 +276,8 @@ describe('SC-7 --raw posture stderr scorecard verbatim', () => {
|
|||
}
|
||||
const expected = await readFile(stderrSnapshotPath, 'utf8');
|
||||
assert.equal(
|
||||
normalizeStderrDurations(stderr.trim()),
|
||||
normalizeStderrDurations(expected.trim()),
|
||||
normalizeStderrDurations(stripAddedScannerStderr(stderr.trim())),
|
||||
normalizeStderrDurations(stripAddedScannerStderr(expected.trim())),
|
||||
'posture --raw stderr must reproduce the v5.0.0 scorecard verbatim (apart from durations)',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
169
tests/scanners/output-style-scanner.test.mjs
Normal file
169
tests/scanners/output-style-scanner.test.mjs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* OST scanner tests — output-style validation (v5.6 C).
|
||||
*
|
||||
* Each finding has a positive fixture (fires) and a negative fixture (silent),
|
||||
* per the v5.5+ plan acceptance criteria. The scanner reads the active config
|
||||
* (project + user + plugin output styles, settings cascade) via HOME, so every
|
||||
* test builds a hermetic temp HOME + repo, overrides process.env.HOME, runs,
|
||||
* then restores and cleans up — never touching real user state.
|
||||
*
|
||||
* CA-OST-001 user/project custom style missing keep-coding-instructions:true (medium)
|
||||
* CA-OST-002 plugin output style with force-for-plugin:true (low)
|
||||
* CA-OST-003 settings outputStyle resolving to a non-existent style (medium)
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'node:path';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||
import { scan } from '../../scanners/output-style-scanner.mjs';
|
||||
|
||||
function tmp(prefix) {
|
||||
return mkdtemp(join(tmpdir(), `ca-ost-${prefix}-`));
|
||||
}
|
||||
|
||||
async function writeStyle(dir, name, frontmatter) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
const fm = frontmatter ?? `name: ${name}\ndescription: test style`;
|
||||
await writeFile(join(dir, `${name}.md`), `---\n${fm}\n---\n\nStyle body for ${name}.\n`, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a hermetic HOME + repo from a spec, run the OST scanner, restore + clean.
|
||||
* @param {object} opts
|
||||
* @param {Array<{name:string, frontmatter?:string}>} [opts.projectStyles]
|
||||
* @param {Array<{name:string, frontmatter?:string}>} [opts.userStyles]
|
||||
* @param {Array<{plugin:string, name:string, frontmatter?:string}>} [opts.pluginStyles]
|
||||
* @param {object} [opts.projectSettings]
|
||||
*/
|
||||
async function runOst(opts = {}) {
|
||||
const home = await tmp('home');
|
||||
const repo = await tmp('repo');
|
||||
|
||||
for (const s of opts.projectStyles || []) {
|
||||
await writeStyle(join(repo, '.claude', 'output-styles'), s.name, s.frontmatter);
|
||||
}
|
||||
for (const s of opts.userStyles || []) {
|
||||
await writeStyle(join(home, '.claude', 'output-styles'), s.name, s.frontmatter);
|
||||
}
|
||||
for (const s of opts.pluginStyles || []) {
|
||||
const root = join(home, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins', s.plugin);
|
||||
await mkdir(join(root, '.claude-plugin'), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, '.claude-plugin', 'plugin.json'),
|
||||
JSON.stringify({ name: s.plugin, version: '1.0.0' }),
|
||||
'utf-8',
|
||||
);
|
||||
await writeStyle(join(root, 'output-styles'), s.name, s.frontmatter);
|
||||
}
|
||||
if (opts.projectSettings) {
|
||||
await mkdir(join(repo, '.claude'), { recursive: true });
|
||||
await writeFile(join(repo, '.claude', 'settings.json'), JSON.stringify(opts.projectSettings), 'utf-8');
|
||||
}
|
||||
|
||||
const originalHome = process.env.HOME;
|
||||
const originalProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
resetCounter();
|
||||
try {
|
||||
return await scan(repo, { files: [] });
|
||||
} finally {
|
||||
process.env.HOME = originalHome;
|
||||
process.env.USERPROFILE = originalProfile;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
await rm(repo, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const has = (result, re) => result.findings.find(f => re.test(String(f.title || '')));
|
||||
|
||||
describe('OST scanner — basic structure', () => {
|
||||
it('reports scanner prefix OST', async () => {
|
||||
const result = await runOst({ projectStyles: [{ name: 'plain' }] });
|
||||
assert.equal(result.scanner, 'OST');
|
||||
assert.equal(result.status, 'ok');
|
||||
});
|
||||
|
||||
it('finding IDs match CA-OST-NNN', async () => {
|
||||
const result = await runOst({ projectStyles: [{ name: 'plain' }] });
|
||||
for (const f of result.findings) {
|
||||
assert.match(f.id, /^CA-OST-\d{3}$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('is fixture-gated — no output styles, no settings → zero findings', async () => {
|
||||
const result = await runOst({});
|
||||
assert.equal(result.findings.length, 0, `expected silent; got: ${result.findings.map(f => f.title).join(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OST scanner — CA-OST-001 keep-coding-instructions', () => {
|
||||
it('flags a project custom style missing keep-coding-instructions:true (medium)', async () => {
|
||||
const result = await runOst({ projectStyles: [{ name: 'myco' }] });
|
||||
const f = has(result, /coding instructions/i);
|
||||
assert.ok(f, `expected OST-001; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
assert.equal(f.severity, 'medium');
|
||||
assert.match(String(f.file), /myco\.md$/);
|
||||
});
|
||||
|
||||
it('flags a user custom style missing the flag (source=user in evidence)', async () => {
|
||||
const result = await runOst({ userStyles: [{ name: 'mine' }] });
|
||||
const f = has(result, /coding instructions/i);
|
||||
assert.ok(f);
|
||||
assert.match(String(f.evidence), /source=user/);
|
||||
});
|
||||
|
||||
it('does NOT flag a style that sets keep-coding-instructions:true', async () => {
|
||||
const result = await runOst({
|
||||
projectStyles: [{ name: 'keeper', frontmatter: 'name: keeper\ndescription: test\nkeep-coding-instructions: true' }],
|
||||
});
|
||||
assert.equal(has(result, /coding instructions/i), undefined,
|
||||
`expected silent; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OST scanner — CA-OST-002 force-for-plugin', () => {
|
||||
it('flags a plugin style with force-for-plugin:true (low, names the plugin)', async () => {
|
||||
const result = await runOst({
|
||||
pluginStyles: [{ plugin: 'styler', name: 'forced', frontmatter: 'name: forced\ndescription: test\nforce-for-plugin: true' }],
|
||||
});
|
||||
const f = has(result, /override/i);
|
||||
assert.ok(f, `expected OST-002; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
assert.equal(f.severity, 'low');
|
||||
assert.match(String(f.evidence), /styler/);
|
||||
});
|
||||
|
||||
it('does NOT flag a plugin style without force-for-plugin (and OST-001 stays project/user-only)', async () => {
|
||||
const result = await runOst({
|
||||
pluginStyles: [{ plugin: 'styler', name: 'plain', frontmatter: 'name: plain\ndescription: test' }],
|
||||
});
|
||||
assert.equal(result.findings.length, 0,
|
||||
`expected silent for a plain plugin style; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OST scanner — CA-OST-003 dead outputStyle', () => {
|
||||
it('flags settings.outputStyle that resolves to no known style (medium)', async () => {
|
||||
const result = await runOst({ projectSettings: { outputStyle: 'Ghostwriter' } });
|
||||
const f = has(result, /does not exist/i);
|
||||
assert.ok(f, `expected OST-003; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
assert.equal(f.severity, 'medium');
|
||||
assert.match(String(f.evidence), /Ghostwriter/);
|
||||
});
|
||||
|
||||
it('does NOT flag a built-in style name (Explanatory)', async () => {
|
||||
const result = await runOst({ projectSettings: { outputStyle: 'Explanatory' } });
|
||||
assert.equal(has(result, /does not exist/i), undefined);
|
||||
});
|
||||
|
||||
it('does NOT flag an outputStyle matching an existing custom style', async () => {
|
||||
const result = await runOst({
|
||||
projectStyles: [{ name: 'myco', frontmatter: 'name: myco\ndescription: test\nkeep-coding-instructions: true' }],
|
||||
projectSettings: { outputStyle: 'myco' },
|
||||
});
|
||||
assert.equal(has(result, /does not exist/i), undefined,
|
||||
`expected no dead-config finding; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|||
import { readFile, unlink } from 'node:fs/promises';
|
||||
import { hermeticEnv } from '../helpers/hermetic-home.mjs';
|
||||
import { stripHotspotLoadPattern } from '../helpers/strip-hotspot-load-pattern.mjs';
|
||||
import { stripAddedScanners, stripAddedScannerStderr } from '../helpers/strip-added-scanner.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -39,12 +40,16 @@ function normalizePosture(p) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
/** Strip time-varying durations (Xms) so progress lines compare verbatim across runs. */
|
||||
/**
|
||||
* Strip time-varying durations (Xms) so progress lines compare verbatim across
|
||||
* runs, and drop the additive [OST] progress line (v5.6 C) so a 14-scanner live
|
||||
* run matches the frozen 13-scanner v5.0.0 stderr scorecard.
|
||||
*/
|
||||
function normalizeStderr(s) {
|
||||
return s.replace(/\(\d+ms\)/g, '(0ms)');
|
||||
return stripAddedScannerStderr(s).replace(/\(\d+ms\)/g, '(0ms)');
|
||||
}
|
||||
|
||||
async function runPosture(flags) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|||
import { readFile, unlink } from 'node:fs/promises';
|
||||
import { hermeticEnv } from '../helpers/hermetic-home.mjs';
|
||||
import { stripHotspotLoadPattern } from '../helpers/strip-hotspot-load-pattern.mjs';
|
||||
import { stripAddedScanners } from '../helpers/strip-added-scanner.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -37,7 +38,7 @@ function normalizeEnvelope(env) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
async function runOrchestrator(flags) {
|
||||
|
|
@ -102,7 +103,7 @@ describe('scan-orchestrator humanizer wiring (Step 5)', () => {
|
|||
assert.ok(actual.meta, 'meta present');
|
||||
assert.ok(Array.isArray(actual.scanners), 'scanners array present');
|
||||
assert.ok(actual.aggregate, 'aggregate present');
|
||||
assert.equal(actual.scanners.length, 13, 'all 13 scanners present');
|
||||
assert.equal(actual.scanners.length, 14, 'all 14 scanners present');
|
||||
});
|
||||
|
||||
it('preserves scanner shape (scanner/status/findings/counts)', async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"kind": "text",
|
||||
"payload": "`[CML] CLAUDE.md Linter`: 1 finding(s) (0ms)\n `[SET] Settings Validator`: 0 finding(s) (0ms)\n `[HKV] Hook Validator`: 0 finding(s) (0ms)\n `[RUL] Rules Validator`: 0 finding(s) (0ms)\n `[MCP] MCP Config Validator`: 0 finding(s) (0ms)\n `[IMP] Import Resolver`: 0 finding(s) (0ms)\n `[CNF] Conflict Detector`: 0 finding(s) (0ms)\n `[GAP] Feature Gap Scanner`: 17 finding(s) (0ms)\n `[TOK] Token Hotspots`: 1 finding(s) (0ms)\n `[CPS] Cache-Prefix Stability`: 0 finding(s) (0ms)\n `[DIS] Disabled-In-Schema`: 0 finding(s) (0ms)\n `[COL] Plugin Skill Collision`: 0 finding(s) (0ms)\n `[SKL] Skill-Listing Budget`: 0 finding(s) (0ms)\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Configuration health\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Health: A (98/100) — Healthy setup, only minor polish needed\n 9 areas reviewed\n\n Area scores\n ───────────\n `CLAUDE.md` ........... A (90) `Settings` ............ A (100)\n `Hooks` ............... A (100) `Rules` ............... A (100)\n `MCP` ................. A (100) `Imports` ............. A (100)\n `Conflicts` ........... A (100) `Token Efficiency` .... A (90)\n `Plugin Hygiene` ...... A (100)\n\n 17 ways you could get more out of Claude Code — see /config-audit feature-gap\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
"payload": "`[CML] CLAUDE.md Linter`: 1 finding(s) (0ms)\n `[SET] Settings Validator`: 0 finding(s) (0ms)\n `[HKV] Hook Validator`: 0 finding(s) (0ms)\n `[RUL] Rules Validator`: 0 finding(s) (0ms)\n `[MCP] MCP Config Validator`: 0 finding(s) (0ms)\n `[IMP] Import Resolver`: 0 finding(s) (0ms)\n `[CNF] Conflict Detector`: 0 finding(s) (0ms)\n `[GAP] Feature Gap Scanner`: 17 finding(s) (0ms)\n `[TOK] Token Hotspots`: 1 finding(s) (0ms)\n `[CPS] Cache-Prefix Stability`: 0 finding(s) (0ms)\n `[DIS] Disabled-In-Schema`: 0 finding(s) (0ms)\n `[COL] Plugin Skill Collision`: 0 finding(s) (0ms)\n `[SKL] Skill-Listing Budget`: 0 finding(s) (0ms)\n `[OST] Output-Style Validation`: 0 finding(s) (0ms)\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Configuration health\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Health: A (98/100) — Healthy setup, only minor polish needed\n 9 areas reviewed\n\n Area scores\n ───────────\n `CLAUDE.md` ........... A (90) `Settings` ............ A (100)\n `Hooks` ............... A (100) `Rules` ............... A (100)\n `MCP` ................. A (100) `Imports` ............. A (100)\n `Conflicts` ........... A (100) `Token Efficiency` .... A (90)\n `Plugin Hygiene` ...... A (100)\n\n 17 ways you could get more out of Claude Code — see /config-audit feature-gap\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,6 +568,20 @@
|
|||
"low": 0,
|
||||
"info": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"scanner": "OST",
|
||||
"status": "ok",
|
||||
"files_scanned": 0,
|
||||
"duration_ms": 0,
|
||||
"findings": [],
|
||||
"counts": {
|
||||
"critical": 0,
|
||||
"high": 0,
|
||||
"medium": 0,
|
||||
"low": 0,
|
||||
"info": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregate": {
|
||||
|
|
@ -582,7 +596,7 @@
|
|||
"risk_score": 11,
|
||||
"risk_band": "Medium",
|
||||
"verdict": "PASS",
|
||||
"scanners_ok": 12,
|
||||
"scanners_ok": 13,
|
||||
"scanners_error": 0,
|
||||
"scanners_skipped": 1
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue