feat(#972): graphify command cutover — first capability owning a command family (ADR-857 phase 4d-impl-2) (#975)

Migrate graphify into a Capability that owns its `graphify` command family,
dispatched via the registry (#961 mechanism) instead of a hardcoded case.
graphify is now an enable/disable plug-in.

- src/graphify-command-router.cts: routeGraphifyCommand (standard route*Command),
  reproduces the removed case EXACTLY (query +--budget, status, diff, build,
  hidden build snapshot, usage/unknown errors); injectable _graphify test seam.
- capabilities/graphify/capability.json: role feature, tier:full, skills:[graphify],
  config:{graphify.enabled default false}, commands:[{family:graphify, module,
  router:routeGraphifyCommand}].
- removed case 'graphify' from gsd-tools.cjs; graphify now flows
  default -> dispatchCapabilityCommand -> commandFamilies.graphify -> router.
- regenerated registry (commandFamilies/bySkill/configSchema/profileMembership/
  capabilityClusters for graphify); tier:full keeps 4c install/surface a no-op.

Equivalence-proven: 8 recording-mock unit tests assert the exact fn+args per
subcommand (budget, snapshot-vs-build); 9 subprocess tests assert distinguishing
output shapes; existing graphify tests pass unchanged through the new path.

Surfaced (not silently accepted): the pre-existing --budget-no-value NaN no-op
quirk, preserved for equivalence, filed separately.

Closes #972

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-10 07:12:57 -04:00
committed by GitHub
parent 354e0e1b94
commit 77bfd943dd
13 changed files with 748 additions and 38 deletions

View File

@@ -1507,33 +1507,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
break;
}
// ─── Graphify ──────────────────────────────────────────────────────────
case 'graphify': {
const graphify = require('./lib/graphify.cjs');
const subcommand = args[1];
if (subcommand === 'query') {
const term = args[2];
if (!term) error('Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
const budgetIdx = args.indexOf('--budget');
const budget = budgetIdx !== -1 ? parseInt(args[budgetIdx + 1], 10) : null;
core.output(graphify.graphifyQuery(cwd, term, { budget }), raw);
} else if (subcommand === 'status') {
core.output(graphify.graphifyStatus(cwd), raw);
} else if (subcommand === 'diff') {
core.output(graphify.graphifyDiff(cwd), raw);
} else if (subcommand === 'build') {
if (args[2] === 'snapshot') {
core.output(graphify.writeSnapshot(cwd), raw);
} else {
core.output(graphify.graphifyBuild(cwd), raw);
}
} else {
error('Unknown graphify subcommand. Available: build, query, status, diff', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
break;
}
// ─── Documentation ────────────────────────────────────────────────────
case 'docs-init': {
@@ -2086,7 +2059,8 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
// An unmigrated command still hits its hardcoded `case` above — untouched.
// A migrated command's `case` is removed at cutover, so it reaches here and
// dispatchCapabilityCommand routes it to the capability's registered router.
// With commandFamilies={} today, this always returns false and is a no-op.
// commandFamilies now includes migrated capabilities (e.g. graphify → graphify-command-router.cjs);
// this returns true when a registered capability owns the command, false otherwise.
if (dispatchCapabilityCommand({ command, args, cwd, raw, error })) break;
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim

View File

@@ -7,6 +7,36 @@
*/
const capabilities = {
"graphify": {
"id": "graphify",
"role": "feature",
"title": "Knowledge graph",
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
"tier": "full",
"requires": [],
"skills": [
"graphify"
],
"agents": [],
"config": {
"graphify.enabled": {
"type": "boolean",
"default": false,
"description": "Enable the graphify knowledge-graph command + skill."
}
},
"commands": [
{
"family": "graphify",
"module": "graphify-command-router.cjs",
"router": "routeGraphifyCommand"
}
],
"hooks": [],
"steps": [],
"contributions": [],
"gates": []
},
"ui": {
"id": "ui",
"role": "feature",
@@ -86,6 +116,7 @@ const capabilities = {
};
const bySkill = {
"graphify": "graphify",
"ui-phase": "ui",
"ui-review": "ui"
};
@@ -202,12 +233,19 @@ const byLoopPoint = {
};
const configKeys = {
"graphify.enabled": "graphify",
"workflow.ui_phase": "ui",
"workflow.ui_review": "ui",
"workflow.ui_safety_gate": "ui"
};
const configSchema = {
"graphify.enabled": {
"owner": "graphify",
"type": "boolean",
"default": false,
"description": "Enable the graphify knowledge-graph command + skill."
},
"workflow.ui_phase": {
"owner": "ui",
"type": "boolean",
@@ -230,9 +268,18 @@ const configSchema = {
const runtimes = {};
const commandFamilies = {};
const commandFamilies = {
"graphify": {
"capId": "graphify",
"module": "graphify-command-router.cjs",
"router": "routeGraphifyCommand"
}
};
const capabilityClusters = {
"graphify": [
"graphify"
],
"ui": [
"ui-phase",
"ui-review"
@@ -240,6 +287,12 @@ const capabilityClusters = {
};
const profileMembership = {
"graphify": {
"tier": "full",
"profiles": [
"full"
]
},
"ui": {
"tier": "full",
"profiles": [
@@ -249,6 +302,7 @@ const profileMembership = {
};
const _requiresGraph = {
"graphify": [],
"ui": []
};