feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2) (#1440)

* feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2)

Promote the registry from a frozen data file to loadRegistry({includeInstalled}),
composing the first-party registry with a validated installed overlay (ADR-1244 D2):

- Extract the conformance validator to a shared runtime-callable module
  (gsd-core/bin/lib/capability-validator.cjs); the generator re-exports it
  verbatim, guarded by a generative-parity test (no build-time/runtime drift).
- capability-loader.cts: loadRegistry({includeInstalled}) composes first-party
  ∪ validated overlay from $GSD_HOME/.gsd/capabilities (global) and
  <root>/.gsd/capabilities (project) via the canonical buildRegistry. First-party
  always wins (id/skill/agent/config/command-family + reserved gsd-/anthropic-
  prefixes); full merged-set cross-capability validation; engines.gsd load-time
  re-gate (skip-with-warning); gate-kind capabilities FAIL CLOSED; fragment-path
  escapes rejected.
- semverSatisfies (hand-written, no dep) for the engines.gsd gate, fail-closed.
- Wire surface/state + loop to the overlay; loop injects a blocking gate for each
  skipped gate-kind overlay (fail-closed).
- cwd-aware overlay config-key federation: config-loader _federatedConfigSchema(cwd)
  + config-schema isValidConfigKey(key, cwd) compose the overlay per loadConfig/
  config-set call (never eager at module load, never wrong-cwd); first-party path
  unchanged with no cwd.
- run-tests.cjs sandboxes GSD_HOME (idempotent — nested spawns reuse it) for test
  hermeticity; capability-loader.cjs git+eslint-ignored (tsc artifact);
  capability-validator.cjs stays linted (#551 migration coverage).

Closes #1431

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1431): add changeset for runtime capability registry overlay

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1431): kill config-schema cwd-aware federation mutants (Stryker ≥52)

The cwd-aware overlay config-key federation added to config-schema.cts
(_capabilityConfigSchema(cwd) + isCapabilityConfigKey/isValidConfigKey cwd
threading) introduced mutable surface uncovered by config-schema's mutation
test set, dropping its score to 39.58% (below the 52 break threshold). Add a
real-overlay-fixture describe block exercising every branch (cwd guard, overlay
loadRegistry, found-branch, first-party fallback, cwd threading); local Stryker
score 39.58% -> 77.08%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-18 14:41:20 -04:00
committed by GitHub
parent 2421cf1b4a
commit 353f63d170
25 changed files with 3389 additions and 1929 deletions

View File

