diff --git a/scanners/manifest.mjs b/scanners/manifest.mjs index 6bafe2f..1940076 100644 --- a/scanners/manifest.mjs +++ b/scanners/manifest.mjs @@ -171,19 +171,26 @@ export function summarizeByLoadPattern(sources) { /** * 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. + * repo: the global ~/.claude CLAUDE.md (`user`) and managed enterprise policy + * (`managed`). Installed plugins are also shared but are matched by the + * `plugin:` prefix below, not by this set. + * + * Deliberately NOT here: `~/.claude.json:projects`. Although that file lives in + * HOME, `readClaudeJsonProjectSlice` returns the slice keyed to the SPECIFIC + * repo path — those MCP servers are per-repo, load only in their own project, + * and differ across repos, so they are a delta (folding them into the + * once-counted shared layer would drop every repo's slice but the first). The + * only machine-global MCP is plugin-provided (caught by the `plugin:` prefix). */ -const SHARED_GLOBAL_SOURCES = Object.freeze(new Set(['user', 'managed', '~/.claude.json:projects'])); +const SHARED_GLOBAL_SOURCES = Object.freeze(new Set(['user', 'managed'])); /** * 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. + * (project / local / .mcp.json / ~/.claude.json:projects / @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'} */ diff --git a/tests/scanners/manifest.test.mjs b/tests/scanners/manifest.test.mjs index d2ca618..cd0da1a 100644 --- a/tests/scanners/manifest.test.mjs +++ b/tests/scanners/manifest.test.mjs @@ -203,11 +203,12 @@ describe('splitManifestByOwnership — shared-global vs per-repo-delta (unit, v5 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'); + // ~/.claude.json:projects is a per-repo MCP slice (keyed by repo path), NOT shared. + assert.equal(classifyOwnership('~/.claude.json:projects'), 'delta'); // import + anything unrecognized → delta (never silently folded into the shared layer). assert.equal(classifyOwnership('import'), 'delta'); assert.equal(classifyOwnership('mystery'), 'delta'); @@ -218,7 +219,7 @@ describe('splitManifestByOwnership — shared-global vs per-repo-delta (unit, v5 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('~/.claude.json:projects', 'always', 20), // per-repo MCP slice → delta src('project', 'always', 10), // project CLAUDE.md/rule → delta src('local', 'always', 5), // local → delta src('.mcp.json', 'always', 7), // project MCP → delta @@ -226,14 +227,14 @@ describe('splitManifestByOwnership — shared-global vs per-repo-delta (unit, v5 ]; 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('folds global/user/managed/plugin into the shared layer', () => { + assert.equal(shared.always.tokens, 100 + 50 + 30); // 180 + assert.equal(shared.always.count, 3); }); - 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('attributes project/local/.mcp.json/claude.json-slice/import to the per-repo delta', () => { + assert.equal(delta.always.tokens, 20 + 10 + 5 + 7 + 3); // 45 + assert.equal(delta.always.count, 5); }); it('preserves load-pattern buckets within each layer', () => {