* 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:
@@ -42,6 +42,10 @@ import federatedConfigModule = require('./federated-config.cjs');
|
||||
const { mergeFederatedConfig } = federatedConfigModule;
|
||||
// The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
|
||||
// Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
|
||||
// This is the FROZEN first-party registry — used as the test-seam default and the
|
||||
// fallback. Overlay (installed third-party) config-key federation is cwd-dependent
|
||||
// and composed PER loadConfig CALL by _federatedConfigSchema(cwd) below (ADR-1244 D2),
|
||||
// never eagerly at module load.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
||||
const _capabilityRegistryReal: { configSchema?: Record<string, unknown> } = require('./capability-registry.cjs');
|
||||
|
||||
@@ -349,11 +353,33 @@ function _applyFederatedValues(
|
||||
* When validKeys is non-empty, applies values into a shallow clone to avoid
|
||||
* mutating shared CONFIG_DEFAULTS/module constants.
|
||||
*/
|
||||
// Resolve the federated capability config-schema for a project (ADR-1244 D2).
|
||||
// A test override (via _setFederatedRegistryForTests) wins; otherwise, when a
|
||||
// project cwd is available, compose the installed overlay for THAT project —
|
||||
// LAZILY (never at module load, so a bare require never scans the filesystem and
|
||||
// the result is never cached for the wrong cwd) — falling back to the frozen
|
||||
// first-party schema when there is no cwd or the loader is unavailable.
|
||||
function _federatedConfigSchema(cwd?: string): Record<string, unknown> | undefined {
|
||||
if (_capabilityRegistry !== _capabilityRegistryReal) {
|
||||
return _capabilityRegistry.configSchema; // explicit test override
|
||||
}
|
||||
if (typeof cwd === 'string' && cwd) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
||||
const loaderMod: { loadRegistry: (o?: Record<string, unknown>) => { configSchema?: Record<string, unknown> } } = require('./capability-loader.cjs');
|
||||
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd }).configSchema;
|
||||
if (schema && typeof schema === 'object') return schema;
|
||||
} catch { /* fall back to first-party */ }
|
||||
}
|
||||
return _capabilityRegistryReal.configSchema;
|
||||
}
|
||||
|
||||
function _applyFederatedOverlay(
|
||||
baseConfig: Record<string, unknown>,
|
||||
userConfig: Record<string, unknown>,
|
||||
cwd?: string,
|
||||
): Record<string, unknown> {
|
||||
const _fedRegistrySchema = _capabilityRegistry.configSchema;
|
||||
const _fedRegistrySchema = _federatedConfigSchema(cwd);
|
||||
if (!_fedRegistrySchema || typeof _fedRegistrySchema !== 'object') return baseConfig;
|
||||
const _fedOverlay = mergeFederatedConfig({
|
||||
configSchema: _fedRegistrySchema,
|
||||
@@ -508,7 +534,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
|
||||
let _preWarningFedValidKeys: string[] = [];
|
||||
try {
|
||||
const _fedRegistrySchemaEarly = _capabilityRegistry.configSchema;
|
||||
const _fedRegistrySchemaEarly = _federatedConfigSchema(cwd);
|
||||
if (_fedRegistrySchemaEarly && typeof _fedRegistrySchemaEarly === 'object') {
|
||||
const _earlyOverlay = mergeFederatedConfig({
|
||||
configSchema: _fedRegistrySchemaEarly,
|
||||
@@ -613,7 +639,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
// ADR-857 phase 3b: federated config overlay
|
||||
try {
|
||||
if (_preWarningFedValidKeys.length > 0) {
|
||||
const _fedRegistrySchema = _capabilityRegistry.configSchema;
|
||||
const _fedRegistrySchema = _federatedConfigSchema(cwd);
|
||||
if (_fedRegistrySchema && typeof _fedRegistrySchema === 'object') {
|
||||
const _fedOverlay = mergeFederatedConfig({
|
||||
configSchema: _fedRegistrySchema,
|
||||
@@ -651,7 +677,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
}
|
||||
// Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
|
||||
try {
|
||||
return { config: _applyFederatedOverlay(defaults, {}), source: 'builtin-defaults', degraded: false };
|
||||
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
||||
} catch {
|
||||
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
||||
}
|
||||
@@ -692,14 +718,14 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
};
|
||||
// Branch D: global-defaults
|
||||
try {
|
||||
return { config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults), source: 'global-defaults', degraded: false };
|
||||
return { config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false };
|
||||
} catch {
|
||||
return { config: _globalBaseCfg, source: 'global-defaults', degraded: false };
|
||||
}
|
||||
} catch {
|
||||
// Branch E: no global defaults
|
||||
try {
|
||||
return { config: _applyFederatedOverlay(defaults, {}), source: 'builtin-defaults', degraded: false };
|
||||
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
||||
} catch {
|
||||
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user