feat(catalog): gate catalog README labels against ref + close the drift
The catalog README's per-plugin `vX.Y.Z` labels were an UNGUARDED surface: check-versions validated each plugin's OWN README badge, never the catalog README's labels, so they drifted (config-audit shown v5.5.0 while pinned to v5.7.0; voyage v5.1.1 while pinned to v5.6.0) — the doc misstated what `claude plugin install` actually resolves. Close the class, same pattern as the ref surface: - check-versions.mjs: new ERROR rule "catalog README label == catalog ref" via pure extractCatalogLabel() (matches the /open/<name>) heading, takes the first `vX.Y.Z`, ignores a trailing lang/flag badge). No legitimate transient state lets label != ref, so ERROR (not WARN). +5 tests. - release-plugin.mjs: on --write, bump the README label atomically with the ref via pure reconcileReadmeLabel() and git add README.md on --commit, so future releases keep label and ref in lock-step. +4 tests. - README.md: reconcile the 2 stale labels (config-audit -> v5.7.0, voyage -> v5.6.0). Gate now 9 OK / 1 WARN / 0 ERROR. - CLAUDE.md: doctrine updated to document the new gate rule + atomic label bump. ms-ai-architect stays WARN by decision (1.16.0 is unreleased WIP, never tagged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cm2RxKbomdLqjiWGcwCCPi
This commit is contained in:
parent
cf2420f7c5
commit
2e1d206ab3
6 changed files with 155 additions and 18 deletions
|
|
@ -5,6 +5,7 @@
|
|||
// against the plugin repo found as a sibling directory (../<name>):
|
||||
// - catalog ref must point at an existing git tag → else ERROR (install-breaking)
|
||||
// - plugin.json version must equal the README version-badge → else ERROR (internal)
|
||||
// - catalog README per-plugin label must equal catalog ref → else ERROR (doc misstates install)
|
||||
// - catalog ref should equal plugin.json version → else WARN (catalog lags / unreleased bump)
|
||||
// - sibling repo missing → SKIP
|
||||
//
|
||||
|
|
@ -24,9 +25,23 @@ export function extractBadgeVersion(readmeText) {
|
|||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Read the catalog README's per-plugin version label, e.g.
|
||||
// ### [Config-Audit](https://.../open/config-audit) `v5.7.0`
|
||||
// Matches the heading line by its `/open/<name>)` link and returns the FIRST `vX.Y.Z`
|
||||
// token on it (so a trailing `🇳🇴 Norwegian`-style badge is ignored). null if no entry.
|
||||
export function extractCatalogLabel(readmeText, name) {
|
||||
for (const line of String(readmeText || '').split('\n')) {
|
||||
if (line.includes(`/open/${name})`)) {
|
||||
const m = /`v(\d+\.\d+\.\d+)`/.exec(line);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pure classifier — all I/O is resolved into the input shape before this is called.
|
||||
// tags === null means "repo not inspected" (missing locally); [] means "no tags".
|
||||
export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, tags }) {
|
||||
export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, tags, catalogLabel = null }) {
|
||||
if (pluginVersion === null && tags === null) {
|
||||
return { name, status: 'SKIP', findings: [{ level: 'SKIP', msg: 'plugin repo not found locally — cannot verify' }] };
|
||||
}
|
||||
|
|
@ -47,7 +62,13 @@ export function classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, t
|
|||
findings.push({ level: 'ERROR', msg: `plugin.json version ${pluginVersion} != README version-badge ${readmeBadge}` });
|
||||
}
|
||||
|
||||
// 3. catalog ref vs plugin.json version
|
||||
// 3. catalog README label vs catalog ref — the human-facing doc must match what installs.
|
||||
// Unlike ref-vs-plugin.json, there is no legitimate transient state where these differ.
|
||||
if (catalogLabel !== null && normalizeVersion(catalogRef) !== catalogLabel) {
|
||||
findings.push({ level: 'ERROR', msg: `catalog README label v${catalogLabel} != catalog ref ${catalogRef} (README misstates the installed version)` });
|
||||
}
|
||||
|
||||
// 4. catalog ref vs plugin.json version
|
||||
if (pluginVersion !== null && normalizeVersion(catalogRef) !== pluginVersion) {
|
||||
const releasedTag = tags !== null && tags.includes('v' + pluginVersion);
|
||||
const reason = releasedTag
|
||||
|
|
@ -93,7 +114,12 @@ export function inspectPlugin(catalogDir, plugin) {
|
|||
readmeBadge = extractBadgeVersion(readFileSync(join(repoDir, 'README.md'), 'utf8'));
|
||||
} catch { /* no README → badge check skipped */ }
|
||||
|
||||
return classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, tags: gitTags(repoDir) });
|
||||
let catalogLabel = null;
|
||||
try {
|
||||
catalogLabel = extractCatalogLabel(readFileSync(join(catalogDir, 'README.md'), 'utf8'), name);
|
||||
} catch { /* no catalog README → label check skipped */ }
|
||||
|
||||
return classifyPlugin({ name, catalogRef, pluginVersion, readmeBadge, tags: gitTags(repoDir), catalogLabel });
|
||||
}
|
||||
|
||||
export function runGate(catalogDir, { strict = false } = {}) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue