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:
parent
bb647ce35f
commit
778b517e6f
17 changed files with 236 additions and 40 deletions
37
tests/helpers/strip-hotspot-load-pattern.mjs
Normal file
37
tests/helpers/strip-hotspot-load-pattern.mjs
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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})` : ''}`;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue