feat(tok,acr): v5.6 B2 — load-pattern column in token-hotspots

Annotate every ranked TOK hotspot with the load-pattern triple
(loadPattern/survivesCompaction/derivationConfidence):

- hotspotLoadPattern() maps each discovery `type` → a deriveLoadPattern kind.
  Rules reuse activeConfig.rules for precise `scoped` handling; claude-md maps
  by scope. Two new deriveLoadPattern kinds back the rest: `command`
  (on-demand — body loads on /invoke) and `harness-config` (external —
  settings/keybindings/.mcp.json/hooks.json/plugin.json configure the CLI, not
  the model context, so they cost no per-turn context tokens). Honest split:
  the .mcp.json FILE is external; the MCP server's tool schemas are a separate
  `always` hotspot.

Byte-stability — the opposite of B1's manifest. token-hotspots IS a byte-equal
SC-6/SC-7 CLI, and its hotspots ride inside scan-orchestrator + posture, so the
change touched SIX frozen-v5.0.0 comparisons across five test files. Resolved by
preserving the frozen baselines: a shared tests/helpers/strip-hotspot-load-pattern.mjs
strips the additive triple before each byte-equal compare (proves the original
schema is byte-identical). SC-5 default-output snapshots (scan-orchestrator +
token-hotspots) regenerated — diff reviewed as additive-only.

