* feat(#1435): capability matrix (generated + drift-guarded) + trust-model doc consolidation ADR-1244 Phase 6. Adds the capability matrix reference, generated FROM the committed registry so it can never drift from the actual capability set: - scripts/gen-capability-matrix.cjs (--write / --check); --check is a CI drift guard. - tests/capability-matrix-sync.test.cjs (4 tests): drift guard, buildMatrix==committed, every cap present, no placeholders. - docs/reference/capability-matrix.md regenerated from the registry (release-stable: shows engines.gsd, omits the lockstep per-cap version that would churn the file every release). - Consolidated the duplicate trust-model doc: deleted docs/explanation/the-capability-trust-model.md, merged its content into capability-trust-model.md, redirected ~10 references; no stale links remain. - Diataxis verification (now that gsd capability is a real command): corrected the matrix's third-party section — the matrix is the first-party catalogue; the overlay-aware view of installed third-party capabilities is 'gsd capability list', not this generated file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1435): Added changeset for the capability matrix reference Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1435): address code-review — non-vacuous matrix test + generator polish - capability-matrix-sync.test.cjs: assert the 'security registers a ship:pre gate' precondition unconditionally so the extension-point check can never degrade to a vacuous pass on registry drift. - gen-capability-matrix.cjs: warn (stderr) on an unknown loop point at generation time; rename enginesOf -> fmtEngines for consistency with the other fmt* helpers (output unchanged). - capability-trust-model.md: point the two how-to links at the real files (import-a-capability-from-a-url.md, version-a-capability.md) instead of the bare directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1435): backfill changeset PR number → #1458 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
285 lines
13 KiB
JavaScript
285 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* gen-capability-matrix.cjs — ADR-1244 Phase 6 (Decision D9).
|
|
*
|
|
* Generates docs/reference/capability-matrix.md FROM the committed capability
|
|
* registry (gsd-core/bin/lib/capability-registry.cjs), so the matrix can never
|
|
* drift from the actual capability set. Kept honest by a drift guard
|
|
* (tests/capability-matrix-sync.test.cjs runs `--check`).
|
|
*
|
|
* The matrix is RELEASE-STABLE by design: it does NOT embed each capability's
|
|
* exact `version` (which tracks the GSD package version in lockstep and would
|
|
* churn the committed file — and trip the drift guard — on every release). It
|
|
* shows `engines.gsd` (the stable host-compatibility RANGE) instead, and notes
|
|
* the version-lockstep rule in prose. The committed matrix therefore changes
|
|
* only on intentional capability edits (add/remove a capability, change its
|
|
* tier/role/engines/extension-points/hook-kinds) — never on a version bump.
|
|
*
|
|
* Usage:
|
|
* node scripts/gen-capability-matrix.cjs # print to stdout
|
|
* node scripts/gen-capability-matrix.cjs --write # write the committed file
|
|
* node scripts/gen-capability-matrix.cjs --check # exit 1 if the committed file is stale
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const REGISTRY_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs');
|
|
const MATRIX_PATH = path.join(ROOT, 'docs', 'reference', 'capability-matrix.md');
|
|
|
|
/** Canonical loop extension points, in order (mirrors the phase loop). */
|
|
const LOOP_POINTS = [
|
|
'discuss:pre', 'discuss:post',
|
|
'plan:pre', 'plan:post',
|
|
'execute:pre', 'execute:wave:pre', 'execute:wave:post', 'execute:post',
|
|
'verify:pre', 'verify:post',
|
|
'ship:pre', 'ship:post',
|
|
];
|
|
const POINT_ORDER = new Map(LOOP_POINTS.map((p, i) => [p, i]));
|
|
|
|
/**
|
|
* Build a capId → { points:Set, kinds:Set } map from the registry's byLoopPoint
|
|
* index — the authoritative record of which loop points each capability registers
|
|
* into and with which hook kind (step / contribution / gate).
|
|
*/
|
|
function extensionsByCapability(registry) {
|
|
const out = new Map();
|
|
const byPoint = registry.byLoopPoint || {};
|
|
const KIND = { steps: 'step', contributions: 'contribution', gates: 'gate' };
|
|
for (const point of Object.keys(byPoint)) {
|
|
const reg = byPoint[point] || {};
|
|
for (const arrKey of ['steps', 'contributions', 'gates']) {
|
|
for (const hook of reg[arrKey] || []) {
|
|
const capId = hook && hook.capId;
|
|
if (typeof capId !== 'string') continue;
|
|
let e = out.get(capId);
|
|
if (!e) { e = { points: new Set(), kinds: new Set() }; out.set(capId, e); }
|
|
e.points.add(point);
|
|
e.kinds.add(KIND[arrKey]);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function fmtPoints(set) {
|
|
if (!set || set.size === 0) return '—';
|
|
for (const p of set) {
|
|
// Surface a typo'd/unknown loop point at generation time rather than silently sorting it last.
|
|
// The registry validates point names at load, so this should never fire — but if it does, the
|
|
// generator (not a confused reader) is where it must be caught.
|
|
if (!POINT_ORDER.has(p)) {
|
|
process.stderr.write(`gen-capability-matrix: WARNING — unknown loop point "${p}" (not one of the ${LOOP_POINTS.length} canonical points)\n`);
|
|
}
|
|
}
|
|
return [...set]
|
|
.sort((a, b) => (POINT_ORDER.has(a) ? POINT_ORDER.get(a) : 99) - (POINT_ORDER.has(b) ? POINT_ORDER.get(b) : 99) || a.localeCompare(b))
|
|
.map((p) => '`' + p + '`')
|
|
.join(', ');
|
|
}
|
|
|
|
function fmtKinds(set) {
|
|
if (!set || set.size === 0) return '—';
|
|
const order = { step: 0, contribution: 1, gate: 2 };
|
|
return [...set].sort((a, b) => (order[a] ?? 9) - (order[b] ?? 9)).join(', ');
|
|
}
|
|
|
|
function fmtEngines(cap) {
|
|
const g = cap && cap.engines && cap.engines.gsd;
|
|
return typeof g === 'string' && g ? '`' + g + '`' : '—';
|
|
}
|
|
|
|
/** Render one capability table (rows sorted by id) for the given role. */
|
|
function renderTable(caps, role, extByCap) {
|
|
const rows = caps
|
|
.filter((c) => c.role === role)
|
|
.sort((a, b) => a.id.localeCompare(b.id))
|
|
.map((c) => {
|
|
const ext = extByCap.get(c.id) || { points: null, kinds: null };
|
|
return `| \`${c.id}\` | ${c.role} | ${c.tier || '—'} | ${fmtEngines(c)} | ${fmtPoints(ext.points)} | ${fmtKinds(ext.kinds)} | first-party |`;
|
|
});
|
|
return [
|
|
'| id | role | tier | engines.gsd | extension points | hook kinds | source |',
|
|
'|---|---|---|---|---|---|---|',
|
|
...rows,
|
|
].join('\n');
|
|
}
|
|
|
|
function buildMatrix(registry) {
|
|
const caps = Object.values(registry.capabilities || {});
|
|
const extByCap = extensionsByCapability(registry);
|
|
const featureTable = renderTable(caps, 'feature', extByCap);
|
|
const runtimeTable = renderTable(caps, 'runtime', extByCap);
|
|
const featureCount = caps.filter((c) => c.role === 'feature').length;
|
|
const runtimeCount = caps.filter((c) => c.role === 'runtime').length;
|
|
|
|
return `# Capability matrix reference
|
|
|
|
> **Generated file — do not edit by hand.**
|
|
> This matrix is generated from the capability registry by
|
|
> \`scripts/gen-capability-matrix.cjs\` and kept honest by a drift guard
|
|
> (\`tests/capability-matrix-sync.test.cjs\` runs \`--check\`). Any manual edit is
|
|
> overwritten on the next generation run. To change a capability's declared
|
|
> metadata, edit the corresponding \`capabilities/<id>/capability.json\` and run
|
|
> \`node scripts/gen-capability-matrix.cjs --write\`.
|
|
|
|
See also: [ADR-1244](../adr/1244-capability-ecosystem.md) —
|
|
[Capability manifest fields](#manifest-field-reference) —
|
|
[The capability trust model](../explanation/capability-trust-model.md)
|
|
|
|
---
|
|
|
|
## Column definitions
|
|
|
|
| Column | Description |
|
|
|---|---|
|
|
| **id** | Canonical capability identifier; unique across first- and third-party capabilities. Reserved prefixes: \`gsd-\`, \`gsd-core-\`, \`anthropic-\`. |
|
|
| **role** | \`feature\` — extends what the loop does; \`runtime\` — adapts GSD to a specific AI runtime/IDE. |
|
|
| **tier** | \`core\` — always active; \`standard\` — active when the runtime supports it; \`full\` — opt-in or runtime-specific. |
|
|
| **engines.gsd** | Semver RANGE expressing host-version compatibility. A hard gate at install and at load. \`—\` means the capability declares no range. |
|
|
| **extension points** | The loop points this capability registers hooks into (from the registry's \`byLoopPoint\` index). \`—\` means it registers none (typical for runtime capabilities, whose job is surface emission). |
|
|
| **hook kinds** | Which of \`step\`, \`contribution\`, \`gate\` the capability's hooks use. \`—\` means none. |
|
|
| **source** | \`first-party\` — ships with GSD Core; \`third-party\` — installed from an external source via \`gsd capability install\`. |
|
|
|
|
> **On versions.** This matrix intentionally omits a per-capability \`version\`
|
|
> column. First-party capabilities are versioned **in lockstep** with the GSD
|
|
> Core package (their \`capability.json\` \`version\` always equals the GSD release
|
|
> version), so a per-row version would simply repeat the package version and
|
|
> churn the committed file on every release. The stable host-compatibility
|
|
> signal — \`engines.gsd\` — is shown instead. A third-party capability's exact
|
|
> version is recorded in the per-runtime ledger (\`.gsd-capabilities.json\`) at
|
|
> install time.
|
|
|
|
---
|
|
|
|
## Native (first-party) capabilities
|
|
|
|
First-party capabilities are implicitly trusted: they ship as part of the GSD
|
|
Core package and are stamped with the package version at release (per
|
|
ADR-1244 D6). They are not subject to the consent or integrity-pin flow applied
|
|
to third-party capabilities.
|
|
|
|
### Feature capabilities (role: feature) — ${featureCount}
|
|
|
|
Feature capabilities extend what the loop does — contributing research,
|
|
planning, execution, verification, or ship artefacts at the loop extension
|
|
points.
|
|
|
|
${featureTable}
|
|
|
|
### Runtime capabilities (role: runtime) — ${runtimeCount}
|
|
|
|
Runtime capabilities adapt GSD to a specific AI runtime or IDE — emitting
|
|
skills, agents, hooks configuration, and surface files for that host. They
|
|
typically register no loop hooks (their primary responsibility is surface
|
|
emission), so their extension-point and hook-kind cells are \`—\`.
|
|
|
|
${runtimeTable}
|
|
|
|
---
|
|
|
|
## Third-party capabilities
|
|
|
|
This matrix is the **first-party catalogue**: it is generated from the committed
|
|
registry and therefore lists only the capabilities that ship with GSD Core.
|
|
Installed third-party capabilities are NOT written into this committed file. Once a
|
|
user installs one via \`gsd capability install <spec>\` it enters the **runtime
|
|
registry overlay** (ADR-1244 D2); the overlay-aware view of what is installed on a
|
|
given machine is \`gsd capability list\` (see the
|
|
[\`gsd capability\` command reference](gsd-capability-command.md)), which reports
|
|
first-party and installed third-party capabilities together using the same column
|
|
fields described below, with \`source\` = \`third-party\`.
|
|
|
|
### Column values for third-party rows
|
|
|
|
| Column | Value |
|
|
|---|---|
|
|
| **id** | As declared in \`capability.json\`. Must not use reserved prefixes (\`gsd-\`, \`gsd-core-\`, \`anthropic-\`). |
|
|
| **role** | \`feature\` or \`runtime\`, as declared. |
|
|
| **tier** | \`core\`, \`standard\`, or \`full\`, as declared. |
|
|
| **engines.gsd** | Range from \`capability.json\`; verified at install and at each load. |
|
|
| **extension points** | The loop points the capability registers into, validated against the known 12 identifiers. |
|
|
| **hook kinds** | \`step\`, \`contribution\`, and/or \`gate\` as declared. Disclosed in the consent summary at install. |
|
|
| **source** | \`third-party\` |
|
|
|
|
### Community registry
|
|
|
|
Whether GSD operates or advertises a central community registry of third-party
|
|
capabilities is **TBD/TBA** (PRD). The matrix mechanic and all manifest fields
|
|
ship regardless of that decision; URL/git/npm/tarball import does not depend on
|
|
a central registry.
|
|
|
|
---
|
|
|
|
## Manifest field reference
|
|
|
|
The fields below are defined in \`capability.json\` and govern how a capability
|
|
appears in this matrix. For the full schema, see
|
|
[ADR-1244 D1](../adr/1244-capability-ecosystem.md#d1--versioned-capability-manifest)
|
|
and the [capability manifest reference](capability-manifest.md).
|
|
|
|
| Field | Required | Type | Purpose |
|
|
|---|---|---|---|
|
|
| \`version\` | **Yes** | semver string | Capability version. The registry rejects manifests without it. |
|
|
| \`engines.gsd\` | Recommended | semver range | Host-version compatibility gate. Enforced at install and load. |
|
|
| \`compatVersions\` | No | object: cap-version → gsd-range | Graceful-downgrade table for sources that enumerate versions (git tags, registry, npm). |
|
|
| \`integrity\` | No | \`sha512-<base64>\` | SHA-512 digest of the fetched bundle. Verified before extraction when present; mismatch aborts. |
|
|
| \`provenance\` | No | \`{ sourceRepo, commit }\` | Source provenance; populated in CI for first-party/curated capabilities. |
|
|
|
|
---
|
|
|
|
## Related documents
|
|
|
|
- [ADR-1244 — Capability Ecosystem](../adr/1244-capability-ecosystem.md)
|
|
- [The capability trust model](../explanation/capability-trust-model.md) — why the trust rules are structured as they are
|
|
- [The phase loop](../explanation/the-phase-loop.md) — the 12 loop extension points in context
|
|
- [Capability manifest reference](capability-manifest.md) — the full \`capability.json\` schema
|
|
- [ADR-857](../adr/857-capability-system.md) — the original capability architecture (D7/D8 extended by ADR-1244)
|
|
`;
|
|
}
|
|
|
|
function loadRegistry() {
|
|
delete require.cache[require.resolve(REGISTRY_PATH)];
|
|
return require(REGISTRY_PATH);
|
|
}
|
|
|
|
/** Normalize CRLF→LF + ensure a single trailing newline, for cross-platform compare. */
|
|
function normalize(s) {
|
|
return s.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
}
|
|
|
|
function main() {
|
|
const flag = process.argv[2];
|
|
const registry = loadRegistry();
|
|
const content = buildMatrix(registry);
|
|
|
|
if (flag === '--check') {
|
|
let committed;
|
|
try {
|
|
committed = fs.readFileSync(MATRIX_PATH, 'utf8');
|
|
} catch {
|
|
throw new ExitError(1, `${path.relative(ROOT, MATRIX_PATH)} is missing. Run:\n node scripts/gen-capability-matrix.cjs --write`);
|
|
}
|
|
if (normalize(committed) !== normalize(content)) {
|
|
throw new ExitError(1, `${path.relative(ROOT, MATRIX_PATH)} is stale. Run:\n node scripts/gen-capability-matrix.cjs --write`);
|
|
}
|
|
console.log(`${path.relative(ROOT, MATRIX_PATH)} is up to date.`);
|
|
return;
|
|
}
|
|
if (flag === '--write') {
|
|
fs.mkdirSync(path.dirname(MATRIX_PATH), { recursive: true });
|
|
fs.writeFileSync(MATRIX_PATH, content, 'utf8');
|
|
console.log(`Wrote ${path.relative(ROOT, MATRIX_PATH)}`);
|
|
return;
|
|
}
|
|
process.stdout.write(content);
|
|
}
|
|
|
|
if (require.main === module) runMain(main);
|
|
|
|
module.exports = { buildMatrix, extensionsByCapability };
|