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

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