Tests 1008→1012. Self-audit A/A, scanner count unchanged at 13 (C bumps to 14).
Completes v5.6 B (B1 manifest + B2 token-hotspots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-20 20:21:01 +02:00
commit 778b517e6f
17 changed files with 236 additions and 40 deletions

View file

@ -17,7 +17,7 @@ Analyzes and optimizes Claude Code configuration across three pillars:
|---------|-------------|
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
| `/config-audit posture` | Quick health scorecard (A-F grades, 10 quality areas incl. Token Efficiency, Plugin Hygiene) |
| `/config-audit tokens` | prompt-cache-aware token hotspots (6 patterns: cache-breaking, redundant perms, deep imports, oversized cascade, bloated SKILL.md desc, MCP tool-schema budget) — optional `--accurate-tokens` API calibration, `--with-telemetry-recipe` cache-hit recipe pointer |
| `/config-audit tokens` | prompt-cache-aware token hotspots (6 patterns: cache-breaking, redundant perms, deep imports, oversized cascade, bloated SKILL.md desc, MCP tool-schema budget), each ranked hotspot tagged with its load pattern (always / on-demand / external) — optional `--accurate-tokens` API calibration, `--with-telemetry-recipe` cache-hit recipe pointer |
| `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens, each tagged with its load pattern (always-loaded / on-demand / external) + an always-loaded subtotal ("tokens that enter context every turn") |
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact (incl. a conditional `disableBundledSkills` lever when the active skill listing is over budget — remediation companion to SKL `CA-SKL-002`) |
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
@ -109,7 +109,7 @@ Default: auto-detects scope from git context. Override with `/config-audit full|
node --test 'tests/**/*.test.mjs'
```
1008 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
1012 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
### active-config-reader — load-pattern model + rule/agent/output-style enumeration (v5.6 Foundation)
@ -154,8 +154,26 @@ list becomes an array.
**mode-equivalence** (`--json == --raw`), not byte-equal against a frozen snapshot, and it is not in
SC-5 default-output. Adding fields in place therefore keeps all snapshots green with **no regen**
(verified). `total` changes (de-duped, component-level) — that is the intended correctness fix.
**TOK's load-pattern column is deferred to the next chunk** (it *is* a byte-equal SC-6 CLI, so it
needs snapshot handling); this chunk is manifest-only.
### token-hotspots — load-pattern column (v5.6 B2)
TOK now annotates every ranked hotspot with the same load-pattern triple (`hotspotLoadPattern`
maps each discovery `type`→a `deriveLoadPattern` kind; rules reuse `activeConfig.rules` for precise
`scoped` handling; `claude-md` maps by scope). Two new `deriveLoadPattern` kinds back this:
**`command`** (on-demand — body loads on `/invoke`) and **`harness-config`** (external — settings/
keybindings/`.mcp.json`/hooks.json/plugin.json configure the CLI, **not** the model context, so they
cost no per-turn context tokens). Note the honest split: the `.mcp.json` **file** is `external`,
while the MCP **server**'s tool schemas are a separate `always` hotspot.
**Byte-stability — the opposite of manifest.** token-hotspots **is** a byte-equal SC-6/SC-7 CLI,
**and** its hotspots ride inside the scan-orchestrator + posture payloads, so the change broke
**six** frozen-v5.0.0 comparisons across five test files (json/raw-backcompat + the three Step 5/6/7
humanizer tests). Resolved by **preserving the frozen v5.0.0 baselines**: a shared
`tests/helpers/strip-hotspot-load-pattern.mjs` strips the additive triple before each byte-equal
compare (proves the original schema is byte-identical), and the **SC-5 default-output** snapshots
(scan-orchestrator + token-hotspots) were **regenerated** (`UPDATE_SNAPSHOT=1`) since their job is to
track current output — diff reviewed as additive-only. **Lesson for any future hotspot/scanner-output
field:** grep every frozen-v5.0.0 comparator (it is 5 files, not 2) before assuming the blast radius.
### CML scanner — context-window-scaled char budget

View file

@ -12,7 +12,7 @@
![Commands](https://img.shields.io/badge/commands-18-green)
![Agents](https://img.shields.io/badge/agents-6-orange)
![Hooks](https://img.shields.io/badge/hooks-4-red)
![Tests](https://img.shields.io/badge/tests-1008+-brightgreen)
![Tests](https://img.shields.io/badge/tests-1012+-brightgreen)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 13 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, and cross-plugin collision detection. Zero external dependencies.
@ -275,7 +275,7 @@ Your team configuration changes over time. Track it:
|---------|-------------|
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
| `/config-audit posture` | Quick health scorecard: A-F grades across 10 quality areas (incl. Token Efficiency, Plugin Hygiene) |
| `/config-audit tokens` | prompt-cache-aware token hotspots — ranked by estimated waste; 6 patterns + optional `--accurate-tokens` API calibration |
| `/config-audit tokens` | prompt-cache-aware token hotspots — ranked by estimated waste, each tagged with its load pattern (always / on-demand / external); 6 patterns + optional `--accurate-tokens` API calibration |
| `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens — each tagged with its **load pattern** (always-loaded / on-demand / external) plus an **always-loaded subtotal** ("≈X tokens enter context every turn before you type"). Component-level: no coarse plugin roll-up (it would double-count) |
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
@ -588,7 +588,7 @@ Reference documents that inform the feature-gap agent and context-aware recommen
node --test 'tests/**/*.test.mjs'
```
1008 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Requires Node.js 18+ (`node:test`).
1012 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Requires Node.js 18+ (`node:test`).
---

View file

@ -2,32 +2,32 @@
_Current state-of-play for DENNE pluginen. Auto-injisert ved sesjonsstart. Overskrives (ikke append) ved sesjonsslutt. Historikk → git (STATE.md er tracked). Stier relative til plugin-mappa._
## Aktiv kontekst (v5.6 B1 = manifest load-pattern accounting LANDET på main — B2/TOK + C neste)
v5.5.0 released. v5.6 Foundation landet (`62d910e`). Nå: **v5.6 B delt i to****B1 (manifest) implementert + committet på main** (TDD). TOK-kolonnen ble bevisst utsatt (B2) fordi TOK er en byte-equal SC-6-CLI (krever snapshot-håndtering); manifest er env-aware → mode-equivalence → byte-stabil UTEN regen. Suite **1008** (996→1008), self-audit **A/A** (config A, plugin A), count **13** (ingen ny scanner). v5.6 IKKE release-cuttet (krever B2 + C + eget GO).
## Aktiv kontekst (v5.6 B KOMPLETT på main — C neste)
v5.5.0 released. v5.6 Foundation (`62d910e`) + **B1 manifest (`bb647ce`)** + **B2 token-hotspots** landet på main (TDD). **Hele B (load-pattern accounting) er nå ferdig.** Suite **1012** (996→1008→1012), self-audit **A/A**, count **13** (B la ingen scanner — C gir 14). v5.6 IKKE release-cuttet (krever C + eget GO).
## Gjort denne økten (2026-06-20, lør — pushet hvis i vindu)
- **`buildManifest` (`scanners/manifest.mjs`):** komponent-nivå — **plugin roll-up FJERNET** (dobbelttalte komponenter; plugin-bidrag enumereres allerede én gang som skills/rules/agents/output-styles/hooks/mcp). Source-kinds nå claude-md/skill/rule/agent/output-style/mcp-server/hook. Hver record får `loadPattern`/`survivesCompaction`/`derivationConfidence`. Rules/agents/output-styles **propagerer** Foundation-verdiene; CLAUDE.md scope→kind via `CLAUDE_MD_SCOPE_KIND` (alle cascade-filer walker opp → always); skills tagget **on-demand** (`skill-body` — målt token = body, ikke always-listing). Ny `summary` (always/onDemand/external/unknown {tokens,count}) i CLI-output. Ny `summarizeByLoadPattern` (eksportert).
- **Headline:** always-loaded subtotal. På plugin-rota: 24,3k always / 107 kilder (101 agenter dominerer), vs 125k on-demand (skill-bodies), 855 external (hooks). `total` endret (de-dup) — tilsiktet korrekthetsfiks.
- **Tester:** +12 (deterministisk enhetstest på `buildManifest` med syntetisk activeConfig: loadPattern per kind, summary-reconcil, plugin/disabled-mcp ekskludert fra total; + CLI loadPattern/summary-presence). Endret brutt `kind==='plugin'`-test → komponent-nivå.
- **Docs (docs-gate):** README (badge 996→1008, prosa + 3 manifest-beskrivelser), CLAUDE.md (test 1008 + ny «manifest — load-pattern accounting (v5.6 B)»-seksjon + tabellrad), scanner-internals manifest-rad.
**B1 (manifest, committet `bb647ce`):** komponent-nivå, plugin roll-up fjernet (dobbelttalte), loadPattern-triple per kilde + `summary` (always/onDemand/external) + always-loaded subtotal-headline. manifest = env-aware → mode-equivalence → byte-stabil uten regen.
**B2 (token-hotspots, denne committen):**
- **`token-hotspots.mjs`:** hver ranked hotspot får loadPattern-triple via `hotspotLoadPattern(h, ruleMap)``HOTSPOT_TYPE_TO_KIND`-mapping; rules slår opp `activeConfig.rules` for presis `scoped`; claude-md mapper via scope. To nye `deriveLoadPattern`-kinds: **`command`** (on-demand) + **`harness-config`** (external — settings/keybindings/.mcp.json/hooks.json/plugin.json = harness-config, IKKE model-context). Ærlig split: `.mcp.json`-FILA = external, MCP-serverens tool-schemas = separat `always`-hotspot.
- **Snapshot-blast (6 brudd / 5 filer):** TOK ER byte-equal SC-6/7 OG hotspots embedder i scan-orchestrator + posture. Løst ved å **bevare frozen v5.0.0**: delt `tests/helpers/strip-hotspot-load-pattern.mjs` stripper de additive feltene før byte-equal (json/raw-backcompat + de 3 Step 5/6/7 humanizer-testene). **SC-5 default-output regenerert** (`UPDATE_SNAPSHOT=1`, scan-orch + token-hotspots) — diff reviewet additiv-only.
- **Tester:** +4 (deriveLoadPattern command+harness-config; TOK hotspots loadPattern-triple + CLAUDE.md-always/skill-on-demand). Docs: README badge/prosa 1012, CLAUDE.md (1012 + ny B2-seksjon), tokens.md Load-kolonne, scanner-internals.
## Åpne tråder / neste steg (prioritert, alle KREVER GO)
1. **v5.6 B2 (TOK load-pattern-kolonne):** legg loadPattern på token-hotspots source-records. TOK ER byte-equal SC-6 → **bevar v5.0.0-baseline via normalizer-strip** (additive felt ignoreres, frozen-kontrakt intakt) + **regen SC-5 default-output/token-hotspots.json** (UPDATE_SNAPSHOT=1). Foundation + manifest-mønsteret ligger klart.
2. **v5.6 C (output-style scanner):** ny `CA-OST` (count 13→**14**): keep-coding-instructions/force-for-plugin/dead outputStyle. Badge + lore + humanizer/scoring/discovery-wiring.
3. **v5.6 release-cut:** etter B2+C (eget GO, som v5.5.0).
4. **v5.7:** D (mechanism-fit, heuristisk, precision-gated).
5. **llm-security KRITISK** (annet repo) + 3 aktive plugins + STATE-sveip — uendret.
1. **v5.6 C (output-style scanner):** ny `CA-OST` (count 13→**14**): `CA-OST-001` custom style mangler `keep-coding-instructions:true` (medium, V10), `CA-OST-002` `force-for-plugin` override (low, V11), `CA-OST-003` dead `outputStyle` (dead config). Foundation `enumerateOutputStyles` ligger klar. Badge 13→14 + lore-sweep (README/CLAUDE/scanner-internals + «count stays 13»-noter) + humanizer-data + scoring + discovery-wiring. Ny discovery-type for `.claude/output-styles/`? (Foundation gjorde det DIREKTE, ikke via file-discovery — vurder for scanner.)
2. **v5.6 release-cut:** etter C (eget GO, som v5.5.0): tag + catalog-bump (separat repo).
3. **v5.7:** D (mechanism-fit, heuristisk, precision-gated).
4. **llm-security KRITISK** (annet repo) + 3 aktive plugins + STATE-sveip — uendret.
## Gotchas (UFRAVIKELIG)
- **manifest er env-aware → SC-6/SC-7 = mode-equivalence (--json==--raw), IKKE byte-equal.** Derfor byte-stabil ved add-field-in-place (verifisert). **TOK er motsatt** (byte-equal) — B2 må håndtere snapshot. manifest IKKE i SC-5.
- **manifest har INGEN findings** → ingen humanizer/fix-engine-tittelkobling (gjelder kun finding-scannere).
- **Skill on-demand, IKKE always:** målt token = body (invoke). Always-listing (navn+desc) er liten, spores av skill-listing-budget/posture. Ikke tagg skill-body always — det blåser opp headline.
- Versjon KUN i plugin.json + README version-badge. **tests-badge = EKSAKT ` tests N` fra `node --test`** (self-audit `countTestCases`); verifiser `readmeCheck.passed:true`/`mismatches:[]` via `--json`. 1008 nå.
- **docs-gate (non-marketplace gren) krever ≥3 ikke-ws-linjer i README OG CLAUDE.md for `feat:`.** Oppfylt ærlig denne økten. Alt.: `[skip-docs]`.
- Scanner-count 13. C gir 14. Path-guard blokkerer Write på `.claude-plugin/`+settings/hooks → hermetisk `mkdtemp` i tester. Push-vindu: hverdag 2023, helg fritt.
- **To motsatte byte-stabilitets-regimer:** manifest = env-aware → mode-equivalence (additive felt gratis). TOK = byte-equal v5.0.0 OG embedder i scan-orch+posture → **5 frozen-v5.0.0-komparatorer** (json/raw-backcompat + cli-humanizer + posture-humanizer + scan-orchestrator-humanizer). **Grep ALLE 5 før du antar blast-radius** ved nytt scanner-output-felt. Bevar frozen via strip-helper; regen KUN SC-5 default-output.
- **manifest har INGEN findings** → ingen humanizer/fix-engine-tittelkobling (kun finding-scannere).
- **Skill on-demand** (body), ikke always. Always-listing (navn+desc) liten, spores av skill-listing-budget/posture.
- **harness-config = external** (settings/manifests koster ingen per-tur context-tokens). `.mcp.json`-fil ≠ MCP-tool-schemas (sistnevnte always).
- tests-badge = EKSAKT ` tests N` (self-audit `countTestCases`); verifiser `readmeCheck.passed:true`/`mismatches:[]`. 1012 nå. Versjon KUN i plugin.json + README version-badge.
- **docs-gate (non-marketplace) krever ≥3 ikke-ws-linjer i README OG CLAUDE.md for `feat:`.** Path-guard blokkerer Write på `.claude-plugin/`+settings/hooks → hermetisk `mkdtemp`. Push-vindu: hverdag 2023, helg fritt.
## Scope-gjerde
v5.6 B1 (manifest) godkjent (GO) og landet. B2 (TOK) + C + release-cut + v5.7 + andre repos = egne GO. Ikke start B2 uten GO.
v5.6 B (B1+B2) godkjent (GO) og landet. C + release-cut + v5.7 + andre repos = egne GO. Ikke start C uten GO.
## Kontinuitet
Tre lag: STATE.md (tracked) + auto-memory + CLAUDE.md. Oppdater DENNE som siste handling.

View file

@ -62,10 +62,12 @@ Do NOT render tables in JSON mode.
Use the Read tool on `$TMPFILE`. Extract:
- `total_estimated_tokens` — top-line number
- `hotspots[]` — top 10 ranked sources
- `hotspots[]` — top 10 ranked sources; each carries a **load pattern** (`loadPattern` ∈ always / on-demand / external, plus `survivesCompaction` / `derivationConfidence`)
- `findings[]` — prompt-cache pattern findings (CA-TOK-001..003); each finding in default mode carries humanizer fields (`userImpactCategory`, `userActionLanguage`, `relevanceContext`) alongside the v5.0.0 fields
- `counts` — severity breakdown
A hotspot's **load pattern** matters as much as its size: an **always**-loaded source (CLAUDE.md, MCP tool schemas) is paid on *every* turn, an **on-demand** one (skill body, path-scoped rule) only when invoked/matched, and an **external** one (hooks, harness-config files like settings.json/.mcp.json) costs no per-turn context tokens at all. A big always-loaded hotspot is the most worth trimming.
Render as markdown. Group findings by `userImpactCategory` (e.g., "Wasted tokens" vs "Configuration mistake") rather than re-deriving severity prose; lead each line with `userActionLanguage` ("Fix this now", "Fix soon", "Optional cleanup", etc.) so the urgency phrasing stays consistent with the rest of the toolchain. The humanizer already replaced jargon-heavy `title`/`description`/`recommendation` strings with plain-language equivalents — render them verbatim.
```markdown
@ -73,9 +75,11 @@ Render as markdown. Group findings by `userImpactCategory` (e.g., "Wasted tokens
### Top hotspots (ranked by estimated tokens)
| Rank | Source | Tokens | Recommendations |
|------|--------|--------|-----------------|
| {rank} | `{source}` | ~{estimated_tokens} | {recommendations joined as `· ` bullets} |
| Rank | Source | Tokens | Load | Recommendations |
|------|--------|--------|------|-----------------|
| {rank} | `{source}` | ~{estimated_tokens} | {loadPattern} | {recommendations joined as `· ` bullets} |
_Load column: **always** (every turn) / **on-demand** (on invoke/match) / **external** (out-of-context). Append `°` when `derivationConfidence` is `inferred`._
### Findings, grouped by impact

View file

@ -53,7 +53,7 @@ Scanner CLI: `node scanners/scan-orchestrator.mjs <path> [--global] [--full-mach
| `fix-cli.mjs` | CLI: `node fix-cli.mjs <path> [--apply] [--json] [--global]` |
| `drift-cli.mjs` | CLI: `node drift-cli.mjs <path> [--save] [--baseline name] [--json]` |
| `whats-active.mjs` | CLI: `node whats-active.mjs <path> [--json] [--verbose] [--suggest-disables]` — read-only active-config inventory |
| `token-hotspots-cli.mjs` | CLI: `node token-hotspots-cli.mjs <path> [--json] [--global] [--output-file path] [--accurate-tokens] [--with-telemetry-recipe]` — prompt-cache token hotspots ranking with optional API calibration |
| `token-hotspots-cli.mjs` | CLI: `node token-hotspots-cli.mjs <path> [--json] [--global] [--output-file path] [--accurate-tokens] [--with-telemetry-recipe]` — prompt-cache token hotspots ranking (each hotspot tagged with its load pattern, v5.6 B2) with optional API calibration |
| `manifest.mjs` | CLI: `node manifest.mjs <path> [--json]` — ranked component-level token-source table, each source tagged with its load pattern + an always-loaded subtotal (v5 N2; load-pattern accounting v5.6 B) |
## Standalone Scanner

View file

@ -99,6 +99,14 @@ export function deriveLoadPattern(kind, opts = {}) {
// MCP tool schemas are part of the per-turn payload (no explicit
// compaction-survival row → 'inferred').
case 'mcp': return mk('always', 'yes', 'inferred');
// Slash-command body loads when the command is invoked (on-demand). No
// primary-doc row pins the always-loaded command listing cost → 'inferred'.
case 'command': return mk('on-demand', 'n/a', 'inferred');
// Harness-config files (settings.json, keybindings.json, .mcp.json, hooks.json,
// plugin.json, ~/.claude.json) are read by the CLI to configure the harness —
// they are NOT injected into the model context, so they cost no per-turn
// context tokens. 'external' = outside the context window (like hooks).
case 'harness-config': return mk('external', 'n/a', 'inferred');
default: return mk('unknown', 'n/a', 'inferred');
}
}

View file

@ -27,10 +27,64 @@ import { readTextFile } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { findImports, parseJson, parseFrontmatter } from './lib/yaml-parser.mjs';
import { estimateTokens, readActiveConfig } from './lib/active-config-reader.mjs';
import { estimateTokens, readActiveConfig, deriveLoadPattern } from './lib/active-config-reader.mjs';
const SCANNER = 'TOK';
// v5.6 B2 — map a discovery `type` to a deriveLoadPattern kind so each ranked
// hotspot can be annotated with its load pattern. `claude-md` and `rule` are
// handled specially (by scope / scoped); the synthetic 'mcp-server' → 'mcp'.
// Harness-config files (settings/manifests) configure the CLI, not the model
// context, so they cost no per-turn context tokens (deriveLoadPattern maps
// 'harness-config' → external).
const HOTSPOT_TYPE_TO_KIND = {
'skill-md': 'skill-body',
'agent-md': 'agent',
'command-md': 'command',
'mcp-server': 'mcp',
'settings-json': 'harness-config',
'mcp-json': 'harness-config',
'hooks-json': 'harness-config',
'plugin-json': 'harness-config',
'claude-json': 'harness-config',
'keybindings-json': 'harness-config',
};
const CLAUDE_MD_SCOPE_KIND = {
project: 'claude-md-root',
local: 'claude-md-root',
user: 'claude-md-user',
managed: 'claude-md-managed',
import: 'claude-md-import',
};
/**
* Derive the load-pattern triple for a ranked hotspot. Rules reuse the
* active-config enumeration (which parsed `paths:` to set `scoped`) for precise
* scoped/unscoped handling; everything else maps by type.
*
* @param {{type:string, scope?:string, absPath?:string}} h
* @param {Map<string, {loadPattern:string, survivesCompaction:string, derivationConfidence:string}>} ruleMap
*/
function hotspotLoadPattern(h, ruleMap) {
if (h.type === 'claude-md') {
return deriveLoadPattern(CLAUDE_MD_SCOPE_KIND[h.scope] || 'claude-md-root');
}
if (h.type === 'rule') {
const r = ruleMap.get(h.absPath);
if (r) {
return {
loadPattern: r.loadPattern,
survivesCompaction: r.survivesCompaction,
derivationConfidence: r.derivationConfidence,
};
}
return deriveLoadPattern('rule', { scoped: false });
}
// Unmapped types fall through to deriveLoadPattern's safe 'unknown' default.
return deriveLoadPattern(HOTSPOT_TYPE_TO_KIND[h.type] || h.type);
}
const VOLATILE_TOP_LINES = 30;
const VOLATILE_PATTERNS = [
/\{timestamp\}/i,
@ -241,14 +295,25 @@ async function buildHotspots(discovery, targetPath, activeConfig) {
}
ranked.sort((a, b) => b.estimated_tokens - a.estimated_tokens);
// v5.6 B2 — precise rule load-pattern lookup: the active-config enumeration
// already parsed `paths:` and derived the pattern, keyed by absolute path.
const ruleMap = new Map();
if (activeConfig && Array.isArray(activeConfig.rules)) {
for (const r of activeConfig.rules) ruleMap.set(r.path, r);
}
const top = ranked.slice(0, HOTSPOTS_MAX);
const out = [];
for (let i = 0; i < top.length; i++) {
const h = top[i];
const lp = hotspotLoadPattern(h, ruleMap);
const entry = {
source: h.relPath || h.absPath,
estimated_tokens: h.estimated_tokens,
rank: i + 1,
loadPattern: lp.loadPattern,
survivesCompaction: lp.survivesCompaction,
derivationConfidence: lp.derivationConfidence,
recommendations: hotspotRecommendations(h),
};
// Expose the on-disk path for file-backed hotspots so the

View file

@ -0,0 +1,37 @@
/**
* v5.6 B2 added a load-pattern triple (loadPattern / survivesCompaction /
* derivationConfidence) to each TOK hotspot. The frozen v5.0.0 byte-equal
* baselines strip it before comparison so they still prove the ORIGINAL schema
* is byte-identical; the new fields are purely additive.
*
* TOK hotspots ride along inside scan-orchestrator and posture payloads too
* (`scanners[].hotspots`, `scannerEnvelope.scanners[].hotspots`), so this
* strips every hotspots array reachable in a CLI payload / envelope. Mutates
* in place and returns the payload for chaining inside a normalizer.
*
* @template T
* @param {T} payload
* @returns {T}
*/
export function stripHotspotLoadPattern(payload) {
const drop = (arr) => {
if (!Array.isArray(arr)) return;
for (const h of arr) {
delete h.loadPattern;
delete h.survivesCompaction;
delete h.derivationConfidence;
}
};
if (!payload || typeof payload !== 'object') return payload;
drop(payload.hotspots);
if (Array.isArray(payload.scanners)) {
for (const s of payload.scanners) drop(s.hotspots);
}
if (payload.scannerEnvelope) {
drop(payload.scannerEnvelope.hotspots);
if (Array.isArray(payload.scannerEnvelope.scanners)) {
for (const s of payload.scannerEnvelope.scanners) drop(s.hotspots);
}
}
return payload;
}

View file

@ -29,6 +29,7 @@ import { promisify } from 'node:util';
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';
const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -102,7 +103,7 @@ function normalizeScanOrchestrator(env) {
}
}
stripAncestorDerived(out);
return out;
return stripHotspotLoadPattern(out);
}
function normalizePosture(p) {
@ -119,13 +120,13 @@ function normalizePosture(p) {
}
stripAncestorDerived(out.scannerEnvelope);
}
return out;
return stripHotspotLoadPattern(out);
}
function normalizeTokenHotspots(p) {
const out = JSON.parse(JSON.stringify(p));
out.duration_ms = 0;
return out;
return stripHotspotLoadPattern(out);
}
function normalizeDrift(p) {

View file

@ -731,6 +731,9 @@ describe('deriveLoadPattern (v5.6)', () => {
['output-style', {}, 'always', 'yes', 'confirmed'],
['hook', {}, 'external', 'n/a', 'confirmed'],
['mcp', {}, 'always', 'yes', 'inferred'],
// v5.6 B2 — kinds surfaced by the token-hotspots discovery surface.
['command', {}, 'on-demand', 'n/a', 'inferred'], // body loads on /invoke
['harness-config', {}, 'external', 'n/a', 'inferred'], // settings/manifests: not model context
];
for (const [kind, opts, loadPattern, survivesCompaction, derivationConfidence] of cases) {
const label = `${kind}${opts.scoped !== undefined ? ` (scoped=${opts.scoped})` : ''}`;

View file

@ -26,6 +26,7 @@ import { promisify } from 'node:util';
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';
const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -97,7 +98,7 @@ function normalizeScanOrchestrator(env) {
}
}
stripAncestorDerived(out);
return out;
return stripHotspotLoadPattern(out);
}
function normalizePosture(p) {
@ -114,13 +115,13 @@ function normalizePosture(p) {
}
stripAncestorDerived(out.scannerEnvelope);
}
return out;
return stripHotspotLoadPattern(out);
}
function normalizeTokenHotspots(p) {
const out = JSON.parse(JSON.stringify(p));
out.duration_ms = 0;
return out;
return stripHotspotLoadPattern(out);
}
function normalizeDrift(p) {

View file

@ -6,6 +6,7 @@ import { execFile } from 'node:child_process';
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';
const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -42,7 +43,7 @@ async function runCli(cliPath, args, env = {}) {
function normalizeTokenHotspotsPayload(p) {
const out = JSON.parse(JSON.stringify(p));
out.duration_ms = 0;
return out;
return stripHotspotLoadPattern(out);
}
function normalizeManifestOutput(o) {

View file

@ -6,6 +6,7 @@ import { execFile } from 'node:child_process';
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';
const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -38,7 +39,7 @@ function normalizePosture(p) {
}
}
}
return out;
return stripHotspotLoadPattern(out);
}
/** Strip time-varying durations (Xms) so progress lines compare verbatim across runs. */

View file

@ -6,6 +6,7 @@ import { execFile } from 'node:child_process';
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';
const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -36,7 +37,7 @@ function normalizeEnvelope(env) {
}
}
}
return out;
return stripHotspotLoadPattern(out);
}
async function runOrchestrator(flags) {

View file

@ -182,6 +182,32 @@ describe('TOK scanner — hotspots contract', () => {
}
});
it('every hotspot carries a load-pattern triple (v5.6 B2)', () => {
const LOAD_PATTERNS = new Set(['always', 'on-demand', 'external', 'unknown']);
const SURVIVES = new Set(['yes', 'no', 'n/a']);
for (const h of result.hotspots) {
assert.ok(LOAD_PATTERNS.has(h.loadPattern),
`hotspot ${h.source} has invalid loadPattern: ${h.loadPattern}`);
assert.ok(SURVIVES.has(h.survivesCompaction),
`hotspot ${h.source} has invalid survivesCompaction: ${h.survivesCompaction}`);
assert.ok(typeof h.derivationConfidence === 'string' && h.derivationConfidence.length > 0,
`hotspot ${h.source} missing derivationConfidence`);
}
});
it('tags a CLAUDE.md hotspot always-loaded and a skill hotspot on-demand (v5.6 B2)', () => {
const claudeMd = result.hotspots.find(h => /CLAUDE\.md/.test(h.source));
if (claudeMd) {
assert.equal(claudeMd.loadPattern, 'always',
`expected CLAUDE.md hotspot always-loaded; got ${claudeMd.loadPattern}`);
}
const skill = result.hotspots.find(h => /SKILL\.md/.test(h.source) || /skills?\//.test(h.source));
if (skill) {
assert.equal(skill.loadPattern, 'on-demand',
`expected skill hotspot on-demand; got ${skill.loadPattern}`);
}
});
it('every hotspot.source is unique (v5 F4: no padding)', () => {
const sources = result.hotspots.map(h => h.source);
const unique = new Set(sources);

View file

@ -445,6 +445,9 @@
"source": "mcp:memory (.mcp.json)",
"estimated_tokens": 500,
"rank": 1,
"loadPattern": "always",
"survivesCompaction": "yes",
"derivationConfidence": "inferred",
"recommendations": [
"Review whether this source needs to load on every turn."
]
@ -453,6 +456,9 @@
"source": "CLAUDE.md",
"estimated_tokens": 116,
"rank": 2,
"loadPattern": "always",
"survivesCompaction": "yes",
"derivationConfidence": "confirmed",
"recommendations": [
"Move volatile top-of-file content to the bottom or extract to an @import-ed file.",
"Split overlong CLAUDE.md into focused @imports (≤200 lines each)."
@ -463,6 +469,9 @@
"source": "hooks/hooks.json",
"estimated_tokens": 81,
"rank": 3,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."
@ -473,6 +482,9 @@
"source": ".claude/settings.json",
"estimated_tokens": 59,
"rank": 4,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."
@ -483,6 +495,9 @@
"source": ".mcp.json",
"estimated_tokens": 45,
"rank": 5,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."

View file

@ -11,6 +11,9 @@
"source": "mcp:memory (.mcp.json)",
"estimated_tokens": 500,
"rank": 1,
"loadPattern": "always",
"survivesCompaction": "yes",
"derivationConfidence": "inferred",
"recommendations": [
"Review whether this source needs to load on every turn."
]
@ -19,6 +22,9 @@
"source": "CLAUDE.md",
"estimated_tokens": 116,
"rank": 2,
"loadPattern": "always",
"survivesCompaction": "yes",
"derivationConfidence": "confirmed",
"recommendations": [
"Move volatile top-of-file content to the bottom or extract to an @import-ed file.",
"Split overlong CLAUDE.md into focused @imports (≤200 lines each)."
@ -29,6 +35,9 @@
"source": "hooks/hooks.json",
"estimated_tokens": 81,
"rank": 3,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."
@ -39,6 +48,9 @@
"source": ".claude/settings.json",
"estimated_tokens": 59,
"rank": 4,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."
@ -49,6 +61,9 @@
"source": ".mcp.json",
"estimated_tokens": 45,
"rank": 5,
"loadPattern": "external",
"survivesCompaction": "n/a",
"derivationConfidence": "inferred",
"recommendations": [
"Deduplicate overlapping entries — each duplicate inflates the per-turn schema payload.",
"Move rarely-used permissions to a project-local override."