@@ -0,0 +1,275 @@
'use strict';
/**
* capability-loader.test.cjs — ADR-1244 D2 runtime registry overlay.
*
* Behavioral tests for loadRegistry({ includeInstalled }): first-party ∪
* validated overlay composition, first-party-wins collisions, reserved
* namespace, engines.gsd load-time re-gate (skip-with-warning), gate-kind
* fail-closed tracking, and parity with the canonical builder.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { loadRegistry } = require('../gsd-core/bin/lib/capability-loader.cjs');
const baseRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
const { buildRegistry } = require('../scripts/gen-capability-registry.cjs');
const HOST = '1.6.0';
function featureCap(id, extra) {
return {
id, role: 'feature', version: '1.0.0', title: id, description: 'overlay cap',
tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' },
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [],
...extra,
};
}
// Build a temp GSD home containing .gsd/capabilities/<id>/capability.json for each cap.
function makeOverlayHome(caps) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-overlay-'));
for (const cap of caps) {
const dir = path.join(home, '.gsd', 'capabilities', cap.id);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
}
return home;
}
// Always pass cwd === home so the project-root probe cannot wander into the
// real repo; root-dedup makes the project scope a no-op there.
function load(home, opts) {
return loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST, ...opts });
}
describe('loadRegistry — base behavior', () => {
test('without includeInstalled returns the frozen registry (identity-stable)', () => {
assert.strictEqual(loadRegistry(), baseRegistry);
assert.strictEqual(loadRegistry({ includeInstalled: false }), baseRegistry);
});
test('includeInstalled with no overlay directory returns the frozen registry unchanged', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-empty-'));
try {
assert.strictEqual(load(home), baseRegistry);
} finally {
cleanup(home);
}
});
});
describe('loadRegistry — accepting valid overlays', () => {
test('a valid overlay capability appears in every derived view (toggable + federated)', (t) => {
const home = makeOverlayHome([
featureCap('deploy-gate', {
skills: ['deploy-review'],
agents: ['gsd-deploy-checker'],
config: { 'workflow.deploy_gate': { type: 'boolean', default: true, description: 'Enable the deploy gate.' } },
steps: [{ point: 'execute:wave:post', ref: { skill: 'deploy-review' }, produces: ['DEPLOY.md'], consumes: [], when: 'workflow.deploy_gate', onError: 'skip' }],
}),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(reg.capabilities['deploy-gate'], 'overlay in capabilities');
assert.equal(reg.bySkill['deploy-review'], 'deploy-gate', 'overlay skill indexed (surface)');
assert.equal(reg.byAgent['gsd-deploy-checker'], 'deploy-gate', 'overlay agent indexed');
assert.ok(reg.configSchema['workflow.deploy_gate'], 'overlay config federated');
assert.equal(reg.configKeys['workflow.deploy_gate'], 'deploy-gate', 'overlay config key owned');
assert.ok(reg.capabilityClusters['deploy-gate'], 'overlay in capabilityClusters (surface toggle)');
assert.ok(reg.profileMembership['deploy-gate'], 'overlay in profileMembership (surface toggle)');
const wavePost = reg.byLoopPoint['execute:wave:post'];
assert.ok(wavePost && Array.isArray(wavePost.steps) &&
wavePost.steps.some((h) => h.capId === 'deploy-gate'), 'overlay step wired into the loop');
// First-party is preserved.
assert.equal(reg.capabilities['ui'].title, 'UI design contracts');
assert.equal(reg._overlay.warnings.length, 0, 'no warnings when all overlays accepted');
assert.deepEqual(reg._overlay.incompatibleGateCapIds, []);
});
test('composed registry equals buildRegistry over the same merged cap-map (no drift / no dropped caps)', (t) => {
const overlay = featureCap('extra-cap', { skills: ['extra-skill'] });
const home = makeOverlayHome([overlay]);
t.after(() => cleanup(home));
const reg = load(home);
const mergedMap = new Map(Object.entries(baseRegistry.capabilities));
mergedMap.set('extra-cap', overlay);
const expected = buildRegistry(mergedMap);
assert.deepEqual(Object.keys(reg.capabilities).sort(), Object.keys(expected.capabilities).sort());
assert.deepEqual(reg.bySkill, expected.bySkill);
assert.deepEqual(Object.keys(reg.configSchema).sort(), Object.keys(expected.configSchema).sort());
assert.deepEqual(reg.capabilityClusters['extra-cap'], expected.capabilityClusters['extra-cap']);
});
});
describe('loadRegistry — first-party always wins', () => {
test('overlay whose id collides with a first-party id is rejected; first-party preserved', (t) => {
const home = makeOverlayHome([featureCap('ui', { skills: ['hijacked'] })]);
t.after(() => cleanup(home));
const reg = load(home);
assert.equal(reg.capabilities['ui'].title, 'UI design contracts', 'first-party ui untouched');
assert.ok(reg._overlay.warnings.some((w) => w.id === 'ui' && /collide/i.test(w.reason)));
});
test('overlay claiming a first-party skill stem is rejected', (t) => {
const home = makeOverlayHome([featureCap('skill-thief', { skills: ['ui-phase'] })]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['skill-thief']);
assert.ok(reg._overlay.warnings.some((w) => w.id === 'skill-thief' && /skill/i.test(w.reason)));
});
test('reserved id prefix (gsd-/gsd-core-/anthropic-) is rejected', (t) => {
const home = makeOverlayHome([
featureCap('gsd-impostor'),
featureCap('anthropic-impostor'),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['gsd-impostor']);
assert.ok(!reg.capabilities['anthropic-impostor']);
assert.equal(reg._overlay.warnings.filter((w) => /reserved/i.test(w.reason)).length, 2);
});
});
describe('loadRegistry — load-time re-gate (engines.gsd) + fail-closed gates', () => {
test('incompatible engines.gsd is skipped with a warning', (t) => {
const home = makeOverlayHome([featureCap('future-cap', { engines: { gsd: '>=99.0.0' } })]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['future-cap']);
assert.ok(reg._overlay.warnings.some((w) => w.id === 'future-cap' && /incompatible/i.test(w.reason)));
assert.deepEqual(reg._overlay.incompatibleGateCapIds, [], 'no gate declared → not a fail-closed blocker');
});
test('a skipped capability that DECLARES a gate is recorded for fail-closed handling', (t) => {
const home = makeOverlayHome([
featureCap('incompat-gate', {
engines: { gsd: '>=99.0.0' },
gates: [{ point: 'execute:wave:post', check: { query: 'x.deploy' }, blocking: true, onError: 'halt' }],
}),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['incompat-gate'], 'incompatible cap not loaded');
assert.ok(reg._overlay.incompatibleGateCapIds.includes('incompat-gate'), 'gate-kind tracked as fail-closed');
assert.ok(
reg._overlay.blockedGates.some((g) => g.point === 'execute:wave:post' && g.capId === 'incompat-gate'),
'declared gate point recorded for per-point fail-closed injection',
);
});
test('compatible engines.gsd is accepted', (t) => {
const home = makeOverlayHome([featureCap('compat-cap', { engines: { gsd: '>=1.6.0 <3.0.0' } })]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(reg.capabilities['compat-cap']);
});
});
describe('loadRegistry — malformed overlays are skipped, never crash', () => {
test('manifest failing validation is skipped with a warning', (t) => {
const home = makeOverlayHome([
// missing required version → validateCapability error
(() => { const c = featureCap('no-version'); delete c.version; return c; })(),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['no-version']);
assert.ok(reg._overlay.warnings.some((w) => w.id === 'no-version' && /version/i.test(w.reason)));
});
test('unreadable / invalid JSON is skipped with a warning (no throw)', (t) => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-badjson-'));
t.after(() => cleanup(home));
const dir = path.join(home, '.gsd', 'capabilities', 'broken');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'capability.json'), '{ not valid json', 'utf8');
let reg;
assert.doesNotThrow(() => { reg = load(home); });
assert.ok(!reg.capabilities['broken']);
assert.ok(reg._overlay.warnings.some((w) => w.id === 'broken'));
});
test('the loop never crashes — first-party registry remains fully intact alongside bad overlays', (t) => {
const home = makeOverlayHome([
featureCap('gsd-reserved'),
(() => { const c = featureCap('bad'); c.role = 'nonsense'; return c; })(),
featureCap('good', { skills: ['good-only-skill'] }),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.equal(Object.keys(baseRegistry.capabilities).length + 1, Object.keys(reg.capabilities).length,
'exactly the one good overlay is added; first-party count preserved');
assert.ok(reg.capabilities['good']);
});
});
describe('loadRegistry — full merged-set cross-capability validation', () => {
test('overlay claiming a first-party command family is rejected (first-party wins)', (t) => {
const firstPartyFamily = Object.keys(baseRegistry.commandFamilies || {})[0];
assert.ok(firstPartyFamily, 'precondition: first-party owns at least one command family');
const home = makeOverlayHome([
featureCap('cmd-thief', { commands: [{ family: firstPartyFamily, module: 'thief.cjs', router: 'route' }] }),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['cmd-thief'], 'overlay hijacking a first-party command family is not loaded');
assert.ok(reg._overlay.warnings.some((w) => w.id === 'cmd-thief' && /command family/i.test(w.reason)));
assert.ok(reg.commandFamilies[firstPartyFamily], 'first-party command family preserved');
});
test('overlay with an unsatisfiable consumes is rejected by cross-capability validation', (t) => {
const home = makeOverlayHome([
featureCap('bad-consumes', {
skills: ['bad-consumes-skill'],
config: { 'workflow.bad_consumes': { type: 'boolean', default: true, description: 'x' } },
steps: [{ point: 'plan:pre', ref: { skill: 'bad-consumes-skill' }, produces: [], consumes: ['NONEXISTENT-ARTIFACT.md'], when: 'workflow.bad_consumes', onError: 'skip' }],
}),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['bad-consumes'], 'overlay failing consumes-satisfiability is not loaded');
assert.ok(reg._overlay.warnings.some((w) => w.id === 'bad-consumes' && /cross-capability/i.test(w.reason)));
});
test('an invalid hook fragment path (escaping the capability dir) is rejected', (t) => {
const home = makeOverlayHome([
featureCap('frag-escape', {
contributions: [{ point: 'plan:pre', into: 'planner', fragment: { path: '../../../etc/passwd' }, when: 'workflow.frag', onError: 'skip' }],
config: { 'workflow.frag': { type: 'boolean', default: true, description: 'x' } },
}),
]);
t.after(() => cleanup(home));
const reg = load(home);
assert.ok(!reg.capabilities['frag-escape'], 'overlay with an escaping fragment path is not loaded');
assert.ok(reg._overlay.warnings.some((w) => w.id === 'frag-escape' && /fragment/i.test(w.reason)));
});
});
describe('loadRegistry — project-scoped overlay root', () => {
test('reads an overlay from <projectRoot>/.gsd/capabilities when cwd is inside a project', (t) => {
const proj = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-proj-'));
t.after(() => cleanup(proj));
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // project-root marker
const dir = path.join(proj, '.gsd', 'capabilities', 'proj-cap');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('proj-cap', { skills: ['proj-skill'] })), 'utf8');
// Point the global home elsewhere (empty) so only the project scope contributes.
const emptyHome = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-emptyhome-'));
t.after(() => cleanup(emptyHome));
const reg = loadRegistry({ includeInstalled: true, gsdHome: emptyHome, cwd: proj, hostVersion: HOST });
assert.ok(reg.capabilities['proj-cap'], 'project-scoped overlay loaded');
});
});