feat(mft): v5.6 B1 — load-pattern accounting in manifest
Consume the v5.6 Foundation enumeration in buildManifest:
- Component-level sources: drop the coarse `kind:'plugin'` roll-up (it
double-counted skills/rules/agents already enumerated once). Kinds are now
claude-md/skill/rule/agent/output-style/mcp-server/hook.
- Every source carries loadPattern/survivesCompaction/derivationConfidence.
Rules/agents/output-styles propagate the foundation-derived values; CLAUDE.md
maps scope→kind (all cascade files always-loaded); skills are tagged on-demand
(skill-body) so the body cost does not inflate the always-loaded subtotal.
- New `summary` (always/onDemand/external/unknown {tokens,count}); the
always-loaded subtotal — "tokens that enter context every turn" — is the headline.
manifest is an environment-aware CLI → SC-6/SC-7 verify it by mode-equivalence,
not byte-equal, and it is not in SC-5. Adding fields in place keeps all snapshots
green with no regen (verified). `total` changes (de-duped) — intended correctness fix.
TOK's load-pattern column (byte-equal SC-6) is deferred to the next chunk (B2).
Tests 996→1008 (deterministic buildManifest unit test + CLI presence checks).
Self-audit A/A, scanner count unchanged at 13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
62d910ed6d
commit
bb647ce35f
7 changed files with 324 additions and 66 deletions
|
|
@ -9,13 +9,29 @@
|
|||
* {
|
||||
* meta: { repoPath, generatedAt, durationMs },
|
||||
* sources: [
|
||||
* { kind: 'claude-md'|'plugin'|'skill'|'mcp-server'|'hook',
|
||||
* name: string, source: string, estimated_tokens: number },
|
||||
* { kind: 'claude-md'|'skill'|'rule'|'agent'|'output-style'|'mcp-server'|'hook',
|
||||
* name: string, source: string, estimated_tokens: number,
|
||||
* loadPattern: 'always'|'on-demand'|'external'|'unknown',
|
||||
* survivesCompaction: 'yes'|'no'|'n/a',
|
||||
* derivationConfidence: 'confirmed'|'inferred' },
|
||||
* ...
|
||||
* ],
|
||||
* summary: {
|
||||
* always: { tokens, count }, // enter context every turn before you type
|
||||
* onDemand: { tokens, count }, // loaded on invoke / on file read
|
||||
* external: { tokens, count }, // run outside the context window (hooks)
|
||||
* unknown: { tokens, count },
|
||||
* },
|
||||
* total: <sum of sources.estimated_tokens>
|
||||
* }
|
||||
*
|
||||
* v5.6 B — load-pattern accounting. Sources are component-level: the coarse
|
||||
* "plugin" roll-up was dropped because a plugin's contributions (skills, rules,
|
||||
* agents, output styles, hooks, MCP) are each enumerated once on their own —
|
||||
* keeping the roll-up double-counted them and corrupted the always-loaded
|
||||
* subtotal. Every record now carries the load pattern derived from the
|
||||
* published Claude Code loading model (deriveLoadPattern).
|
||||
*
|
||||
* Usage:
|
||||
* node manifest.mjs [path] [--json] [--output-file <path>]
|
||||
*
|
||||
|
|
@ -25,64 +41,131 @@
|
|||
|
||||
import { resolve } from 'node:path';
|
||||
import { writeFile, stat } from 'node:fs/promises';
|
||||
import { readActiveConfig } from './lib/active-config-reader.mjs';
|
||||
import { readActiveConfig, deriveLoadPattern } from './lib/active-config-reader.mjs';
|
||||
|
||||
// CLAUDE.md cascade files are all discovered by walking UP from the repo, so
|
||||
// each one is always-loaded; the scope only changes the derivation confidence.
|
||||
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',
|
||||
};
|
||||
|
||||
/** Spread the three load-pattern fields onto a source record. */
|
||||
function withLoadPattern(record, lp) {
|
||||
return {
|
||||
...record,
|
||||
loadPattern: lp.loadPattern,
|
||||
survivesCompaction: lp.survivesCompaction,
|
||||
derivationConfidence: lp.derivationConfidence,
|
||||
};
|
||||
}
|
||||
|
||||
const sourceLabel = (item, fallback) =>
|
||||
item.pluginName ? `plugin:${item.pluginName}` : item.source || fallback;
|
||||
|
||||
/**
|
||||
* Flatten an activeConfig snapshot into a single ranked array of sources.
|
||||
* Flatten an activeConfig snapshot into a single ranked array of sources, each
|
||||
* tagged with its load pattern, plus a load-pattern summary.
|
||||
*/
|
||||
export function buildManifest(activeConfig) {
|
||||
const sources = [];
|
||||
|
||||
for (const f of activeConfig.claudeMd?.files || []) {
|
||||
const tokens = estimateClaudeMdEntryTokens(f, activeConfig);
|
||||
sources.push({
|
||||
const kind = CLAUDE_MD_SCOPE_KIND[f.scope] || 'claude-md-root';
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'claude-md',
|
||||
name: f.path,
|
||||
source: f.scope,
|
||||
estimated_tokens: tokens,
|
||||
});
|
||||
}
|
||||
|
||||
for (const p of activeConfig.plugins || []) {
|
||||
sources.push({
|
||||
kind: 'plugin',
|
||||
name: p.name,
|
||||
source: p.path,
|
||||
estimated_tokens: p.estimatedTokens || 0,
|
||||
});
|
||||
}, deriveLoadPattern(kind)));
|
||||
}
|
||||
|
||||
// Skills: the measured tokens are the skill BODY (full file), paid on invoke.
|
||||
// The always-loaded part (name+description listing) is small and tracked
|
||||
// separately (skill-listing-budget / posture), so the body is tagged
|
||||
// on-demand here rather than inflating the always-loaded subtotal.
|
||||
for (const s of activeConfig.skills || []) {
|
||||
sources.push({
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'skill',
|
||||
name: s.name,
|
||||
source: s.pluginName ? `plugin:${s.pluginName}` : s.source || 'user',
|
||||
source: sourceLabel(s, 'user'),
|
||||
estimated_tokens: s.estimatedTokens || 0,
|
||||
});
|
||||
}, deriveLoadPattern('skill-body')));
|
||||
}
|
||||
|
||||
// Rules / agents / output styles — the foundation enumeration already derived
|
||||
// the load pattern (rules vary by `scoped`), so propagate it verbatim.
|
||||
for (const r of activeConfig.rules || []) {
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'rule',
|
||||
name: r.name,
|
||||
source: sourceLabel(r, 'project'),
|
||||
estimated_tokens: r.estimatedTokens || 0,
|
||||
}, r));
|
||||
}
|
||||
|
||||
for (const a of activeConfig.agents || []) {
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'agent',
|
||||
name: a.name,
|
||||
source: sourceLabel(a, 'project'),
|
||||
estimated_tokens: a.estimatedTokens || 0,
|
||||
}, a));
|
||||
}
|
||||
|
||||
for (const o of activeConfig.outputStyles || []) {
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'output-style',
|
||||
name: o.name,
|
||||
source: sourceLabel(o, 'project'),
|
||||
estimated_tokens: o.estimatedTokens || 0,
|
||||
}, o));
|
||||
}
|
||||
|
||||
for (const m of activeConfig.mcpServers || []) {
|
||||
if (m && m.enabled === false) continue;
|
||||
sources.push({
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'mcp-server',
|
||||
name: m.name,
|
||||
source: m.source || 'unknown',
|
||||
estimated_tokens: m.estimatedTokens || 0,
|
||||
});
|
||||
}, deriveLoadPattern('mcp')));
|
||||
}
|
||||
|
||||
for (const h of activeConfig.hooks || []) {
|
||||
sources.push({
|
||||
sources.push(withLoadPattern({
|
||||
kind: 'hook',
|
||||
name: `${h.event}${h.matcher ? `:${h.matcher}` : ''}`,
|
||||
source: h.source || h.sourcePath || 'unknown',
|
||||
estimated_tokens: h.estimatedTokens || 0,
|
||||
});
|
||||
}, deriveLoadPattern('hook')));
|
||||
}
|
||||
|
||||
sources.sort((a, b) => b.estimated_tokens - a.estimated_tokens);
|
||||
const total = sources.reduce((s, x) => s + (x.estimated_tokens || 0), 0);
|
||||
return { sources, total };
|
||||
const summary = summarizeByLoadPattern(sources);
|
||||
return { sources, total, summary };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucket sources by load pattern into {tokens, count} subtotals. The `always`
|
||||
* bucket is the headline: tokens that enter context every turn before the user
|
||||
* types anything.
|
||||
*/
|
||||
export function summarizeByLoadPattern(sources) {
|
||||
const mk = () => ({ tokens: 0, count: 0 });
|
||||
const summary = { always: mk(), onDemand: mk(), external: mk(), unknown: mk() };
|
||||
const BUCKET = { always: 'always', 'on-demand': 'onDemand', external: 'external' };
|
||||
for (const s of sources) {
|
||||
const key = BUCKET[s.loadPattern] || 'unknown';
|
||||
summary[key].tokens += s.estimated_tokens || 0;
|
||||
summary[key].count += 1;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -138,6 +221,7 @@ async function main() {
|
|||
durationMs: Date.now() - start,
|
||||
},
|
||||
sources: manifest.sources,
|
||||
summary: manifest.summary,
|
||||
total: manifest.total,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue