feat(manifest): ownership split for machine-wide token roll-up (v5.9 B2b-1) [skip-docs]

Pure classifier splitManifestByOwnership(sources) → {shared, delta}, the
FS-free core that B2b's live cross-repo sweep will feed into the campaign
ledger's setSharedGlobal/setRepoTokens (B2a).

classifyOwnership maps each source string to its layer:
  shared : user | managed | plugin:* | ~/.claude.json:projects (global MCP)
  delta  : project | local | .mcp.json | @import | unrecognized

Anything not positively global falls to delta, so a source is never silently
folded into the once-counted shared layer (a wrong fold HIDES machine-wide
cost; a wrong delta is at worst visibly attributed to a repo). Both layers
carry the canonical summarizeByLoadPattern shape so the ledger setters consume
them verbatim.

TDD: 6 unit tests first (RED → GREEN). buildManifest/CLI output unchanged →
manifest snapshot byte-identical. Suite 1195 green (1189 + 6).

[skip-docs]: internal pure export only, no user-facing surface yet. User-facing
docs (commands/campaign.md + CLAUDE.md manifest/campaign rows) land in B2b-3
when refresh-tokens + the rendered token-bill ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 17:10:03 +02:00
commit 872b8ac281
2 changed files with 126 additions and 1 deletions

View file

@ -168,6 +168,56 @@ export function summarizeByLoadPattern(sources) {
return summary;
}
/**
* Source strings (the `source` field buildManifest stamps) that belong to the
* SHARED GLOBAL layer config paid once per machine and identical in every
* repo: the global ~/.claude CLAUDE.md, managed enterprise policy, and the
* global MCP slice of ~/.claude.json. Installed plugins are also shared but are
* matched by the `plugin:` prefix below, not by this set.
*/
const SHARED_GLOBAL_SOURCES = Object.freeze(new Set(['user', 'managed', '~/.claude.json:projects']));
/**
* Classify one manifest source as part of the once-counted shared global layer
* or a per-repo delta (v5.9 B2b). Anything not positively identified as global
* (project / local / .mcp.json / @import / unrecognized) falls to `delta`, so a
* source is never silently folded into the shared layer a wrong fold would
* HIDE machine-wide cost, whereas a wrong delta is at worst attributed visibly
* to a repo.
* @param {string} source
* @returns {'shared'|'delta'}
*/
export function classifyOwnership(source) {
if (typeof source === 'string') {
if (source.startsWith('plugin:')) return 'shared'; // installed plugins are machine-global
if (SHARED_GLOBAL_SOURCES.has(source)) return 'shared';
}
return 'delta';
}
/**
* Partition manifest sources by ownership for the machine-wide token roll-up,
* returning two load-pattern summaries in the exact shape `summarizeByLoadPattern`
* emits ({always,onDemand,external,unknown:{tokens,count}}), so the campaign
* ledger setters (`setSharedGlobal` / `setRepoTokens`) consume them verbatim.
*
* - `shared`: the global layer, identical across repos set ONCE on the ledger
* root so the roll-up counts it exactly once (the structural double-count guard).
* - `delta`: this repo's own project/local contribution beyond the shared layer.
*
* The split is total: every source lands in exactly one layer.
* @param {Array<{source:string, loadPattern:string, estimated_tokens:number}>} sources
* @returns {{shared:object, delta:object}}
*/
export function splitManifestByOwnership(sources) {
const shared = [];
const delta = [];
for (const s of sources || []) {
(classifyOwnership(s.source) === 'shared' ? shared : delta).push(s);
}
return { shared: summarizeByLoadPattern(shared), delta: summarizeByLoadPattern(delta) };
}
/**
* Distribute the cascade-level estimated tokens across the individual files
* proportional to their byte size. claudeMd.estimatedTokens is computed for