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:
parent
cefa751990
commit
872b8ac281
2 changed files with 126 additions and 1 deletions
|
|
@ -5,7 +5,7 @@ import { resolve, join, dirname } from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { buildManifest } from '../../scanners/manifest.mjs';
|
||||
import { buildManifest, splitManifestByOwnership, classifyOwnership } from '../../scanners/manifest.mjs';
|
||||
import { deriveLoadPattern } from '../../scanners/lib/active-config-reader.mjs';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
|
@ -193,6 +193,81 @@ describe('buildManifest — load-pattern accounting (unit)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('splitManifestByOwnership — shared-global vs per-repo-delta (unit, v5.9 B2b)', () => {
|
||||
// Minimal source records — only the fields the splitter reads (source, loadPattern,
|
||||
// estimated_tokens), matching the shape buildManifest stamps onto every source.
|
||||
const src = (source, loadPattern, tokens) => ({ source, loadPattern, estimated_tokens: tokens });
|
||||
|
||||
it('classifyOwnership routes each source string to the right layer', () => {
|
||||
// Shared: paid once per machine, identical in every repo.
|
||||
assert.equal(classifyOwnership('user'), 'shared');
|
||||
assert.equal(classifyOwnership('managed'), 'shared');
|
||||
assert.equal(classifyOwnership('plugin:foo'), 'shared');
|
||||
assert.equal(classifyOwnership('~/.claude.json:projects'), 'shared');
|
||||
// Per-repo delta: the repo's own project/local contribution.
|
||||
assert.equal(classifyOwnership('project'), 'delta');
|
||||
assert.equal(classifyOwnership('local'), 'delta');
|
||||
assert.equal(classifyOwnership('.mcp.json'), 'delta');
|
||||
// import + anything unrecognized → delta (never silently folded into the shared layer).
|
||||
assert.equal(classifyOwnership('import'), 'delta');
|
||||
assert.equal(classifyOwnership('mystery'), 'delta');
|
||||
assert.equal(classifyOwnership(undefined), 'delta');
|
||||
});
|
||||
|
||||
const sources = [
|
||||
src('user', 'always', 100), // global CLAUDE.md → shared
|
||||
src('managed', 'always', 50), // managed policy → shared
|
||||
src('plugin:foo', 'always', 30), // plugin agent/rule → shared
|
||||
src('~/.claude.json:projects', 'always', 20), // global MCP → shared
|
||||
src('project', 'always', 10), // project CLAUDE.md/rule → delta
|
||||
src('local', 'always', 5), // local → delta
|
||||
src('.mcp.json', 'always', 7), // project MCP → delta
|
||||
src('import', 'always', 3), // conservative → delta
|
||||
];
|
||||
const { shared, delta } = splitManifestByOwnership(sources);
|
||||
|
||||
it('folds global/user/managed/plugin/global-MCP into the shared layer', () => {
|
||||
assert.equal(shared.always.tokens, 100 + 50 + 30 + 20); // 200
|
||||
assert.equal(shared.always.count, 4);
|
||||
});
|
||||
|
||||
it('attributes project/local/.mcp.json/import to the per-repo delta', () => {
|
||||
assert.equal(delta.always.tokens, 10 + 5 + 7 + 3); // 25
|
||||
assert.equal(delta.always.count, 4);
|
||||
});
|
||||
|
||||
it('preserves load-pattern buckets within each layer', () => {
|
||||
const mixed = [
|
||||
src('user', 'always', 100),
|
||||
src('plugin:foo', 'on-demand', 40), // plugin skill body → shared, onDemand
|
||||
src('plugin:foo', 'external', 0), // plugin hook → shared, external
|
||||
src('project', 'on-demand', 8), // delta, onDemand
|
||||
];
|
||||
const r = splitManifestByOwnership(mixed);
|
||||
assert.equal(r.shared.always.tokens, 100);
|
||||
assert.equal(r.shared.onDemand.tokens, 40);
|
||||
assert.equal(r.shared.external.count, 1);
|
||||
assert.equal(r.delta.onDemand.tokens, 8);
|
||||
});
|
||||
|
||||
it('is a total partition — every source lands in exactly one layer (no loss, no dup)', () => {
|
||||
const wholeTokens = sources.reduce((a, s) => a + s.estimated_tokens, 0);
|
||||
const buckets = (sum) => sum.always.tokens + sum.onDemand.tokens + sum.external.tokens + sum.unknown.tokens;
|
||||
const counts = (sum) => sum.always.count + sum.onDemand.count + sum.external.count + sum.unknown.count;
|
||||
assert.equal(buckets(shared) + buckets(delta), wholeTokens);
|
||||
assert.equal(counts(shared) + counts(delta), sources.length);
|
||||
});
|
||||
|
||||
it('emits the canonical {always,onDemand,external,unknown:{tokens,count}} shape for both layers', () => {
|
||||
for (const summary of [shared, delta]) {
|
||||
for (const bucket of ['always', 'onDemand', 'external', 'unknown']) {
|
||||
assert.equal(typeof summary[bucket].tokens, 'number', `${bucket}.tokens`);
|
||||
assert.equal(typeof summary[bucket].count, 'number', `${bucket}.count`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('manifest CLI — fixture path (rich-repo with patched HOME)', () => {
|
||||
let fixture;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue