* 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)
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
|
||||
* (and, once PR2 ships docs/registries/eos.json, docs/registries/eos-registry.md)
|
||||
* from the corresponding source JSON, via registry-schema.cjs#renderMarkdown.
|
||||
* Issue #2182.
|
||||
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md,
|
||||
* docs/registries/eos-registry.md, and docs/registries/reviewer-registry.md
|
||||
* from their corresponding source JSON, via registry-schema.cjs#renderMarkdown.
|
||||
* Issue #2182 (capability/eos); issue #2904 (reviewer).
|
||||
*
|
||||
* eos.json and reviewers.json are both OPTIONAL sources (`SOURCES[].optional`)
|
||||
* — an absent one is skipped silently. capabilities.json is the primary
|
||||
* source and is never optional.
|
||||
*
|
||||
* NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
|
||||
* generates the RUNTIME capability manifest consumed by the host at runtime
|
||||
@@ -34,7 +38,8 @@ const { renderMarkdown } = require('./registry-schema.cjs');
|
||||
|
||||
const SOURCES = [
|
||||
{ type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
|
||||
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
|
||||
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md', optional: true },
|
||||
{ type: 'reviewer', jsonFile: 'reviewers.json', mdFile: 'reviewer-registry.md', optional: true },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -57,14 +62,16 @@ function getRegistriesDir() {
|
||||
* Render the markdown for a single registry type from its committed source
|
||||
* JSON.
|
||||
*
|
||||
* Only `eos.json` is optional (pre-PR2, before that source JSON ships) —
|
||||
* an absent `eos.json` returns null and callers treat that as "nothing to
|
||||
* do". `capabilities.json` is the primary registry source: a missing
|
||||
* `capabilities.json` is ALWAYS an error (never a silent "up to date"
|
||||
* pass), mirroring the type distinction in `scripts/validate-registry.cjs`
|
||||
* (`type === 'eos' && !exists → continue`).
|
||||
* Optionality is a per-source data flag (`SOURCES[].optional`), not a
|
||||
* hardcoded type literal: `eos.json` (pre-PR2) and `reviewers.json` (issue
|
||||
* #2904) are both optional — an absent source JSON returns null and callers
|
||||
* treat that as "nothing to do". `capabilities.json` is still the primary
|
||||
* registry source and is never optional: a missing `capabilities.json` is
|
||||
* ALWAYS an error (never a silent "up to date" pass), mirroring the same
|
||||
* `optional` flag in `scripts/validate-registry.cjs`
|
||||
* (`optional && !exists → continue`).
|
||||
*
|
||||
* @param {'capability'|'eos'} type
|
||||
* @param {'capability'|'eos'|'reviewer'} type
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function renderFor(type) {
|
||||
@@ -73,14 +80,31 @@ function renderFor(type) {
|
||||
|
||||
const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
|
||||
if (!fs.existsSync(jsonPath)) {
|
||||
if (type === 'eos') return null;
|
||||
if (source.optional) return null;
|
||||
throw new ExitError(
|
||||
1,
|
||||
`${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
|
||||
);
|
||||
}
|
||||
|
||||
const entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
// A malformed source JSON must surface as an actionable CLI error, not an
|
||||
// unhandled SyntaxError with a raw Node stack trace — mirrors
|
||||
// scripts/validate-registry.cjs#validateFile's try/catch around JSON.parse.
|
||||
let entries;
|
||||
try {
|
||||
entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
} catch (err) {
|
||||
throw new ExitError(1, `${source.jsonFile} is not valid JSON at ${jsonPath}: ${err.message}`);
|
||||
}
|
||||
|
||||
// Mirrors validate-registry.cjs's explicit non-array rejection: a source
|
||||
// JSON that parses to a non-array (object, string, etc.) would otherwise
|
||||
// throw an opaque TypeError from `[...entries].sort()` in renderMarkdown,
|
||||
// or silently mis-render for an iterable-but-wrong-shape value like a string.
|
||||
if (!Array.isArray(entries)) {
|
||||
throw new ExitError(1, `${source.jsonFile} must be a JSON array of entries`);
|
||||
}
|
||||
|
||||
return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
|
||||
}
|
||||
|
||||
@@ -91,7 +115,7 @@ function main() {
|
||||
|
||||
for (const { type, mdFile } of SOURCES) {
|
||||
const rendered = renderFor(type);
|
||||
if (rendered === null) continue; // source JSON absent (eos.json before PR2)
|
||||
if (rendered === null) continue; // source JSON absent and optional (eos.json before PR2 / reviewers.json)
|
||||
|
||||
const mdPath = path.join(registriesDir, mdFile);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user