Files
msd-core/tests/registry-reviewer-parity.test.cjs
Tom Boucher 90771ddf02 enh(#2904): add a reviewer entry type so third-party reviewer lanes are discoverable (#2912)
* feat(#2904): add a `reviewer` entry type so third-party reviewer lanes are discoverable

ADR-2782 made a reviewer lane installable by a third party, but neither
discoverability catalog could hold one. The Community Capability Registry
requires a non-empty `loopExtensionPoints` and forbids a lane from declaring
any hook kind, so a `role: "reviewer"` entry is unsatisfiable by construction;
the EoS Registry is for ADR-1239 host integrations, which a lane is not.

Adds a third catalog — `docs/registries/reviewers.json` →
`docs/registries/reviewer-registry.md` — whose `interactions` describes the
lane: slug, flags, transport, evidenceClass, reviewsSection, requiresBinaries,
configKeys, runtimeCompat.

The lane vocabulary is a hand-written mirror of `capability-validator.cjs`
(the same pattern as `AXES` mirroring `HOST_INTEGRATION_AXES`), with parity
enforced by tests/registry-reviewer-parity.test.cjs. `slug` deliberately uses
the runtime `LANE_SLUG_RE` grammar rather than the registry's kebab-only `id`
rule, so real lanes (`lm_studio`, `4o-mini`) are not rejected.

Two binary type branches became three-way Map dispatch. Both now fail loudly
on an unrecognized type instead of silently treating it as a capability —
`renderMarkdown` in particular writes a committed catalog file, so a silent
wrong-title render was the worst failure mode available.

Also fixed while here: `gen-registry.cjs` parsed source JSON with no error
handling, so a malformed or non-array `capabilities.json` surfaced as a raw
SyntaxError/TypeError instead of an actionable CLI error.

Closes #2904

* fix(#2904): bound and sanitize untrusted registry `interactions` strings

Review findings from the pre-PR passes.

Security (isolated pass): `interactions` string fields reached the generated,
committed Markdown catalog with no control-character check and no length
bound. A `reviewsSection` carrying ESC and a `requiresBinaries` element
carrying NUL plus 5000 characters validated clean and landed verbatim in the
rendered page — `mdInline` escapes Markdown metacharacters and collapses CRLF,
but nothing else. The identical gap already existed on the capability type's
`configKeys`/`requires`/`runtimeCompat`/`produces`/`consumes`, so it is fixed
there too rather than inherited into a third type.

`hasDisallowedControlChar` is lifted to module scope so exactly one
implementation exists, and a shared `validateStringArrayField` enforces
control-character rejection, a 200-character element cap and a 50-element
array cap for both types.

Correctness (standards pass): `renderMarkdown`'s per-entry summary builder was
still an if/else-if chain whose final `else` was the capability branch — the
one per-type dispatch point this change had not converted, and the same silent
fallthrough it removes elsewhere. It now lives in `RENDER_META` alongside the
title, so a fourth type cannot silently inherit capability's rendering. All
three types' rendered output is byte-identical to before the refactor.

Also corrects a test comment that still claimed the reviewer suites were
failing-first against an unmodified module.

* chore(#2904): backfill changeset PR number (#2912)
2026-07-31 08:11:57 -04:00

154 lines
6.9 KiB
JavaScript

'use strict';
process.env.GSD_TEST_MODE = '1';
/**
* tests/registry-reviewer-parity.test.cjs — regression coverage for issue
* #2904 ("reviewer" registry entry type).
*
* `scripts/registry-schema.cjs`'s reviewer vocabulary
* (`REVIEWER_LANE_TRANSPORTS`, `REVIEWER_EVIDENCE_CLASSES`,
* `REVIEWER_SLUG_RE`, `REVIEWER_FLAG_RE`) and
* `gsd-core/bin/lib/capability-validator.cjs`'s runtime reviewer-lane
* vocabulary (`VALID_LANE_TRANSPORTS`, `VALID_EVIDENCE_CLASSES`,
* `LANE_SLUG_RE`, `LANE_FLAG_RE`) are two independent, hand-written mirrors
* of the same underlying grammar — the registry is a third-party
* DISCOVERABILITY catalog (documentation-scoped), the capability-validator
* is the RUNTIME manifest validator that actually gates what a shipped
* `capabilities/<id>/capability.json`'s `reviewer` body may declare. Nothing
* imports one from the other (capability-validator.cjs's own header comment,
* `gsd-core/bin/lib/capability-validator.cjs:798-810`, explains why: the
* canonical descriptor `LANE_SLUG_RE` mirrors lives in
* `src/review-lane-descriptor.cts`, which compiles to gitignored build
* output that this committed plain `.cjs` cannot depend on before
* `npm run build:lib` has ever run) — so the two vocabularies can silently
* drift apart with no error to read: a registry entry that faithfully
* mirrors a real shipped lane (e.g. `lm_studio`, `llama_cpp`, `4o-mini` —
* all real slugs that a naive kebab-only grammar would reject) would look
* "strict but simply wrong" if the registry's copy of the slug grammar ever
* diverged from `LANE_SLUG_RE`.
*
* `capability-validator.cjs:807-810` states the byte-identical requirement
* explicitly: "A LEADING DIGIT IS PERMITTED. ... Keep the two grammars
* byte-identical." This file is that parity guard for the reviewer registry
* entry type, sibling in structure/intent to
* `tests/registry-axes-parity.test.cjs` (which pins `AXES`/`OPTIONAL_AXES`
* against `HOST_INTEGRATION_AXES`).
*
* Row 75 additionally reads every real, shipped `capabilities/<id>/capability.json`
* and asserts each one's `reviewer.slug` (where present) validates against
* `REVIEWER_SLUG_RE` — a reality check that the registry grammar isn't just
* parity-pinned against `capability-validator.cjs` in the abstract, but
* actually accepts every lane slug the repository ships today. This reads
* JSON DATA files (not source), so it does not trip `local/no-source-grep`
* and is not a source-grep-in-disguise — no `.cjs`/`.js`/`.ts` source file is
* ever `readFileSync`'d and string-matched in this file.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const {
REVIEWER_LANE_TRANSPORTS,
REVIEWER_EVIDENCE_CLASSES,
REVIEWER_SLUG_RE,
REVIEWER_FLAG_RE,
} = require(path.join(__dirname, '..', 'scripts', 'registry-schema.cjs'));
const {
VALID_LANE_TRANSPORTS,
VALID_EVIDENCE_CLASSES,
LANE_SLUG_RE,
LANE_FLAG_RE,
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'capability-validator.cjs'));
// ─── Parity: transport / evidence-class vocabularies ───────────────────────
describe('registry-reviewer-parity: vocab set-equality vs capability-validator.cjs', () => {
test('registry transport vocab matches capability-validator', () => {
const registrySet = new Set(REVIEWER_LANE_TRANSPORTS);
assert.ok(registrySet.size > 0, 'expected REVIEWER_LANE_TRANSPORTS to be non-empty');
assert.ok(VALID_LANE_TRANSPORTS.size > 0, 'expected VALID_LANE_TRANSPORTS to be non-empty');
assert.deepEqual(
[...registrySet].sort(),
[...VALID_LANE_TRANSPORTS].sort(),
'REVIEWER_LANE_TRANSPORTS must be set-equal to capability-validator.cjs VALID_LANE_TRANSPORTS',
);
});
test('registry evidence-class vocab matches capability-validator', () => {
const registrySet = new Set(REVIEWER_EVIDENCE_CLASSES);
assert.ok(registrySet.size > 0, 'expected REVIEWER_EVIDENCE_CLASSES to be non-empty');
assert.ok(VALID_EVIDENCE_CLASSES.size > 0, 'expected VALID_EVIDENCE_CLASSES to be non-empty');
assert.deepEqual(
[...registrySet].sort(),
[...VALID_EVIDENCE_CLASSES].sort(),
'REVIEWER_EVIDENCE_CLASSES must be set-equal to capability-validator.cjs VALID_EVIDENCE_CLASSES',
);
});
});
// ─── Parity: slug / flag grammar — byte-identical regexes ──────────────────
describe('registry-reviewer-parity: grammar regexes are byte-identical to capability-validator.cjs', () => {
test('registry slug grammar is byte-identical to LANE_SLUG_RE', () => {
assert.equal(
REVIEWER_SLUG_RE.source,
LANE_SLUG_RE.source,
'REVIEWER_SLUG_RE.source must equal LANE_SLUG_RE.source — "keep the two grammars byte-identical" (capability-validator.cjs:807-810)',
);
assert.equal(
REVIEWER_SLUG_RE.flags,
LANE_SLUG_RE.flags,
'REVIEWER_SLUG_RE.flags must equal LANE_SLUG_RE.flags',
);
});
test('registry flag grammar is byte-identical to LANE_FLAG_RE', () => {
assert.equal(
REVIEWER_FLAG_RE.source,
LANE_FLAG_RE.source,
'REVIEWER_FLAG_RE.source must equal LANE_FLAG_RE.source',
);
assert.equal(
REVIEWER_FLAG_RE.flags,
LANE_FLAG_RE.flags,
'REVIEWER_FLAG_RE.flags must equal LANE_FLAG_RE.flags',
);
});
});
// ─── Reality check: every shipped first-party lane slug validates ─────────
describe('registry-reviewer-parity: every first-party lane slug is accepted by the registry schema', () => {
test('every first-party lane slug is accepted by the registry schema', () => {
const capabilitiesDir = path.join(__dirname, '..', 'capabilities');
const capabilityDirs = fs.readdirSync(capabilitiesDir, { withFileTypes: true }).filter((d) => d.isDirectory());
const collectedSlugs = [];
for (const dirent of capabilityDirs) {
const capabilityJsonPath = path.join(capabilitiesDir, dirent.name, 'capability.json');
if (!fs.existsSync(capabilityJsonPath)) continue;
const data = JSON.parse(fs.readFileSync(capabilityJsonPath, 'utf8'));
if (data && typeof data === 'object' && data.reviewer && typeof data.reviewer.slug === 'string') {
collectedSlugs.push({ id: dirent.name, slug: data.reviewer.slug });
}
}
// Sanity: the collected set must be non-empty, or the loop below would
// pass vacuously — a glob that silently matched nothing must fail loudly.
assert.ok(
collectedSlugs.length > 0,
'expected at least one capabilities/*/capability.json with a reviewer.slug — found none',
);
for (const { id, slug } of collectedSlugs) {
assert.ok(
REVIEWER_SLUG_RE.test(slug),
`expected capabilities/${id}/capability.json reviewer.slug "${slug}" to match REVIEWER_SLUG_RE (${REVIEWER_SLUG_RE})`,
);
}
});
});