feat(#985): intel command cutover — commands-only capability, last first-party family (ADR-857 phase 4d-impl-4) (#988)

* feat(#985): intel command cutover — commands-only capability, last first-party family (ADR-857 phase 4d-impl-4)

Migrate the intel CLI command family from a hardcoded gsd-tools.cjs case arm to
a registry-dispatched Capability (commandFamilies mechanism, #961), mirroring
the graphify (#972) and audit (#984) cutovers. New src/intel-command-router.cts
exports routeIntelCommand reproducing all 9 subcommands verbatim (incl. the
status timeAgo non-raw post-processing), lazily requiring intel.cjs inside the
route fn. capabilities/intel/capability.json declares the intel command family;
commands-only (skills:[]), declares the existing intel.enabled gate (default
false — behavior unchanged).

Behavior-CHANGING (dispatch path) but equivalence-proven: CLI output identical;
existing intel.test.cjs passes unchanged. Completes the first-party command-
family cutover sequence (graphify/audit/intel).

Closes #985

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

* test(#985): compute expected planningDir via path.join in intel cutover unit tests (Windows CI)

The intel-command-cutover unit-mock assertions hardcoded a POSIX `/.planning`
expectation while the router builds it with path.join(cwd, '.planning') →
backslashes on Windows, so the planningDir-arg assertions (query/status/diff/
snapshot/validate/update/api-surface) failed only on windows-latest CI. Compute
the expectation with path.join (cross-platform); production router unchanged.

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

---------

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 10:00:30 -04:00
committed by GitHub
parent 9a03539c2d
commit caca4d255c
12 changed files with 979 additions and 52 deletions

1
.gitignore vendored
View File

@@ -120,6 +120,7 @@ build/
/gsd-core/bin/lib/graphify.cjs
/gsd-core/bin/lib/graphify-command-router.cjs
/gsd-core/bin/lib/audit-command-router.cjs
/gsd-core/bin/lib/intel-command-router.cjs
/gsd-core/bin/lib/install-profiles.cjs
/gsd-core/bin/lib/intel.cjs
/gsd-core/bin/lib/installer-migrations.cjs

View File

@@ -161,7 +161,7 @@ A named, stable site on a host loop step (per-step `pre`/`post` plus per-wave in
ADR-857 phase 4b unified resolver that composes the three toggle systems (install profile, runtime surface, config activation) into one per-capability view. ADDITIVE — install/surface/workflows untouched; currently consumed by nothing (phase-6 wiring out of scope). Source of truth: `gsd-core/bin/lib/capability-state.cjs` (generated from `src/capability-state.cts`). Interface: `resolveCapabilityState({ registry, installedSkills, surfacedSkills, config, cwd? }) → { capabilities: CapabilityStateEntry[] }` (pure, no I/O); `cmdCapabilityState(cwd, runtimeConfigDir, raw, opts)` (I/O entry point). CLI surface: `gsd-tools capability state [--config-dir <path>]` — emits `{ runtimeConfigDir, capabilities[] }`. Per-capability output: `{ id, tier, skills[], installed, surfaced, hooks[] }` where `installed` = every owned skill ∈ installedSkills (or `installedSkills==='*'`; vacuously true for empty-skills caps), `surfaced` = every owned skill ∈ surfacedSkills (vacuously true for empty-skills caps), `hooks` = `[{ point, kind: 'step'|'gate'|'contribution', when, active }]` derived from the cap's `steps`, `gates`, `contributions` arrays (no `when` → active=true; `when` resolved via `_resolveActivationValue` from loop-resolver). Capabilities sorted by `id` for determinism. Defensive: malformed registry → `{ capabilities: [] }`, never throws; inline literal `__proto__`/`constructor`/`prototype` prototype-pollution guard on capability id keys. `runtimeConfigDir` auto-detection falls back to `getGlobalConfigDir` based on env-var presence (CODEX_HOME → codex, CURSOR_CONFIG_DIR → cursor, GEMINI_CONFIG_DIR → gemini, CLAUDE_CONFIG_DIR → claude, default → claude/`~/.claude`).
### Capability Command Family [Planned — mechanism built, unconsumed]
ADR-959 (phase 4d) — a CLI command family (a top-level `gsd-tools` command and its subcommands) owned by a Capability via a new optional `commands: [{ family, module, router }]` field on the `feature` role. The Capability declares the `family` name, a first-party in-tree `module` (under `gsd-core/bin/lib/`), and the exported `router` — a standard `route*Command({ args, cwd, raw, error })` function identical in shape to the 12 existing host routers (so it routes through the stateless CommandRoutingHub via `routeCjsCommandFamily`, owning its own subcommand list and arg parsing). The registry materializes a `commandFamilies` index (`family → { capId, module, router }`); the formerly-dead `_dispatchNonFamily` shim is replaced by a real `dispatchCapabilityCommand` (exported from `gsd-core/bin/gsd-tools.cjs`) consulted in `runCommand`'s **`default` case** — an unmigrated command hits its hardcoded `case`; a migrated command's `case` is removed so it reaches `default` → registry → router, making collision structurally impossible. The registry *discovers* a router (it does not rebuild a handler table). First-party only; third-party command loading deferred. **Mechanism built (4d-impl-1):** `commands` schema + validator + single-family-ownership cross-check in `gen-capability-registry.cjs`; `commandFamilies` index emitted in the generated `capability-registry.cjs` (currently `{}` — no capability declares commands yet); `dispatchCapabilityCommand` wired into `runCommand`'s `default` case (behavior-preserving today). **Pilot complete (4d-impl-2):** `graphify` cut over as the first real capability command family — `capabilities/graphify/capability.json` bundles the command (`family: graphify`, `module: graphify-command-router.cjs`, `router: routeGraphifyCommand`), skill (`graphify`), config gate (`graphify.enabled`), and `tier: full`; the `case 'graphify':` arm removed from `gsd-tools.cjs`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies.graphify → graphify-command-router.cjs → routeGraphifyCommand`; behavior proven equivalent (all subcommands: build, query, status, diff, build snapshot, unknown subcommand error, usage error, disabled gate). Template for phase-6 per-feature cutovers. **Audit cutover (4d-impl-3):** `audit-uat` and `audit-open` cut over as the second capability command family pair — `capabilities/audit/capability.json` declares two commands (`family: audit-uat`, `module: audit-command-router.cjs`, `router: routeAuditUat`) and (`family: audit-open`, `module: audit-command-router.cjs`, `router: routeAuditOpen`); the `case 'audit-uat':` and `case 'audit-open':` arms removed from `gsd-tools.cjs`; `commandFamilies` now holds `audit-uat`, `audit-open`, and `graphify`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies["audit-uat"|"audit-open"] → audit-command-router.cjs → routeAuditUat|routeAuditOpen`; behavior equivalence proven by existing regression tests (bug-2659, bug-2911, uat.test.cjs) plus new cutover tests. Confirms hyphenated family names pass registry validator (no format restriction beyond non-empty + non-reserved).
ADR-959 (phase 4d) — a CLI command family (a top-level `gsd-tools` command and its subcommands) owned by a Capability via a new optional `commands: [{ family, module, router }]` field on the `feature` role. The Capability declares the `family` name, a first-party in-tree `module` (under `gsd-core/bin/lib/`), and the exported `router` — a standard `route*Command({ args, cwd, raw, error })` function identical in shape to the 12 existing host routers (so it routes through the stateless CommandRoutingHub via `routeCjsCommandFamily`, owning its own subcommand list and arg parsing). The registry materializes a `commandFamilies` index (`family → { capId, module, router }`); the formerly-dead `_dispatchNonFamily` shim is replaced by a real `dispatchCapabilityCommand` (exported from `gsd-core/bin/gsd-tools.cjs`) consulted in `runCommand`'s **`default` case** — an unmigrated command hits its hardcoded `case`; a migrated command's `case` is removed so it reaches `default` → registry → router, making collision structurally impossible. The registry *discovers* a router (it does not rebuild a handler table). First-party only; third-party command loading deferred. **Mechanism built (4d-impl-1):** `commands` schema + validator + single-family-ownership cross-check in `gen-capability-registry.cjs`; `commandFamilies` index emitted in the generated `capability-registry.cjs` (currently `{}` — no capability declares commands yet); `dispatchCapabilityCommand` wired into `runCommand`'s `default` case (behavior-preserving today). **Pilot complete (4d-impl-2):** `graphify` cut over as the first real capability command family — `capabilities/graphify/capability.json` bundles the command (`family: graphify`, `module: graphify-command-router.cjs`, `router: routeGraphifyCommand`), skill (`graphify`), config gate (`graphify.enabled`), and `tier: full`; the `case 'graphify':` arm removed from `gsd-tools.cjs`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies.graphify → graphify-command-router.cjs → routeGraphifyCommand`; behavior proven equivalent (all subcommands: build, query, status, diff, build snapshot, unknown subcommand error, usage error, disabled gate). Template for phase-6 per-feature cutovers. **Audit cutover (4d-impl-3):** `audit-uat` and `audit-open` cut over as the second capability command family pair — `capabilities/audit/capability.json` declares two commands (`family: audit-uat`, `module: audit-command-router.cjs`, `router: routeAuditUat`) and (`family: audit-open`, `module: audit-command-router.cjs`, `router: routeAuditOpen`); the `case 'audit-uat':` and `case 'audit-open':` arms removed from `gsd-tools.cjs`; `commandFamilies` now holds `audit-uat`, `audit-open`, and `graphify`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies["audit-uat"|"audit-open"] → audit-command-router.cjs → routeAuditUat|routeAuditOpen`; behavior equivalence proven by existing regression tests (bug-2659, bug-2911, uat.test.cjs) plus new cutover tests. Confirms hyphenated family names pass registry validator (no format restriction beyond non-empty + non-reserved). **Intel cutover (4d-impl-4, last first-party cutover):** `intel` cut over — `capabilities/intel/capability.json` declares the command (`family: intel`, `module: intel-command-router.cjs`, `router: routeIntelCommand`) and the existing config gate (`intel.enabled`, default false); the `case 'intel':` arm removed from `gsd-tools.cjs`; `commandFamilies` now holds `intel`, `audit-uat`, `audit-open`, and `graphify`; dispatch flows `default → dispatchCapabilityCommand → commandFamilies.intel → intel-command-router.cjs → routeIntelCommand`; all 9 subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and both usage-error paths preserved; non-raw `timeAgo` transform on `status.files[*].updated_at` preserved exactly. `intel.enabled` declared in capability config (`pending-migration` warning expected during staged 3a-impl cutover). Completes the initial 4d capability command cutover batch.
### Runtime Capability [Planned]
A `role: runtime` variant of a Capability (a Capability carries `role: feature | runtime`) that projects GSD's produced artifacts (skills/agents/hooks/commands) onto one host CLI's conventions — config-surface format, artifact-layout kinds, command template, hooks manifest, sandbox tier. It is a declarative descriptor over a fixed first-party primitive vocabulary (not a code adapter); install composes active Feature Capabilities × the chosen Runtime Capability at the InstallPlan seam (ADR-0058). First-party runtimes are authored through the same descriptor a third party would write (dogfooding the interface); tier-1 (Claude Code, Codex, Antigravity) is fully tested, the other existing runtimes ship lower-tier, none dropped. Third-party runtime loading is deferred to a purely additive external loader + trust gate.

View File

@@ -0,0 +1,28 @@
{
"id": "intel",
"role": "feature",
"title": "Codebase intelligence",
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
"tier": "full",
"requires": [],
"skills": [],
"agents": [],
"config": {
"intel.enabled": {
"type": "boolean",
"default": false,
"description": "Enable the intel code-intelligence command."
}
},
"commands": [
{
"family": "intel",
"module": "intel-command-router.cjs",
"router": "routeIntelCommand"
}
],
"hooks": [],
"steps": [],
"contributions": [],
"gates": []
}

View File

@@ -377,6 +377,7 @@ Node.js CLI utility (`gsd-tools.cjs`) with domain modules split across `gsd-core
| `capability-state.cjs` | Unified capability-state resolver — ADR-857 phase 4b; composes install profile, runtime surface, and config activation into one per-capability view; pure `resolveCapabilityState` + I/O `cmdCapabilityState`; `gsd-tools capability state [--config-dir <path>]` |
| `graphify-command-router.cjs` | ADR-959 capability command router — first real capability command cutover (phase 4d-impl-2); extracted from the `case 'graphify':` arm in `gsd-tools.cjs`; dispatches build/query/status/diff subcommands; discovered via `commandFamilies` in the capability registry |
| `audit-command-router.cjs` | ADR-959 capability command router (phase 4d-impl-3); extracted from the `case 'audit-uat':` and `case 'audit-open':` arms in `gsd-tools.cjs`; `routeAuditUat` → `uat.cjs:cmdAuditUat`, `routeAuditOpen` → `audit.cjs:{auditOpenArtifacts,formatAuditReport}`; discovered via `commandFamilies` in the capability registry |
| `intel-command-router.cjs` | ADR-959 capability command router (phase 4d-impl-4, last first-party cutover); extracted from the `case 'intel':` arm in `gsd-tools.cjs`; `routeIntelCommand` → all 9 intel subcommands via lazy `require('./intel.cjs')`; preserves non-raw `timeAgo` transform on `status.files[*].updated_at`; discovered via `commandFamilies` in the capability registry |
---

View File

@@ -307,6 +307,7 @@
"installer-migration-authoring.cjs",
"installer-migration-report.cjs",
"installer-migrations.cjs",
"intel-command-router.cjs",
"intel.cjs",
"io.cjs",
"learnings.cjs",

View File

@@ -370,7 +370,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (104 shipped)
## CLI Modules (105 shipped)
Full listing: `gsd-core/bin/lib/*.cjs`.
@@ -419,6 +419,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `installer-migration-report.cjs` | Installer migration report projection and blocked-action guard for install/update integration |
| `installer-migrations.cjs` | Installer migration planning, artifact classification, install-state persistence, journaled apply, and rollback helpers |
| `intel.cjs` | Codebase intel store backing `/gsd-map-codebase --query` and `gsd-intel-updater` |
| `intel-command-router.cjs` | ADR-959 capability command router for `gsd-tools intel` — extracted from the `case 'intel':` arm in `gsd-tools.cjs`; dispatches query/status/diff/snapshot/patch-meta/validate/extract-exports/update/api-surface subcommands; preserves `timeAgo` transform on `status.files[*].updated_at` in non-raw mode; phase 4d-impl-4 (last first-party cutover) |
| `io.cjs` | CLI I/O primitives — `output`/`error` emission, JSON-error mode, and large-payload temp-file spillover (extracted from `core.cjs`, ADR-857) |
| `learnings.cjs` | Cross-phase learnings extraction for `/gsd-extract-learnings` |
| `legacy-cleanup.cjs` | Detect and remove leftover get-shit-done-cc artifacts; exports `planLegacyCleanup` (pure scan) and `applyLegacyCleanup` (thin IO applier) that root out stale files from the old package across every GSD-managed runtime config directory (#607) |

View File

@@ -86,6 +86,7 @@ export default tseslint.config(
'gsd-core/bin/lib/graphify.cjs',
'gsd-core/bin/lib/graphify-command-router.cjs',
'gsd-core/bin/lib/audit-command-router.cjs',
'gsd-core/bin/lib/intel-command-router.cjs',
'gsd-core/bin/lib/install-profiles.cjs',
'gsd-core/bin/lib/intel.cjs',
'gsd-core/bin/lib/installer-migrations.cjs',

View File

@@ -1436,56 +1436,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
break;
}
// ─── Intel ────────────────────────────────────────────────────────────
case 'intel': {
const intel = require('./lib/intel.cjs');
const subcommand = args[1];
if (subcommand === 'query') {
const term = args[2];
if (!term) error('Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelQuery(term, planningDir), raw);
} else if (subcommand === 'status') {
const planningDir = path.join(cwd, '.planning');
const status = intel.intelStatus(planningDir);
if (!raw && status.files) {
for (const file of Object.values(status.files)) {
if (file.updated_at) {
file.updated_at = core.timeAgo(new Date(file.updated_at));
}
}
}
core.output(status, raw);
} else if (subcommand === 'diff') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelDiff(planningDir), raw);
} else if (subcommand === 'snapshot') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelSnapshot(planningDir), raw);
} else if (subcommand === 'patch-meta') {
const filePath = args[2];
if (!filePath) error('Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
core.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
} else if (subcommand === 'validate') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelValidate(planningDir), raw);
} else if (subcommand === 'extract-exports') {
const filePath = args[2];
if (!filePath) error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
core.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
} else if (subcommand === 'update') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelUpdate(planningDir), raw);
} else if (subcommand === 'api-surface') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelApiSurface(planningDir), raw);
} else {
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
break;
}
// ─── Documentation ────────────────────────────────────────────────────
case 'docs-init': {

View File

@@ -64,6 +64,34 @@ const capabilities = {
"contributions": [],
"gates": []
},
"intel": {
"id": "intel",
"role": "feature",
"title": "Codebase intelligence",
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
"tier": "full",
"requires": [],
"skills": [],
"agents": [],
"config": {
"intel.enabled": {
"type": "boolean",
"default": false,
"description": "Enable the intel code-intelligence command."
}
},
"commands": [
{
"family": "intel",
"module": "intel-command-router.cjs",
"router": "routeIntelCommand"
}
],
"hooks": [],
"steps": [],
"contributions": [],
"gates": []
},
"ui": {
"id": "ui",
"role": "feature",
@@ -261,6 +289,7 @@ const byLoopPoint = {
const configKeys = {
"graphify.enabled": "graphify",
"intel.enabled": "intel",
"workflow.ui_phase": "ui",
"workflow.ui_review": "ui",
"workflow.ui_safety_gate": "ui"
@@ -273,6 +302,12 @@ const configSchema = {
"default": false,
"description": "Enable the graphify knowledge-graph command + skill."
},
"intel.enabled": {
"owner": "intel",
"type": "boolean",
"default": false,
"description": "Enable the intel code-intelligence command."
},
"workflow.ui_phase": {
"owner": "ui",
"type": "boolean",
@@ -310,6 +345,11 @@ const commandFamilies = {
"capId": "graphify",
"module": "graphify-command-router.cjs",
"router": "routeGraphifyCommand"
},
"intel": {
"capId": "intel",
"module": "intel-command-router.cjs",
"router": "routeIntelCommand"
}
};
@@ -341,6 +381,7 @@ const profileMembership = {
const _requiresGraph = {
"audit": [],
"graphify": [],
"intel": [],
"ui": []
};

View File

@@ -39,6 +39,7 @@
"files": [
"bug-2351-intel-kilo-layout.test.cjs",
"bug-3290-intel-updater-layout-block.test.cjs",
"intel-command-cutover.test.cjs",
"intel.test.cjs"
],
"issue": "TBD"

View File

@@ -0,0 +1,147 @@
'use strict';
/**
* Intel command router — CLI subcommand dispatcher for `gsd-tools intel`.
*
* ADR-959 (phase 4d-impl-4): intel command family cutover — last first-party
* command cutover in the initial capability rollout.
* Extracted from the hardcoded `case 'intel':` arm in gsd-tools.cjs.
* Behaviour is preserved byte-for-behaviour from the prior inline case;
* the dispatch path now flows: default → dispatchCapabilityCommand →
* require(intel-command-router.cjs) → routeIntelCommand.
*
* Router signature: { args, cwd, raw, error } — identical to the existing
* host routers. No new handler/arg convention; the capability registry
* discovers this router by name.
*
* Arg indexing (preserved exactly from the original case):
* args[0] = 'intel' (family — matched by dispatchCapabilityCommand)
* args[1] = subcommand (query | status | diff | snapshot | patch-meta |
* validate | extract-exports | update | api-surface)
* args[2] = term (query) | filePath (patch-meta | extract-exports)
*
* Notable: the `status` subcommand applies a `timeAgo` transform on
* `status.files[*].updated_at` in non-raw mode — preserved exactly.
*
* Test seams: pass `_intel` to inject a mock intel module; pass `_core` to
* inject a mock core module (captures `output` calls and provides a
* deterministic `timeAgo` without writing to real stdout). The `_`-prefix
* follows the repo's established seam convention (see audit-command-router.cts
* for the `_core` seam pattern). Production callers omit both.
*
* Note on `error(); return` pairs: in production `error()` calls
* `process.exit(1)` so the `return` is an equivalent no-op halt. The pairs
* are kept for lint/control-flow clarity; they do NOT change behaviour.
*
* Lazy require: intel.cjs is required INSIDE the route function so it is
* only loaded when an intel command is actually dispatched (preserves
* equivalence with the old inline case arm which required it at the top of
* the case block).
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports
import core = require('./core.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import io = require('./io.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import path = require('path');
const { ERROR_REASON } = io;
// ─── Types ────────────────────────────────────────────────────────────────────
interface IntelModule {
intelQuery(term: string, planningDir: string): unknown;
intelStatus(planningDir: string): { files?: Record<string, { updated_at?: string }> };
intelDiff(planningDir: string): unknown;
intelSnapshot(planningDir: string): unknown;
intelValidate(planningDir: string): unknown;
intelUpdate(planningDir: string): unknown;
intelApiSurface(planningDir: string): unknown;
intelPatchMeta(filePath: string): unknown;
intelExtractExports(filePath: string): unknown;
}
interface CoreModule {
output(value: unknown, raw: boolean): void;
timeAgo(date: Date): string;
}
interface RouteIntelCommandOptions {
args: string[];
cwd: string;
raw: boolean;
error: (message: string, reason?: string) => void;
/** Test seam: inject a mock intel module. Defaults to the real module. */
_intel?: IntelModule;
/** Test seam: inject a mock core module to capture output calls and provide
* a deterministic timeAgo. Defaults to the real module. */
_core?: CoreModule;
}
// ─── Implementation ───────────────────────────────────────────────────────────
function routeIntelCommand({ args, cwd, raw, error, _intel, _core }: RouteIntelCommandOptions): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
const intel: IntelModule = _intel ?? require('./intel.cjs');
const c: CoreModule = _core ?? core;
const subcommand = args[1];
if (subcommand === 'query') {
const term = args[2];
if (!term) {
error('Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
return;
}
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelQuery(term, planningDir), raw);
} else if (subcommand === 'status') {
const planningDir = path.join(cwd, '.planning');
const status = intel.intelStatus(planningDir);
if (!raw && status.files) {
for (const file of Object.values(status.files)) {
if (file.updated_at) {
file.updated_at = c.timeAgo(new Date(file.updated_at));
}
}
}
c.output(status, raw);
} else if (subcommand === 'diff') {
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelDiff(planningDir), raw);
} else if (subcommand === 'snapshot') {
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelSnapshot(planningDir), raw);
} else if (subcommand === 'patch-meta') {
const filePath = args[2];
if (!filePath) {
error('Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
return;
}
c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
} else if (subcommand === 'validate') {
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelValidate(planningDir), raw);
} else if (subcommand === 'extract-exports') {
const filePath = args[2];
if (!filePath) {
error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
return;
}
c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
} else if (subcommand === 'update') {
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelUpdate(planningDir), raw);
} else if (subcommand === 'api-surface') {
const planningDir = path.join(cwd, '.planning');
c.output(intel.intelApiSurface(planningDir), raw);
} else {
error(
'Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface',
ERROR_REASON.SDK_UNKNOWN_COMMAND,
);
}
}
export = {
routeIntelCommand,
};

View File

@@ -0,0 +1,755 @@
'use strict';
/**
* intel-command-cutover.test.cjs — ADR-959 phase 4d-impl-4 equivalence tests.
*
* Verifies that the `intel` command family, after cutover from the hardcoded
* `case 'intel':` arm in gsd-tools.cjs to the capability registry dispatch
* path (default → dispatchCapabilityCommand → intel-command-router.cjs →
* routeIntelCommand), behaves identically to the old inline case.
*
* Test categories:
* 1. UNIT (recording mock) — precise arg/call equivalence for each subcommand
* 2. DISPATCH — command reaches the router via default-case registry dispatch
* 3. BEHAVIOR — subprocess output shape assertions (query, status, disabled gate)
* 4. ERROR PATHS — unknown subcommand, usage (missing term/filePath)
* 5. JSON-ERRORS — structured {ok:false,reason,message} for error paths
* 6. REGISTRY — commandFamilies.intel, configSchema["intel.enabled"], capabilities.intel
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const { routeIntelCommand } = require('../gsd-core/bin/lib/intel-command-router.cjs');
// ─── helpers ─────────────────────────────────────────────────────────────────
function makeErrorRecorder() {
const calls = [];
const fn = (msg, reason) => calls.push({ msg, reason });
fn.calls = calls;
return fn;
}
/**
* Build a recording mock for the intel module.
* Each function records its call and returns a sentinel so tests can assert
* on WHICH function was called and with WHICH arguments without real I/O.
*/
function makeIntelMock(overrides = {}) {
const calls = [];
function recorder(name, ...fnArgs) {
const sentinel = { _mock: name, args: fnArgs };
calls.push(sentinel);
return sentinel;
}
return {
calls,
mock: {
intelQuery: (term, planningDir) => recorder('intelQuery', term, planningDir),
intelStatus: (planningDir) => {
const sentinel = recorder('intelStatus', planningDir);
// Return a status with files so the timeAgo loop can be exercised
sentinel.files = overrides.statusFiles ?? {};
return sentinel;
},
intelDiff: (planningDir) => recorder('intelDiff', planningDir),
intelSnapshot: (planningDir) => recorder('intelSnapshot', planningDir),
intelValidate: (planningDir) => recorder('intelValidate', planningDir),
intelUpdate: (planningDir) => recorder('intelUpdate', planningDir),
intelApiSurface: (planningDir) => recorder('intelApiSurface', planningDir),
intelPatchMeta: (filePath) => recorder('intelPatchMeta', filePath),
intelExtractExports: (filePath) => recorder('intelExtractExports', filePath),
...overrides.methods,
},
};
}
function runJsonErrors(args, tmpDir, env = {}) {
const result = runGsdTools(args, tmpDir, { ...env, GSD_JSON_ERRORS: '1' });
assert.strictEqual(result.success, false,
`Expected failure with GSD_JSON_ERRORS=1 for args: ${args.join(' ')}\n` +
`stdout: ${result.output}\nstderr: ${result.error}`);
let parsed;
try {
parsed = JSON.parse(result.error);
} catch (e) {
throw new Error(
`GSD_JSON_ERRORS=1 must emit valid JSON on stderr.\n` +
`Args: ${args.join(' ')}\nstderr: ${result.error}\nparse error: ${e.message}`,
);
}
return parsed;
}
function assertTypedError(parsed, expectedReason, label) {
assert.strictEqual(parsed.ok, false, `${label}: error object must have ok: false`);
assert.strictEqual(parsed.reason, expectedReason,
`${label}: reason must be "${expectedReason}", got: ${parsed.reason}`);
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
`${label}: message must be a non-empty string`);
}
function enableIntel(tmpDir) {
const planningDir = path.join(tmpDir, '.planning');
const configPath = path.join(planningDir, 'config.json');
const config = fs.existsSync(configPath)
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
: {};
// isIntelEnabled() requires the NESTED form { intel: { enabled: true } }.
// A flat dotted key like config['intel.enabled'] = true is NOT recognised.
config.intel = { ...(config.intel ?? {}), enabled: true };
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
}
/**
* Build a recording mock for the core module.
* Captures all core.output() calls so no bytes reach real stdout.
* Provides a deterministic timeAgo that returns a fixed relative string.
*/
function makeCoreMock() {
const outputCalls = [];
return {
outputCalls,
mock: {
output: (value, raw) => { outputCalls.push({ value, raw }); },
timeAgo: (_date) => '2 hours ago',
},
};
}
// ─── 1. UNIT — recording mocks (precise routing equivalence) ─────────────────
describe('intel router: unit tests via recording mocks', () => {
const CWD = '/fake/cwd';
const PLANNING_DIR = path.join(CWD, '.planning');
// ── query ──────────────────────────────────────────────────────────────────
test('routeIntelCommand query: calls intelQuery(term, planningDir)', () => {
const m = makeIntelMock();
const c = makeCoreMock();
const intelCalls = [];
const TERM = 'myterm';
routeIntelCommand({
args: ['intel', 'query', TERM],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...m.mock,
intelQuery: (term, planningDir) => {
intelCalls.push({ fn: 'intelQuery', term, planningDir });
return { matches: [], total: 0, term };
},
},
});
assert.strictEqual(intelCalls.length, 1, 'intelQuery must be called once');
assert.strictEqual(intelCalls[0].term, TERM, 'term must be forwarded');
assert.strictEqual(intelCalls[0].planningDir, PLANNING_DIR,
'planningDir must be path.join(cwd, ".planning")');
// core.output must be called with the query result (no real stdout write)
assert.strictEqual(c.outputCalls.length, 1, 'core.output must be called once for query result');
});
test('routeIntelCommand query: missing term calls error(USAGE)', () => {
const errFn = makeErrorRecorder();
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'query'],
cwd: CWD, raw: false, error: errFn,
_core: c.mock,
_intel: makeIntelMock().mock,
});
assert.strictEqual(errFn.calls.length, 1, 'error must be called for missing term');
assert.ok(errFn.calls[0].msg.includes('gsd-tools intel query <term>'),
'usage error must mention correct usage');
assert.strictEqual(errFn.calls[0].reason, 'usage',
'reason must be "usage" (ERROR_REASON.USAGE)');
// core.output must NOT be called on usage error (early return)
assert.strictEqual(c.outputCalls.length, 0, 'core.output must not be called on usage error');
});
// ── status ─────────────────────────────────────────────────────────────────
test('routeIntelCommand status (raw=false): calls intelStatus; applies timeAgo on updated_at', () => {
const ISO_DATE = '2020-01-01T00:00:00.000Z';
const statusCalls = [];
let capturedStatus = null;
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'status'],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...makeIntelMock().mock,
intelStatus: (planningDir) => {
statusCalls.push(planningDir);
// Return a status object with a file that has updated_at
capturedStatus = {
files: {
'file-roles.json': { updated_at: ISO_DATE, stale: false },
},
overall_stale: false,
};
return capturedStatus;
},
},
});
assert.strictEqual(statusCalls.length, 1, 'intelStatus must be called once');
assert.strictEqual(statusCalls[0], PLANNING_DIR, 'planningDir must be path.join(cwd, ".planning")');
// core.output must be called exactly once with the (mutated) status
assert.strictEqual(c.outputCalls.length, 1, 'core.output must be called once for status result');
assert.strictEqual(c.outputCalls[0].raw, false, 'core.output must be called with raw=false');
// The timeAgo transform must have mutated updated_at on the status object
// (c.mock.timeAgo returns '2 hours ago' deterministically)
const updatedAt = capturedStatus.files['file-roles.json'].updated_at;
assert.strictEqual(updatedAt, '2 hours ago',
'non-raw mode: updated_at must be replaced with the timeAgo string from core.timeAgo()');
assert.notStrictEqual(updatedAt, ISO_DATE,
'non-raw mode: updated_at must no longer be an ISO string');
});
test('routeIntelCommand status (raw=true): calls intelStatus; does NOT apply timeAgo', () => {
const ISO_DATE = '2020-01-01T00:00:00.000Z';
let capturedStatus = null;
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'status'],
cwd: CWD,
raw: true,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...makeIntelMock().mock,
intelStatus: (_planningDir) => {
capturedStatus = {
files: {
'file-roles.json': { updated_at: ISO_DATE, stale: false },
},
};
return capturedStatus;
},
},
});
// core.output must be called exactly once
assert.strictEqual(c.outputCalls.length, 1, 'core.output must be called once for status result');
assert.strictEqual(c.outputCalls[0].raw, true, 'core.output must be called with raw=true');
// raw=true must skip the timeAgo loop — updated_at must remain unchanged
const updatedAt = capturedStatus.files['file-roles.json'].updated_at;
assert.strictEqual(updatedAt, ISO_DATE,
'raw=true mode: updated_at must NOT be transformed — it must remain an ISO string');
});
test('routeIntelCommand status: files without updated_at are left untouched', () => {
let capturedStatus = null;
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'status'],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...makeIntelMock().mock,
intelStatus: () => {
capturedStatus = {
files: {
'some-file.json': { stale: true }, // no updated_at
},
};
return capturedStatus;
},
},
});
// Must not error and file object must be unchanged (no updated_at added)
assert.ok(!('updated_at' in capturedStatus.files['some-file.json']),
'files without updated_at must not have it added by the transform');
assert.strictEqual(c.outputCalls.length, 1, 'core.output must still be called once');
});
// ── planningDir-only subcommands ───────────────────────────────────────────
for (const subcommand of ['diff', 'snapshot', 'validate', 'update', 'api-surface']) {
// Build the expected function name: 'diff' → 'intelDiff', 'api-surface' → 'intelApiSurface'
const fnName = 'intel' + subcommand.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, c => c.toUpperCase());
test(`routeIntelCommand ${subcommand}: calls ${fnName}(planningDir)`, () => {
const calls = [];
const coreMock = makeCoreMock();
const mockMethod = (planningDir) => {
calls.push(planningDir);
return { result: subcommand };
};
routeIntelCommand({
args: ['intel', subcommand],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: coreMock.mock,
_intel: { ...makeIntelMock().mock, [fnName]: mockMethod },
});
assert.strictEqual(calls.length, 1, `${fnName} must be called once`);
assert.strictEqual(calls[0], PLANNING_DIR,
`${fnName} must be called with path.join(cwd, ".planning")`);
assert.strictEqual(coreMock.outputCalls.length, 1, `core.output must be called once for ${subcommand}`);
});
}
// ── patch-meta ─────────────────────────────────────────────────────────────
test('routeIntelCommand patch-meta: calls intelPatchMeta(path.resolve(cwd, filePath))', () => {
const calls = [];
const c = makeCoreMock();
const FILE_ARG = 'src/auth.ts';
const EXPECTED = path.resolve(CWD, FILE_ARG);
routeIntelCommand({
args: ['intel', 'patch-meta', FILE_ARG],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...makeIntelMock().mock,
intelPatchMeta: (fp) => { calls.push(fp); return { ok: true }; },
},
});
assert.strictEqual(calls.length, 1, 'intelPatchMeta must be called once');
assert.strictEqual(calls[0], EXPECTED,
'intelPatchMeta must receive path.resolve(cwd, filePath)');
assert.strictEqual(c.outputCalls.length, 1, 'core.output must be called once for patch-meta result');
});
test('routeIntelCommand patch-meta: missing filePath calls error(USAGE)', () => {
const errFn = makeErrorRecorder();
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'patch-meta'],
cwd: CWD, raw: false, error: errFn,
_core: c.mock,
_intel: makeIntelMock().mock,
});
assert.strictEqual(errFn.calls.length, 1, 'error must be called for missing filePath');
assert.ok(errFn.calls[0].msg.includes('gsd-tools intel patch-meta <file-path>'),
'usage error must mention correct usage');
assert.strictEqual(errFn.calls[0].reason, 'usage',
'reason must be "usage" (ERROR_REASON.USAGE)');
assert.strictEqual(c.outputCalls.length, 0, 'core.output must not be called on usage error');
});
// ── extract-exports ────────────────────────────────────────────────────────
test('routeIntelCommand extract-exports: calls intelExtractExports(path.resolve(cwd, filePath))', () => {
const calls = [];
const c = makeCoreMock();
const FILE_ARG = 'lib/core.cjs';
const EXPECTED = path.resolve(CWD, FILE_ARG);
routeIntelCommand({
args: ['intel', 'extract-exports', FILE_ARG],
cwd: CWD,
raw: false,
error: makeErrorRecorder(),
_core: c.mock,
_intel: {
...makeIntelMock().mock,
intelExtractExports: (fp) => { calls.push(fp); return { exports: [] }; },
},
});
assert.strictEqual(calls.length, 1, 'intelExtractExports must be called once');
assert.strictEqual(calls[0], EXPECTED,
'intelExtractExports must receive path.resolve(cwd, filePath)');
assert.strictEqual(c.outputCalls.length, 1, 'core.output must be called once for extract-exports result');
});
test('routeIntelCommand extract-exports: missing filePath calls error(USAGE)', () => {
const errFn = makeErrorRecorder();
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'extract-exports'],
cwd: CWD, raw: false, error: errFn,
_core: c.mock,
_intel: makeIntelMock().mock,
});
assert.strictEqual(errFn.calls.length, 1, 'error must be called for missing filePath');
assert.ok(errFn.calls[0].msg.includes('gsd-tools intel extract-exports <file-path>'),
'usage error must mention correct usage');
assert.strictEqual(errFn.calls[0].reason, 'usage',
'reason must be "usage" (ERROR_REASON.USAGE)');
assert.strictEqual(c.outputCalls.length, 0, 'core.output must not be called on usage error');
});
// ── unknown subcommand ─────────────────────────────────────────────────────
test('routeIntelCommand unknown subcommand: calls error(SDK_UNKNOWN_COMMAND)', () => {
const errFn = makeErrorRecorder();
const c = makeCoreMock();
routeIntelCommand({
args: ['intel', 'nonexistent'],
cwd: CWD, raw: false, error: errFn,
_core: c.mock,
_intel: makeIntelMock().mock,
});
assert.strictEqual(errFn.calls.length, 1, 'error must be called for unknown subcommand');
assert.ok(errFn.calls[0].msg.includes('Unknown intel subcommand'),
`error message must say "Unknown intel subcommand"`);
assert.strictEqual(errFn.calls[0].reason, 'sdk_unknown_command',
'reason must be "sdk_unknown_command" (ERROR_REASON.SDK_UNKNOWN_COMMAND)');
assert.strictEqual(c.outputCalls.length, 0, 'core.output must not be called for unknown subcommand');
});
});
// ─── 2. DISPATCH — intel reaches the router via default-case registry ─────────
describe('intel cutover: dispatch path (default-case → capability registry)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('gsd-intel-cutover-');
});
afterEach(() => {
cleanup(tmpDir);
});
test('intel subcommand does not emit "Unknown command: intel" (reaches capability router)', () => {
// With intel.enabled absent/false the router will return a disabled payload —
// but it must NOT fall through to the "Unknown command" error.
const result = runGsdTools(['intel', 'status'], tmpDir);
const stderr = result.error || '';
assert.strictEqual(
stderr.includes('Unknown command: intel'),
false,
`Must not emit "Unknown command: intel". stderr: ${stderr}`,
);
// The disabled gate returns a JSON payload (exit 0) — or the command succeeds
assert.ok(result.success,
`intel status must exit 0. stderr: ${stderr}`);
});
test('intel status --raw dispatches correctly (no "Unknown command")', () => {
const result = runGsdTools(['intel', 'status', '--raw'], tmpDir);
const stderr = result.error || '';
assert.strictEqual(
stderr.includes('Unknown command: intel'),
false,
`intel status --raw must not emit "Unknown command: intel". stderr: ${stderr}`,
);
});
});
// ─── 3. BEHAVIOR — subprocess output shape (equivalence to old inline cases) ──
describe('intel cutover: output shape equivalence', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('gsd-intel-behavior-');
});
afterEach(() => {
cleanup(tmpDir);
});
test('intel status (disabled gate): exits 0, produces JSON payload with disabled:true', () => {
// intel.enabled absent — disabled gate response from intel.cjs
const result = runGsdTools(['intel', 'status'], tmpDir);
assert.ok(result.success, `intel status must exit 0. stderr: ${result.error}`);
let parsed;
assert.doesNotThrow(
() => { parsed = JSON.parse(result.output); },
'intel status must emit valid JSON when disabled',
);
assert.strictEqual(parsed.disabled, true, 'disabled gate must set disabled: true');
});
test('intel query (disabled gate): exits 0, produces JSON with disabled:true', () => {
const result = runGsdTools(['intel', 'query', 'someterm'], tmpDir);
assert.ok(result.success, `intel query must exit 0 (disabled gate). stderr: ${result.error}`);
let parsed;
assert.doesNotThrow(
() => { parsed = JSON.parse(result.output); },
'intel query disabled gate must emit valid JSON',
);
assert.strictEqual(parsed.disabled, true, 'disabled gate must set disabled: true on query');
});
test('intel status --raw (disabled gate): exits 0, produces JSON', () => {
const result = runGsdTools(['intel', 'status', '--raw'], tmpDir);
assert.ok(result.success, `intel status --raw must exit 0. stderr: ${result.error}`);
let parsed;
assert.doesNotThrow(
() => { parsed = JSON.parse(result.output); },
'intel status --raw must emit valid JSON',
);
assert.strictEqual(parsed.disabled, true, 'raw disabled gate must still set disabled: true');
});
test('intel status (non-raw, enabled, with intel files): updated_at is a timeAgo string', () => {
enableIntel(tmpDir);
const planningDir = path.join(tmpDir, '.planning');
const intelDir = path.join(planningDir, 'intel');
fs.mkdirSync(intelDir, { recursive: true });
// Write a file-roles.json intel file with an old updated_at
const oldDate = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); // 2 hours ago
const intelData = {
_meta: { updated_at: oldDate },
entries: {},
};
fs.writeFileSync(path.join(intelDir, 'file-roles.json'), JSON.stringify(intelData, null, 2), 'utf8');
const result = runGsdTools(['intel', 'status'], tmpDir);
assert.ok(result.success, `intel status must exit 0. stderr: ${result.error}`);
let parsed;
assert.doesNotThrow(
() => { parsed = JSON.parse(result.output); },
'intel status must emit valid JSON',
);
// Confirm intel is actually ENABLED (not hitting the disabled gate)
assert.strictEqual(parsed.disabled, undefined,
`intel must be enabled; got disabled gate response instead. config may not be nested correctly.\noutput: ${result.output}`);
// Non-raw: file-roles.json must be present with updated_at set
const fileEntry = parsed.files?.['file-roles.json'];
assert.ok(fileEntry, `intel status must include file-roles.json in files. Got: ${JSON.stringify(parsed)}`);
assert.ok(fileEntry.updated_at,
`file-roles.json must have updated_at. Got: ${JSON.stringify(fileEntry)}`);
// The timeAgo transform converts ISO → "X hours ago" (never an ISO format)
const isIso = /^\d{4}-\d{2}-\d{2}T/.test(fileEntry.updated_at);
assert.strictEqual(isIso, false,
`Non-raw updated_at must be a timeAgo string, not an ISO date. Got: ${fileEntry.updated_at}`);
});
test('intel status raw=true (enabled, with intel files): updated_at remains an ISO string', () => {
enableIntel(tmpDir);
const planningDir = path.join(tmpDir, '.planning');
const intelDir = path.join(planningDir, 'intel');
fs.mkdirSync(intelDir, { recursive: true });
const isoDate = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
const intelData = {
_meta: { updated_at: isoDate },
entries: {},
};
fs.writeFileSync(path.join(intelDir, 'file-roles.json'), JSON.stringify(intelData, null, 2), 'utf8');
const result = runGsdTools(['intel', 'status', '--raw'], tmpDir);
assert.ok(result.success, `intel status --raw must exit 0. stderr: ${result.error}`);
let parsed;
assert.doesNotThrow(
() => { parsed = JSON.parse(result.output); },
'intel status --raw must emit valid JSON',
);
// Confirm intel is actually ENABLED (not hitting the disabled gate)
assert.strictEqual(parsed.disabled, undefined,
`intel must be enabled; got disabled gate response instead. config may not be nested correctly.\noutput: ${result.output}`);
// raw=true: file-roles.json must be present with updated_at set
const fileEntry = parsed.files?.['file-roles.json'];
assert.ok(fileEntry, `intel status --raw must include file-roles.json in files. Got: ${JSON.stringify(parsed)}`);
assert.ok(fileEntry.updated_at,
`file-roles.json must have updated_at in raw mode. Got: ${JSON.stringify(fileEntry)}`);
// raw=true: updated_at must remain an ISO string (no timeAgo transform)
const isIso = /^\d{4}-\d{2}-\d{2}T/.test(fileEntry.updated_at);
assert.strictEqual(isIso, true,
`--raw updated_at must remain an ISO string. Got: ${fileEntry.updated_at}`);
});
});
// ─── 4. ERROR PATHS — unknown subcommand, usage errors ───────────────────────
describe('intel cutover: error paths (exit non-zero + correct messages)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('gsd-intel-err-');
});
afterEach(() => {
cleanup(tmpDir);
});
test('intel query (missing term): exits non-zero, emits usage message', () => {
const result = runGsdTools(['intel', 'query'], tmpDir);
assert.strictEqual(result.success, false, 'intel query without term must exit non-zero');
const output = result.error + result.output;
assert.ok(
output.includes('gsd-tools intel query <term>'),
`Must emit usage hint for missing term. Got: ${output}`,
);
});
test('intel patch-meta (missing filePath): exits non-zero, emits usage message', () => {
const result = runGsdTools(['intel', 'patch-meta'], tmpDir);
assert.strictEqual(result.success, false, 'intel patch-meta without filePath must exit non-zero');
const output = result.error + result.output;
assert.ok(
output.includes('gsd-tools intel patch-meta <file-path>'),
`Must emit usage hint for missing filePath. Got: ${output}`,
);
});
test('intel extract-exports (missing filePath): exits non-zero, emits usage message', () => {
const result = runGsdTools(['intel', 'extract-exports'], tmpDir);
assert.strictEqual(result.success, false, 'intel extract-exports without filePath must exit non-zero');
const output = result.error + result.output;
assert.ok(
output.includes('gsd-tools intel extract-exports <file-path>'),
`Must emit usage hint for missing filePath. Got: ${output}`,
);
});
test('intel unknown subcommand: exits non-zero, emits "Unknown intel subcommand"', () => {
const result = runGsdTools(['intel', 'bogussubcmd'], tmpDir);
assert.strictEqual(result.success, false, 'intel unknown subcommand must exit non-zero');
const output = result.error + result.output;
assert.ok(
output.includes('Unknown intel subcommand'),
`Must emit "Unknown intel subcommand". Got: ${output}`,
);
// Must list all 9 valid subcommands
const EXPECTED_SUBCMDS = ['query', 'status', 'update', 'diff', 'snapshot', 'patch-meta', 'validate', 'extract-exports', 'api-surface'];
for (const sc of EXPECTED_SUBCMDS) {
assert.ok(output.includes(sc),
`Unknown subcommand error must list "${sc}". Got: ${output}`);
}
});
});
// ─── 5. JSON-ERRORS — GSD_JSON_ERRORS mode ───────────────────────────────────
describe('intel cutover: GSD_JSON_ERRORS structured error payloads', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('gsd-intel-jsonerr-');
});
afterEach(() => {
cleanup(tmpDir);
});
test('intel query (missing term) with GSD_JSON_ERRORS=1 emits {ok:false,reason:"usage"}', () => {
const parsed = runJsonErrors(['intel', 'query'], tmpDir);
assertTypedError(parsed, 'usage', 'intel query missing term');
assert.ok(parsed.message.includes('gsd-tools intel query <term>'),
'usage message must include the usage hint');
});
test('intel patch-meta (missing filePath) with GSD_JSON_ERRORS=1 emits {ok:false,reason:"usage"}', () => {
const parsed = runJsonErrors(['intel', 'patch-meta'], tmpDir);
assertTypedError(parsed, 'usage', 'intel patch-meta missing filePath');
assert.ok(parsed.message.includes('gsd-tools intel patch-meta <file-path>'),
'usage message must include the usage hint');
});
test('intel extract-exports (missing filePath) with GSD_JSON_ERRORS=1 emits {ok:false,reason:"usage"}', () => {
const parsed = runJsonErrors(['intel', 'extract-exports'], tmpDir);
assertTypedError(parsed, 'usage', 'intel extract-exports missing filePath');
assert.ok(parsed.message.includes('gsd-tools intel extract-exports <file-path>'),
'usage message must include the usage hint');
});
test('intel unknown subcommand with GSD_JSON_ERRORS=1 emits {ok:false,reason:"sdk_unknown_command"}', () => {
const parsed = runJsonErrors(['intel', 'notasubcmd'], tmpDir);
assertTypedError(parsed, 'sdk_unknown_command', 'intel unknown subcommand');
assert.ok(parsed.message.includes('Unknown intel subcommand'),
'sdk_unknown_command message must say "Unknown intel subcommand"');
});
test('intel status (disabled gate) with GSD_JSON_ERRORS=1 does NOT emit error payload (succeeds)', () => {
// The disabled gate is not a CLI error — it exits 0 with a JSON payload
const result = runGsdTools(['intel', 'status'], tmpDir, { GSD_JSON_ERRORS: '1' });
assert.ok(result.success,
`intel status disabled gate must exit 0 with GSD_JSON_ERRORS=1. stderr: ${result.error}`);
});
});
// ─── 6. REGISTRY — commandFamilies.intel + configSchema + capabilities.intel ──
describe('intel cutover: registry entries correct', () => {
test('commandFamilies["intel"] present and well-shaped', () => {
const entry = registry.commandFamilies['intel'];
assert.ok(entry, 'commandFamilies["intel"] must be present');
assert.strictEqual(entry.capId, 'intel',
'commandFamilies["intel"].capId must be "intel"');
assert.strictEqual(entry.module, 'intel-command-router.cjs',
'commandFamilies["intel"].module must be "intel-command-router.cjs"');
assert.strictEqual(entry.router, 'routeIntelCommand',
'commandFamilies["intel"].router must be "routeIntelCommand"');
});
test('capabilities.intel present with role:feature and tier:full', () => {
const cap = registry.capabilities.intel;
assert.ok(cap, 'capabilities.intel must be present');
assert.strictEqual(cap.role, 'feature', 'intel capability must have role: feature');
assert.strictEqual(cap.tier, 'full', 'intel capability must have tier: full');
});
test('capabilities.intel.commands has exactly one entry with family "intel"', () => {
const cap = registry.capabilities.intel;
assert.ok(Array.isArray(cap.commands) && cap.commands.length === 1,
'intel capability must have exactly 1 command');
const cmd = cap.commands[0];
assert.strictEqual(cmd.family, 'intel', 'command family must be "intel"');
assert.strictEqual(cmd.module, 'intel-command-router.cjs',
'command module must be "intel-command-router.cjs"');
assert.strictEqual(cmd.router, 'routeIntelCommand',
'command router must be "routeIntelCommand"');
});
test('configSchema["intel.enabled"] present with expected shape', () => {
const schemaEntry = registry.configSchema['intel.enabled'];
assert.ok(schemaEntry, 'configSchema["intel.enabled"] must be present');
assert.strictEqual(schemaEntry.owner, 'intel',
'configSchema["intel.enabled"].owner must be "intel"');
assert.strictEqual(schemaEntry.type, 'boolean',
'intel.enabled must have type: boolean');
assert.strictEqual(schemaEntry.default, false,
'intel.enabled must default to false');
});
test('routeIntelCommand is an exported function', () => {
assert.strictEqual(typeof routeIntelCommand, 'function',
'routeIntelCommand must be an exported function');
});
test('intel has no skills — vacuous profileMembership (no entry)', () => {
// intel capability declares skills:[] → no skill-cluster-based profileMembership entry
const cap = registry.capabilities.intel;
assert.deepStrictEqual(cap.skills, [],
'intel capability must have empty skills array');
const pm = registry.profileMembership.intel;
assert.strictEqual(pm, undefined,
'profileMembership.intel must be undefined (no skills declared)');
});
test('intel has no skills — vacuous capabilityClusters (no entry)', () => {
const clusters = registry.capabilityClusters.intel;
assert.strictEqual(clusters, undefined,
'capabilityClusters.intel must be undefined (no skills declared)');
});
test('graphify/audit-uat/audit-open commandFamilies entries still present (no regression)', () => {
assert.ok(registry.commandFamilies['graphify'], 'commandFamilies["graphify"] must still be present');
assert.ok(registry.commandFamilies['audit-uat'], 'commandFamilies["audit-uat"] must still be present');
assert.ok(registry.commandFamilies['audit-open'], 'commandFamilies["audit-open"] must still be present');
});
});