fix(scanners): retire the autoMode GAP dimension, a /doctor duplicate (D1)
CC 2.1.226's /doctor Check 8 covers auto mode with usage-weighted judgement. The binding positioning forbids carrying a feature whose whole value is duplicating a /doctor check, so the "adopt this feature" nudge goes. The deterministic side stays: SET still validates autoMode structure and still flags it as dead config in shared project settings. GAP dimensions 25 -> 24. The title lived in FOUR tables, not the two the removal was scoped against: the dimension list, scoring TITLE_TO_ID, the humanizer's static translations, and the scoring denominators (TIER_COUNTS t3 8->7, TOTAL_DIMENSIONS 25->24, MAX_WEIGHTED 42->41) -- the one that moves a user-visible number. findGapId falls back to 'unknown' silently, so a partial removal would have degraded without failing. A blanket sync invariant now asserts all four against GAP_CHECKS instead of comparing occurrences pairwise; each arm was verified red against its own defect (denominator drift, orphaned humanizer entry, resurrected dimension). Frozen tests/snapshots/v5.0.0/ stays untouched. strip-retired-gap.mjs is the removal twin of strip-added-scanner.mjs: it strips the retired dimension from whichever side still carries it and re-derives GAP IDs, since retiring a dimension from mid-list shifts every later ID by one. Derived utilization figures are dropped from comparison rather than recomputed -- recomputing them in a test helper would assert the new arithmetic against itself, and scoring.test.mjs already pins them exactly. Re-seeding was rejected: it would silently bake in any other drift across every scanner those four files cover. risk_score, risk_band, verdict, overallGrade, maturity and segment are byte-identical across the change (severity info carries zero risk weight; GAP is excluded from the overall grade). Utilization shifts 43 -> 44 on the fixture. D2 (CA-SKL-002) is NOT removed. Verified against the primary source first: the CC changelog carries exactly one budget-fraction statement (L3786, 2.1.32) and nothing supersedes it, so our 2% is current and 002 is not a duplicate with a stale figure. /doctor's ~1% could not be reconciled from the changelog and it discloses its own numbers as disk estimates, so it is recorded, not adopted. Left explicitly unverified in a code note: L3786 says "character budget" while we express tokens -- a 4x difference nobody can settle from the wording. Suite 1531 -> 1535, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsfPGxgwbR3MY54wDC6hat
This commit is contained in:
parent
182a37c1af
commit
4027cdcf54
17 changed files with 368 additions and 75 deletions
29
CHANGELOG.md
29
CHANGELOG.md
|
|
@ -84,6 +84,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
shape (`{qualityAreaCount}`, never emitted) and now takes its count from the humanized scorecard
|
||||
rather than `areas.length`, which counts a Feature Coverage row the table below deliberately excludes.
|
||||
|
||||
### Removed
|
||||
- **GAP dimension `No autoMode classifier` (D1) — retired as a `/doctor` duplicate.** CC 2.1.226's
|
||||
`/doctor` Check 8 covers auto mode with usage-weighted judgement, and the binding positioning
|
||||
(README «config-audit vs. the built-in /doctor») forbids carrying a feature whose whole value is
|
||||
duplicating a `/doctor` check. What is retired is only the *"adopt this feature"* nudge; the
|
||||
deterministic side stays untouched — SET still validates `autoMode` structure and still flags it
|
||||
as dead config in shared project settings. GAP dimensions: **25 → 24**.
|
||||
|
||||
The title lived in **four** tables, not the two the removal was scoped against: the dimension
|
||||
list, the scoring `TITLE_TO_ID` map, the humanizer's static translations, and — the one that
|
||||
moves a user-visible number — the scoring denominators (`TIER_COUNTS` t3 8→7,
|
||||
`TOTAL_DIMENSIONS` 25→24, `MAX_WEIGHTED` 42→41). `findGapId` falls back to `'unknown'` silently,
|
||||
so a partial removal would have degraded without failing. A blanket sync invariant now asserts
|
||||
all four against `GAP_CHECKS` rather than checking occurrences pairwise, and each arm was
|
||||
verified red against its own defect.
|
||||
|
||||
Utilization shifts accordingly (fixture: 43 → 44). `risk_score`, `risk_band`, `verdict`,
|
||||
`overallGrade`, `maturity` and `segment` are byte-identical across the change — the dimension was
|
||||
severity `info` (zero risk weight) and GAP is excluded from the overall grade.
|
||||
|
||||
**Frozen `tests/snapshots/v5.0.0/` stays untouched.** The removal-twin normalizer
|
||||
(`tests/helpers/strip-retired-gap.mjs`, mirroring `strip-added-scanner.mjs`) strips the retired
|
||||
dimension from whichever side still carries it and re-derives GAP IDs — retiring a dimension from
|
||||
mid-list shifts every later ID by one. The derived utilization figures are dropped from
|
||||
comparison rather than recomputed, since recomputing them in a test helper would assert the new
|
||||
arithmetic against itself; they are covered exactly in `tests/lib/scoring.test.mjs`. Re-seeding
|
||||
the baselines was rejected: it would silently bake in any other drift accumulated across every
|
||||
scanner those four files cover.
|
||||
|
||||
### Added
|
||||
- Four command-template shape tests (1449 → 1453), each verified to fail before the fix: a blanket
|
||||
`$$` ban, stdout-redirect discipline for any scanner invoked with `--output-file` in raw/json mode,
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ The CLI tools work standalone — no Claude Code session needed, just Node.js 18
|
|||
|
||||
Most configuration tools stop at "is it valid?" Config-audit goes further: **what could improve your setup, and is it relevant to your project?**
|
||||
|
||||
The feature opportunity scanner checks 25 dimensions and groups recommendations by impact:
|
||||
The feature opportunity scanner checks 24 dimensions and groups recommendations by impact:
|
||||
|
||||
| Impact Level | Focus | Examples |
|
||||
|--------------|-------|---------|
|
||||
|
|
@ -296,7 +296,7 @@ By default, `/config-audit` auto-detects scope from your git context. Override w
|
|||
| `mcp-config-validator.mjs` | MCP | Invalid server types, exposed env vars, unknown fields |
|
||||
| `import-resolver.mjs` | IMP | Broken @imports, circular references, deep chains, tilde path issues |
|
||||
| `conflict-detector.mjs` | CNF | Settings contradictions across scopes, permission conflicts, hook duplicates |
|
||||
| `feature-gap-scanner.mjs` | GAP | 25 feature checks shown as opportunities, not grades — plus a conditional `disableBundledSkills` recommendation when the active skill listing is over budget, and a conditional **filter-before-Claude-reads** lever when a hook injects unfiltered output into `additionalContext` (companion to the HKV advisory; cites the documented `filter-test-output.sh` pattern) |
|
||||
| `feature-gap-scanner.mjs` | GAP | 24 feature checks shown as opportunities, not grades — plus a conditional `disableBundledSkills` recommendation when the active skill listing is over budget, and a conditional **filter-before-Claude-reads** lever when a hook injects unfiltered output into `additionalContext` (companion to the HKV advisory; cites the documented `filter-test-output.sh` pattern) |
|
||||
| `token-hotspots.mjs` | TOK | Cache-breaking volatile content, redundant tool permissions, deep import chains, oversized cascades, bloated skill descriptions, MCP tool-schema budget, and stale `~/.claude/plugins/cache` versions (disk-cleanup, zero live-context impact) — cache-aware ranking excludes superseded plugin versions by default (`--no-exclude-cache` to include) |
|
||||
| `cache-prefix-scanner.mjs` | CPS | Volatile content in lines 31–150 of the CLAUDE.md cascade — beyond Pattern A's top-30 window but still re-loaded every turn — **plus** volatile content inside `@import`-ed files (inlined into the cached prefix, one hop, otherwise invisible to per-file scans) |
|
||||
| `disabled-in-schema-scanner.mjs` | DIS | Dead/ineffective permission entries: (1) tools in BOTH `permissions.deny` and `permissions.allow` — deny wins (incl. the `Tool(*)` deny-all glob, equivalent to a bare deny); (2) unanchored allow wildcards (`*`, `B*`, `mcp__*`) that Claude Code silently skips — valid only as `mcp__<server>__*`; (3) `Tool(param:value)` rules whose key is the tool's own canonicalizing field (`command`/`file_path`/`path`/`notebook_path`/`url`) — CC ignores these and emits a startup warning |
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Scanner CLI: `node scanners/scan-orchestrator.mjs <path> [--global] [--full-mach
|
|||
| `mcp-config-validator.mjs` | MCP | Server types, env vars, unknown fields |
|
||||
| `import-resolver.mjs` | IMP | Broken @imports, circular refs, deep chains, tilde paths |
|
||||
| `conflict-detector.mjs` | CNF | Settings conflicts, permission contradictions, hook duplicates |
|
||||
| `feature-gap-scanner.mjs` | GAP | 25 feature checks across 4 tiers — shown as opportunities, not grades |
|
||||
| `feature-gap-scanner.mjs` | GAP | 24 feature checks across 4 tiers — shown as opportunities, not grades |
|
||||
| `token-hotspots.mjs` | TOK | Cache-breaking volatile content, redundant tool permissions, deep import chains, oversized cascade, bloated SKILL.md descriptions, MCP tool-schema budget, MCP tool-schema deferral (CA-TOK-006), stale plugin-cache disk-cleanup (prompt-cache patterns) |
|
||||
| `cache-prefix-scanner.mjs` | CPS | Volatile content in lines 31–150 of CLAUDE.md cascade (beyond Pattern A's top-30 window); plus volatile content inside `@import`-ed files (v5.10 B6, one hop) |
|
||||
| `disabled-in-schema-scanner.mjs` | DIS | Dead/ineffective permission entries (low). (1) Tools in BOTH `permissions.deny` AND `permissions.allow` — deny wins; dominance is param-aware and treats the `Tool(*)` deny-all glob as equivalent to a bare deny (covers a bare allow). (2) Unanchored allow wildcards (`*`, `B*`, `mcp__*`) that Claude Code silently skips — CC accepts allow globs only after a literal glob-free `mcp__<server>__` prefix. Predicates shared with CNF live in `lib/permission-rules.mjs` |
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export function filterHookLeverFinding({ flaggedHooks } = {}) {
|
|||
}
|
||||
|
||||
/** @type {GapCheck[]} */
|
||||
const GAP_CHECKS = [
|
||||
export const GAP_CHECKS = [
|
||||
// --- Tier 1: Foundation ---
|
||||
{
|
||||
id: 't1_1', tier: 't1',
|
||||
|
|
@ -480,12 +480,11 @@ const GAP_CHECKS = [
|
|||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 't3_8', tier: 't3',
|
||||
title: 'No autoMode classifier',
|
||||
recommendation: 'Configure autoMode in user/local settings with environment context and allow/deny rules.',
|
||||
check: async (ctx) => anySettingsHas(ctx, 'autoMode'),
|
||||
},
|
||||
// t3_8 ('No autoMode classifier') retired in v5.14: CC 2.1.226's /doctor
|
||||
// Check 8 covers auto mode with usage-weighted judgement, so an "adopt this
|
||||
// feature" nudge is a pure duplicate under the binding /doctor positioning.
|
||||
// The DETERMINISTIC side stays: SET still validates autoMode structure and
|
||||
// flags it as dead config in shared project settings.
|
||||
|
||||
// --- Tier 4: Team/Enterprise ---
|
||||
{
|
||||
|
|
|
|||
|
|
@ -499,11 +499,6 @@ export const TRANSLATIONS = {
|
|||
description: 'Dynamic context lets a skill see fresh information (file contents, command output) at the moment it runs, not at the time it was written.',
|
||||
recommendation: 'Use the dynamic-context block in skills that need up-to-date information.',
|
||||
},
|
||||
'No autoMode classifier': {
|
||||
title: 'You haven\'t set up auto-mode classification',
|
||||
description: 'Auto-mode classification helps Claude decide when to act on its own vs. ask you, based on the kind of task.',
|
||||
recommendation: 'Add an auto-mode classifier in your settings if you want this nuance.',
|
||||
},
|
||||
'No project .mcp.json in git': {
|
||||
title: 'Your team has no shared list of connected services',
|
||||
description: 'Without a project-level connected-services file, every teammate has to set up their own connections.',
|
||||
|
|
|
|||
|
|
@ -20,17 +20,17 @@ const GRADE_CONTEXT = {
|
|||
|
||||
// --- Tier weights for utilization calculation ---
|
||||
const TIER_WEIGHTS = { t1: 3, t2: 2, t3: 1, t4: 1 };
|
||||
const TIER_COUNTS = { t1: 5, t2: 7, t3: 8, t4: 5 };
|
||||
const TOTAL_DIMENSIONS = 25;
|
||||
const TIER_COUNTS = { t1: 5, t2: 7, t3: 7, t4: 5 };
|
||||
const TOTAL_DIMENSIONS = 24;
|
||||
const MAX_WEIGHTED = Object.entries(TIER_COUNTS).reduce(
|
||||
(sum, [tier, count]) => sum + count * TIER_WEIGHTS[tier],
|
||||
0,
|
||||
); // 5*3 + 7*2 + 8*1 + 5*1 = 42
|
||||
); // 5*3 + 7*2 + 7*1 + 5*1 = 41
|
||||
|
||||
/**
|
||||
* Calculate weighted utilization from GAP scanner findings.
|
||||
* @param {object[]} gapFindings - Array of GAP scanner findings (each has .category = t1|t2|t3|t4)
|
||||
* @param {number} [totalDimensions=25]
|
||||
* @param {number} [totalDimensions=24]
|
||||
* @returns {{ score: number, overhang: number }}
|
||||
*/
|
||||
export function calculateUtilization(gapFindings, totalDimensions = TOTAL_DIMENSIONS) {
|
||||
|
|
@ -102,7 +102,7 @@ function findGapId(finding) {
|
|||
return TITLE_TO_ID[finding.title] || 'unknown';
|
||||
}
|
||||
|
||||
/** Title→ID mapping for all 25 gap checks */
|
||||
/** Title→ID mapping for all 24 gap checks */
|
||||
const TITLE_TO_ID = {
|
||||
'No CLAUDE.md file': 't1_1',
|
||||
'No permissions configured': 't1_2',
|
||||
|
|
@ -123,7 +123,6 @@ const TITLE_TO_ID = {
|
|||
'No advanced skill frontmatter': 't3_5',
|
||||
'No subagent isolation': 't3_6',
|
||||
'No dynamic skill context': 't3_7',
|
||||
'No autoMode classifier': 't3_8',
|
||||
'No project .mcp.json in git': 't4_1',
|
||||
'No custom plugin': 't4_2',
|
||||
'Agent teams not enabled': 't4_3',
|
||||
|
|
@ -404,4 +403,4 @@ export function generateHealthScorecard(areaScores, opportunityCount, options =
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export { TITLE_TO_ID, TIER_WEIGHTS, TIER_COUNTS, MAX_WEIGHTED, MATURITY_LEVELS, SEGMENTS };
|
||||
export { TITLE_TO_ID, TIER_WEIGHTS, TIER_COUNTS, TOTAL_DIMENSIONS, MAX_WEIGHTED, MATURITY_LEVELS, SEGMENTS };
|
||||
|
|
|
|||
|
|
@ -36,6 +36,23 @@ export const DESCRIPTION_CAP = 1536;
|
|||
// The 200k/1M window constants live in context-window.mjs (single source of
|
||||
// truth, shared with the CML CLAUDE.md char-budget check); re-exported here so
|
||||
// existing importers of this module keep working.
|
||||
// D2 re-verification (2026-08-09, CC 2.1.226). CC 2.1.226's /doctor reports a
|
||||
// combined skill+command+agent listing and puts the budget near ~1% (~10,000
|
||||
// tok on a 1M window) — half of ours. Checked against the primary source
|
||||
// before touching the number: the CC changelog contains EXACTLY ONE
|
||||
// budget-fraction statement (L3786, under 2.1.32) and no later entry
|
||||
// supersedes it, so 2% stands and CA-SKL-002 is NOT a /doctor duplicate
|
||||
// carrying a stale figure. /doctor's arithmetic could not be reconciled from
|
||||
// the changelog, and /doctor discloses its own numbers as disk estimates
|
||||
// (chars÷4), so its ~1% is recorded, not adopted.
|
||||
//
|
||||
// NOT VERIFIED, deliberately left alone: L3786 says "skill CHARACTER budget
|
||||
// now scales with context window (2% of context)". We express the budget in
|
||||
// TOKENS (0.02 × 200k = 4000 tok). Whether CC's budget is 2% counted in
|
||||
// characters or in tokens is not resolvable from the changelog wording, and no
|
||||
// primary source settles it — a 4× difference rides on the answer. Changing
|
||||
// the constant on that ambiguity would be a guess; it stays until a primary
|
||||
// source decides it.
|
||||
export const BUDGET_FRACTION = 0.02;
|
||||
export const AGGREGATE_BUDGET_TOKENS = Math.round(BUDGET_FRACTION * CONTEXT_WINDOW_ANCHOR); // 4000
|
||||
export const LARGE_CONTEXT_BUDGET_TOKENS = Math.round(BUDGET_FRACTION * LARGE_CONTEXT_WINDOW); // 20000
|
||||
|
|
|
|||
203
tests/helpers/strip-retired-gap.mjs
Normal file
203
tests/helpers/strip-retired-gap.mjs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/**
|
||||
* The REMOVAL twin of strip-added-scanner.mjs.
|
||||
*
|
||||
* The frozen v5.0.0 baselines predate v5.14's retirement of the GAP dimension
|
||||
* `No autoMode classifier` (a pure /doctor Check 8 duplicate under the binding
|
||||
* positioning). Where strip-added-scanner.mjs removes post-v5.0.0 ADDITIONS
|
||||
* from live output so it matches the frozen expectation, this removes a
|
||||
* RETIRED dimension from whichever side still carries it — the frozen
|
||||
* expectation. Both sides run through the same normalizer, so every operation
|
||||
* here must be a no-op on live output; that constraint is what dictates the
|
||||
* three shapes below.
|
||||
*
|
||||
* Why strip rather than re-seed: re-seeding these four baselines would bake in
|
||||
* whatever OTHER drift has accumulated since v5.0.0 across every scanner they
|
||||
* cover, silently. Stripping keeps the strongest invariant — every byte the
|
||||
* original scanners emit is still compared — and confines the exception to the
|
||||
* one dimension we deliberately deleted.
|
||||
*
|
||||
* Three payload shapes carry GAP findings, all derived from one scan and all
|
||||
* therefore in emission order:
|
||||
* - envelopes (scan-orchestrator, posture) — `scanners[GAP].findings`
|
||||
* - drift — flat `newFindings`/`resolvedFindings`/`unchangedFindings`/`movedFindings`
|
||||
* - fix-cli — `manual[]` entries keyed `findingId`
|
||||
* A single traversal handles all three rather than three special cases.
|
||||
*
|
||||
* What is adjusted, and what is deliberately NOT:
|
||||
* - the retired finding is dropped, and GAP IDs are re-derived densely in
|
||||
* traversal order. GAP IDs are already position-derived (`output.mjs`
|
||||
* assigns them from an emission counter), so this re-derivation applies the
|
||||
* scanner's own rule, not a new one. Retiring a dimension from the middle
|
||||
* of the list shifts every later ID by one.
|
||||
* - counter bookkeeping is decremented mechanically, exactly as
|
||||
* strip-added-scanner.mjs decrements `scanners_ok`.
|
||||
* - the DERIVED utilization figures (`utilization.score`/`overhang` and the
|
||||
* feature_coverage area `score`) are dropped from comparison instead of
|
||||
* recomputed. Recomputing them inside a test helper would assert the new
|
||||
* arithmetic against itself. They are covered exhaustively by dedicated
|
||||
* unit tests in tests/lib/scoring.test.mjs (exact expected values per
|
||||
* tier), which is the right place for arithmetic.
|
||||
* - risk_score, risk_band, verdict, overallGrade, maturity, segment and
|
||||
* drift's scoreChange are left alone: the retired dimension was
|
||||
* severity `info`, which carries zero risk weight, and GAP is excluded from
|
||||
* the overall grade (scoring.mjs). Measured 2026-08-09: all byte-identical
|
||||
* across the change.
|
||||
*
|
||||
* Mutates in place and returns the payload for chaining inside a normalizer.
|
||||
*/
|
||||
|
||||
const RETIRED_GAP_TITLES = new Set(['No autoMode classifier']);
|
||||
|
||||
const GAP_ID_RE = /^CA-GAP-\d+$/;
|
||||
|
||||
/** Decrement a numeric field by n when it exists. */
|
||||
function decrement(obj, key, n) {
|
||||
if (obj && typeof obj[key] === 'number') obj[key] -= n;
|
||||
}
|
||||
|
||||
/**
|
||||
* True for a FINDING-shaped object belonging to the GAP scanner.
|
||||
*
|
||||
* The `title` requirement is load-bearing: the scanner envelope entry also
|
||||
* carries `scanner: 'GAP'`, and counting it as a finding shifts every
|
||||
* re-derived ID by one.
|
||||
*/
|
||||
function isGapEntry(o) {
|
||||
if (!o || typeof o !== 'object' || typeof o.title !== 'string') return false;
|
||||
if (o.scanner === 'GAP') return true;
|
||||
return GAP_ID_RE.test(o.id || '') || GAP_ID_RE.test(o.findingId || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the payload, removing retired GAP entries from every array and
|
||||
* collecting the survivors in traversal (= emission) order.
|
||||
*/
|
||||
function walk(node, state) {
|
||||
if (Array.isArray(node)) {
|
||||
for (let i = node.length - 1; i >= 0; i--) {
|
||||
const item = node[i];
|
||||
if (isGapEntry(item) && RETIRED_GAP_TITLES.has(item.title)) {
|
||||
state.removed++;
|
||||
if (item.severity) {
|
||||
state.bySeverity[item.severity] = (state.bySeverity[item.severity] || 0) + 1;
|
||||
}
|
||||
node.splice(i, 1);
|
||||
}
|
||||
}
|
||||
for (const item of node) {
|
||||
if (isGapEntry(item)) state.survivors.push(item);
|
||||
walk(item, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node && typeof node === 'object') {
|
||||
for (const v of Object.values(node)) walk(v, state);
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrement every counter location a retired GAP finding contributes to. */
|
||||
function adjustCounters(payload, state) {
|
||||
const { removed, bySeverity } = state;
|
||||
|
||||
const envelopes = [payload, payload.scannerEnvelope].filter(
|
||||
e => e && typeof e === 'object',
|
||||
);
|
||||
for (const env of envelopes) {
|
||||
if (Array.isArray(env.scanners)) {
|
||||
for (const s of env.scanners) {
|
||||
if (s && s.scanner === 'GAP') {
|
||||
for (const [sev, n] of Object.entries(bySeverity)) decrement(s.counts, sev, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (env.aggregate) {
|
||||
decrement(env.aggregate, 'total_findings', removed);
|
||||
for (const [sev, n] of Object.entries(bySeverity)) decrement(env.aggregate.counts, sev, n);
|
||||
}
|
||||
}
|
||||
|
||||
// Posture-shaped roll-ups above the envelope.
|
||||
decrement(payload, 'opportunityCount', removed);
|
||||
if (Array.isArray(payload.areas)) {
|
||||
for (const a of payload.areas) {
|
||||
if (a && a.id === 'feature_coverage') decrement(a, 'findingCount', removed);
|
||||
}
|
||||
}
|
||||
|
||||
// Drift totals.
|
||||
if (payload.summary) {
|
||||
decrement(payload.summary, 'totalBefore', removed);
|
||||
decrement(payload.summary, 'totalAfter', removed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip retired GAP dimensions from any CLI payload and drop the derived
|
||||
* utilization figures from comparison.
|
||||
* @template T
|
||||
* @param {T} payload
|
||||
* @returns {T}
|
||||
*/
|
||||
export function stripRetiredGap(payload) {
|
||||
if (!payload || typeof payload !== 'object') return payload;
|
||||
|
||||
const state = { removed: 0, bySeverity: {}, survivors: [] };
|
||||
walk(payload, state);
|
||||
|
||||
if (state.removed > 0) {
|
||||
// Re-derive GAP IDs densely — the same position rule output.mjs applies.
|
||||
state.survivors.forEach((f, i) => {
|
||||
const id = `CA-GAP-${String(i + 1).padStart(3, '0')}`;
|
||||
if ('id' in f) f.id = id;
|
||||
if ('findingId' in f) f.findingId = id;
|
||||
});
|
||||
adjustCounters(payload, state);
|
||||
}
|
||||
|
||||
// Derived scores: dropped on BOTH sides unconditionally, so live and frozen
|
||||
// stay symmetric. See the header note.
|
||||
if (payload.utilization && typeof payload.utilization === 'object') {
|
||||
delete payload.utilization.score;
|
||||
delete payload.utilization.overhang;
|
||||
}
|
||||
if (Array.isArray(payload.areas)) {
|
||||
for (const a of payload.areas) {
|
||||
if (a && a.id === 'feature_coverage') delete a.score;
|
||||
}
|
||||
}
|
||||
// Drift reports the same derived score under a different key shape.
|
||||
if (Array.isArray(payload.areaChanges)) {
|
||||
for (const a of payload.areaChanges) {
|
||||
if (a && a.name === 'Feature Coverage') {
|
||||
delete a.before?.score;
|
||||
delete a.after?.score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask the GAP finding tally in a captured stderr scorecard.
|
||||
*
|
||||
* The scorecard prints only a count, with nothing in the text identifying WHICH
|
||||
* dimensions it covers — so unlike the line-removal in stripAddedScannerStderr,
|
||||
* a decrement here could not be a no-op on the live side. Both sides go through
|
||||
* the same normalizer, so the tally is masked rather than adjusted; the exact
|
||||
* GAP count is asserted on the structured payload, where the findings
|
||||
* themselves are compared.
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function maskGapTallyStderr(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
return text
|
||||
.replace(
|
||||
/^(\s*`?\[GAP\][^\n]*?)\d+ finding\(s\)/gm,
|
||||
(_m, head) => `${head}<GAPCOUNT> finding(s)`,
|
||||
)
|
||||
// The scorecard footer repeats the tally, in a raw and a humanized wording.
|
||||
.replace(/^(\s*)\d+( opportunities available)/gm, '$1<GAPCOUNT>$2')
|
||||
.replace(/^(\s*)\d+( ways you could get more out of)/gm, '$1<GAPCOUNT>$2');
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ 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';
|
||||
import { stripRetiredGap } from './helpers/strip-retired-gap.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -104,7 +105,7 @@ function normalizeScanOrchestrator(env) {
|
|||
}
|
||||
}
|
||||
stripAncestorDerived(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
function normalizePosture(p) {
|
||||
|
|
@ -121,7 +122,7 @@ function normalizePosture(p) {
|
|||
}
|
||||
stripAncestorDerived(out.scannerEnvelope);
|
||||
}
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
function normalizeTokenHotspots(p) {
|
||||
|
|
@ -132,12 +133,12 @@ function normalizeTokenHotspots(p) {
|
|||
|
||||
function normalizeDrift(p) {
|
||||
// Drift result has no time fields — round-trip through JSON for safety.
|
||||
return JSON.parse(JSON.stringify(p));
|
||||
return stripRetiredGap(JSON.parse(JSON.stringify(p)));
|
||||
}
|
||||
|
||||
function normalizeFix(p) {
|
||||
// Fix-cli stdout is the planFixes result with no time fields.
|
||||
return JSON.parse(JSON.stringify(p));
|
||||
return stripRetiredGap(JSON.parse(JSON.stringify(p)));
|
||||
}
|
||||
|
||||
function normalizePluginHealth(p) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ describe('calculateUtilization', () => {
|
|||
assert.equal(result.overhang, 0);
|
||||
});
|
||||
|
||||
it('returns 0% when all 25 dimensions are gaps', () => {
|
||||
it('returns 0% when all 24 dimensions are gaps', () => {
|
||||
const result = calculateUtilization(allGapFindings());
|
||||
assert.equal(result.score, 0);
|
||||
assert.equal(result.overhang, 100);
|
||||
|
|
@ -101,24 +101,24 @@ describe('calculateUtilization', () => {
|
|||
it('weighs T1 gaps heavier (3x)', () => {
|
||||
const onlyT1 = t1GapFindings(); // 5 T1 gaps = 15 weight lost
|
||||
const result = calculateUtilization(onlyT1);
|
||||
// Lost: 5 × 3 = 15 out of 42. Present: 27/42 = 64%
|
||||
assert.equal(result.score, 64);
|
||||
// Lost: 5 × 3 = 15 out of 41. Present: 26/41 = 63%
|
||||
assert.equal(result.score, 63);
|
||||
});
|
||||
|
||||
it('weighs T4 gaps lighter (1x)', () => {
|
||||
const onlyT4 = t4GapFindings(); // 5 T4 gaps = 5 weight lost
|
||||
const result = calculateUtilization(onlyT4);
|
||||
// Lost: 5 × 1 = 5 out of 42. Present: 37/42 = 88%
|
||||
// Lost: 5 × 1 = 5 out of 41. Present: 36/41 = 88%
|
||||
assert.equal(result.score, 88);
|
||||
});
|
||||
|
||||
it('T1+T2 present but no T3+T4 scores ~69%', () => {
|
||||
// T3: 8 dims × 1 = 8, T4: 5 dims × 1 = 5. Lost = 13 out of 42. Present = 29/42 = 69%
|
||||
it('T1+T2 present but no T3+T4 scores ~71%', () => {
|
||||
// T3: 7 dims × 1 = 7, T4: 5 dims × 1 = 5. Lost = 12 out of 41. Present = 29/41 = 71%
|
||||
const t3t4Gaps = Object.entries(TITLE_TO_ID)
|
||||
.filter(([, id]) => id.startsWith('t3') || id.startsWith('t4'))
|
||||
.map(([title, id]) => makeGapFinding(title, id.split('_')[0]));
|
||||
const result = calculateUtilization(t3t4Gaps);
|
||||
assert.equal(result.score, 69);
|
||||
assert.equal(result.score, 71);
|
||||
});
|
||||
|
||||
it('score + overhang = 100', () => {
|
||||
|
|
@ -581,17 +581,17 @@ describe('generateHealthScorecard', () => {
|
|||
// Constants and exports
|
||||
// ========================================
|
||||
describe('scoring constants', () => {
|
||||
it('TITLE_TO_ID has 25 entries', () => {
|
||||
assert.equal(Object.keys(TITLE_TO_ID).length, 25);
|
||||
it('TITLE_TO_ID has 24 entries', () => {
|
||||
assert.equal(Object.keys(TITLE_TO_ID).length, 24);
|
||||
});
|
||||
|
||||
it('TIER_COUNTS sum to 25', () => {
|
||||
it('TIER_COUNTS sum to 24', () => {
|
||||
const sum = Object.values(TIER_COUNTS).reduce((a, b) => a + b, 0);
|
||||
assert.equal(sum, 25);
|
||||
assert.equal(sum, 24);
|
||||
});
|
||||
|
||||
it('MAX_WEIGHTED is 42', () => {
|
||||
assert.equal(MAX_WEIGHTED, 42);
|
||||
it('MAX_WEIGHTED is 41', () => {
|
||||
assert.equal(MAX_WEIGHTED, 41);
|
||||
});
|
||||
|
||||
it('TIER_WEIGHTS match spec', () => {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ 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';
|
||||
import { stripRetiredGap, maskGapTallyStderr } from './helpers/strip-retired-gap.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -99,7 +100,7 @@ function normalizeScanOrchestrator(env) {
|
|||
}
|
||||
}
|
||||
stripAncestorDerived(out);
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
function normalizePosture(p) {
|
||||
|
|
@ -116,7 +117,7 @@ function normalizePosture(p) {
|
|||
}
|
||||
stripAncestorDerived(out.scannerEnvelope);
|
||||
}
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
function normalizeTokenHotspots(p) {
|
||||
|
|
@ -126,11 +127,11 @@ function normalizeTokenHotspots(p) {
|
|||
}
|
||||
|
||||
function normalizeDrift(p) {
|
||||
return JSON.parse(JSON.stringify(p));
|
||||
return stripRetiredGap(JSON.parse(JSON.stringify(p)));
|
||||
}
|
||||
|
||||
function normalizeFix(p) {
|
||||
return JSON.parse(JSON.stringify(p));
|
||||
return stripRetiredGap(JSON.parse(JSON.stringify(p)));
|
||||
}
|
||||
|
||||
function normalizePluginHealth(p) {
|
||||
|
|
@ -276,8 +277,8 @@ describe('SC-7 --raw posture stderr scorecard verbatim', () => {
|
|||
}
|
||||
const expected = await readFile(stderrSnapshotPath, 'utf8');
|
||||
assert.equal(
|
||||
normalizeStderrDurations(stripAddedScannerStderr(stderr.trim())),
|
||||
normalizeStderrDurations(stripAddedScannerStderr(expected.trim())),
|
||||
normalizeStderrDurations(maskGapTallyStderr(stripAddedScannerStderr(stderr.trim()))),
|
||||
normalizeStderrDurations(maskGapTallyStderr(stripAddedScannerStderr(expected.trim()))),
|
||||
'posture --raw stderr must reproduce the v5.0.0 scorecard verbatim (apart from durations)',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|||
import { readFile, writeFile, unlink, mkdir, access } from 'node:fs/promises';
|
||||
import { hermeticEnv, HERMETIC_HOME } from '../helpers/hermetic-home.mjs';
|
||||
import { stripHotspotLoadPattern } from '../helpers/strip-hotspot-load-pattern.mjs';
|
||||
import { stripRetiredGap } from '../helpers/strip-retired-gap.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -43,7 +44,7 @@ async function runCli(cliPath, args, env = {}) {
|
|||
function normalizeTokenHotspotsPayload(p) {
|
||||
const out = JSON.parse(JSON.stringify(p));
|
||||
out.duration_ms = 0;
|
||||
return stripHotspotLoadPattern(out);
|
||||
return stripRetiredGap(stripHotspotLoadPattern(out));
|
||||
}
|
||||
|
||||
function normalizeManifestOutput(o) {
|
||||
|
|
@ -76,7 +77,7 @@ function normalizePluginHealthOutput(o) {
|
|||
|
||||
function normalizeDriftOutput(o) {
|
||||
// Drift result has no time fields; just round-trip through JSON.
|
||||
return JSON.parse(JSON.stringify(o));
|
||||
return stripRetiredGap(JSON.parse(JSON.stringify(o)));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -318,14 +319,14 @@ describe('fix-cli humanizer (Step 7)', () => {
|
|||
const { stdout } = await runCli(CLI, [FIXTURE, '--json']);
|
||||
const actual = JSON.parse(stdout);
|
||||
const expected = JSON.parse(await readFile(SNAPSHOT, 'utf-8'));
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
assert.deepStrictEqual(stripRetiredGap(actual), stripRetiredGap(expected));
|
||||
});
|
||||
|
||||
it('--raw: stdout JSON byte-equal v5.0.0 snapshot', async () => {
|
||||
const { stdout } = await runCli(CLI, [FIXTURE, '--raw']);
|
||||
const actual = JSON.parse(stdout);
|
||||
const expected = JSON.parse(await readFile(SNAPSHOT, 'utf-8'));
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
assert.deepStrictEqual(stripRetiredGap(actual), stripRetiredGap(expected));
|
||||
});
|
||||
|
||||
it('default mode stderr differs from --raw stderr when findings have humanizer translations', async () => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import { fileURLToPath } from 'node:url';
|
|||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||
import { scan, opportunitySummary, bundledSkillsLeverFinding, cliOverMcpLeverFinding, filterHookLeverFinding } from '../../scanners/feature-gap-scanner.mjs';
|
||||
import { scan, opportunitySummary, bundledSkillsLeverFinding, cliOverMcpLeverFinding, filterHookLeverFinding, GAP_CHECKS } from '../../scanners/feature-gap-scanner.mjs';
|
||||
import { TITLE_TO_ID as GAP_TITLE_TO_ID, TIER_COUNTS, TOTAL_DIMENSIONS } from '../../scanners/lib/scoring.mjs';
|
||||
import { TRANSLATIONS } from '../../scanners/lib/humanizer-data.mjs';
|
||||
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
||||
import { withHermeticHome } from '../helpers/hermetic-home.mjs';
|
||||
|
||||
|
|
@ -506,3 +508,63 @@ describe('GAP scanner — test/demo data must not mask real gaps (M-BUG-13)', ()
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D1 — retired GAP dimensions.
|
||||
*
|
||||
* The binding /doctor positioning (README «config-audit vs. the built-in
|
||||
* /doctor») forbids carrying a dimension whose whole value is duplicating a
|
||||
* /doctor check. `No autoMode classifier` was retired when CC 2.1.226's
|
||||
* /doctor Check 8 was measured to cover auto mode with usage-weighted
|
||||
* judgement (re-measured 2026-08-09; the deterministic SET `autoMode`
|
||||
* structure/dead-config validation is NOT a duplicate and stays).
|
||||
*
|
||||
* The title lived in THREE tables, not two: the dimension list, the scoring
|
||||
* title→id map, and the humanizer's static translations. `findGapId` falls
|
||||
* back to 'unknown' silently, so a partial removal degrades without failing —
|
||||
* hence the blanket sync invariant below rather than three separate absence
|
||||
* checks.
|
||||
*/
|
||||
describe('GAP scanner — retired dimensions (D1)', () => {
|
||||
const RETIRED_TITLES = ['No autoMode classifier'];
|
||||
|
||||
it('emits no retired dimension on a fixture that lacks the feature', async () => {
|
||||
resetCounter();
|
||||
const discovery = await fixtureDiscovery('healthy-project');
|
||||
const result = await withHermeticHome(
|
||||
() => scan(resolve(FIXTURES, 'healthy-project'), discovery),
|
||||
);
|
||||
const titles = result.findings.map(f => f.title);
|
||||
for (const t of RETIRED_TITLES) {
|
||||
assert.ok(!titles.includes(t), `retired dimension still emitted: ${t}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves no orphaned entry in any of the three tables', () => {
|
||||
for (const t of RETIRED_TITLES) {
|
||||
assert.ok(!GAP_CHECKS.some(g => g.title === t), `still in GAP_CHECKS: ${t}`);
|
||||
assert.ok(!(t in GAP_TITLE_TO_ID), `orphan in scoring TITLE_TO_ID: ${t}`);
|
||||
assert.ok(!(t in TRANSLATIONS.GAP.static), `orphan in humanizer TRANSLATIONS.GAP: ${t}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the three title tables carrying exactly the same titles', () => {
|
||||
const checks = GAP_CHECKS.map(g => g.title).sort();
|
||||
const scoring = Object.keys(GAP_TITLE_TO_ID).sort();
|
||||
const humanizer = Object.keys(TRANSLATIONS.GAP.static).sort();
|
||||
assert.deepEqual(scoring, checks, 'scoring TITLE_TO_ID drifted from GAP_CHECKS');
|
||||
assert.deepEqual(humanizer, checks, 'humanizer TRANSLATIONS.GAP drifted from GAP_CHECKS');
|
||||
});
|
||||
|
||||
// The scoring denominators are a FOURTH copy of the dimension inventory, and
|
||||
// the one that silently moves every user's utilization score: the score is
|
||||
// presentWeight / MAX_WEIGHTED, derived from TIER_COUNTS. Adding or retiring a
|
||||
// dimension without updating these skews the score instead of failing.
|
||||
it('keeps the scoring denominators in step with the dimension inventory', () => {
|
||||
const perTier = { t1: 0, t2: 0, t3: 0, t4: 0 };
|
||||
for (const g of GAP_CHECKS) perTier[g.tier]++;
|
||||
assert.deepEqual(TIER_COUNTS, perTier, 'TIER_COUNTS drifted from GAP_CHECKS tiers');
|
||||
assert.equal(TOTAL_DIMENSIONS, GAP_CHECKS.length,
|
||||
'TOTAL_DIMENSIONS drifted from GAP_CHECKS length');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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';
|
||||
import { stripRetiredGap, maskGapTallyStderr } from '../helpers/strip-retired-gap.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -41,7 +42,7 @@ function normalizePosture(p) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,7 +51,7 @@ function normalizePosture(p) {
|
|||
* run matches the frozen 13-scanner v5.0.0 stderr scorecard.
|
||||
*/
|
||||
function normalizeStderr(s) {
|
||||
return stripAddedScannerStderr(s).replace(/\(\d+ms\)/g, '(0ms)');
|
||||
return maskGapTallyStderr(stripAddedScannerStderr(s)).replace(/\(\d+ms\)/g, '(0ms)');
|
||||
}
|
||||
|
||||
async function runPosture(flags) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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';
|
||||
import { stripRetiredGap } from '../helpers/strip-retired-gap.mjs';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -38,7 +39,7 @@ function normalizeEnvelope(env) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
return stripRetiredGap(stripAddedScanners(stripHotspotLoadPattern(out)));
|
||||
}
|
||||
|
||||
async function runOrchestrator(flags) {
|
||||
|
|
|
|||
|
|
@ -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 `[AGT] Agent-Listing Budget`: 0 finding(s) (0ms)\n `[OST] Output-Style Validation`: 0 finding(s) (0ms)\n `[OPT] Optimization Lens`: 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`: 16 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 `[AGT] Agent-Listing Budget`: 0 finding(s) (0ms)\n `[OST] Output-Style Validation`: 0 finding(s) (0ms)\n `[OPT] Optimization Lens`: 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 16 ways you could get more out of Claude Code — see /config-audit feature-gap\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,22 +341,6 @@
|
|||
"id": "CA-GAP-014",
|
||||
"scanner": "GAP",
|
||||
"severity": "info",
|
||||
"title": "You haven't set up auto-mode classification",
|
||||
"description": "Auto-mode classification helps Claude decide when to act on its own vs. ask you, based on the kind of task.",
|
||||
"file": null,
|
||||
"line": null,
|
||||
"evidence": null,
|
||||
"category": "t3",
|
||||
"recommendation": "Add an auto-mode classifier in your settings if you want this nuance.",
|
||||
"autoFixable": false,
|
||||
"userImpactCategory": "Missed opportunity",
|
||||
"userActionLanguage": "FYI",
|
||||
"relevanceContext": "affects-everyone"
|
||||
},
|
||||
{
|
||||
"id": "CA-GAP-015",
|
||||
"scanner": "GAP",
|
||||
"severity": "info",
|
||||
"title": "You haven't built a custom plugin yet",
|
||||
"description": "Plugins let you bundle skills, automations, and connected services that you want available across many projects.",
|
||||
"file": null,
|
||||
|
|
@ -370,7 +354,7 @@
|
|||
"relevanceContext": "affects-everyone"
|
||||
},
|
||||
{
|
||||
"id": "CA-GAP-016",
|
||||
"id": "CA-GAP-015",
|
||||
"scanner": "GAP",
|
||||
"severity": "info",
|
||||
"title": "Your project has no settings managed by your organization",
|
||||
|
|
@ -386,7 +370,7 @@
|
|||
"relevanceContext": "affects-everyone"
|
||||
},
|
||||
{
|
||||
"id": "CA-GAP-017",
|
||||
"id": "CA-GAP-016",
|
||||
"scanner": "GAP",
|
||||
"severity": "info",
|
||||
"title": "You haven't connected Claude to your editor's language servers",
|
||||
|
|
@ -407,7 +391,7 @@
|
|||
"high": 0,
|
||||
"medium": 1,
|
||||
"low": 5,
|
||||
"info": 11
|
||||
"info": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -613,13 +597,13 @@
|
|||
}
|
||||
],
|
||||
"aggregate": {
|
||||
"total_findings": 19,
|
||||
"total_findings": 18,
|
||||
"counts": {
|
||||
"critical": 0,
|
||||
"high": 0,
|
||||
"medium": 1,
|
||||
"low": 7,
|
||||
"info": 11
|
||||
"info": 10
|
||||
},
|
||||
"risk_score": 11,
|
||||
"risk_band": "Medium",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue