mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
a53fc42b86
js-yaml 5 removed the ESM default export, so the website-catalog scripts failed Catalog Drift Check with a SyntaxError on import.
108 lines
4.0 KiB
JavaScript
108 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Sencho tier-reconciliation: canonical catalog validator
|
|
* Verifies docs/feature-catalog.yaml against the canonical schema and
|
|
* the required cross-field invariant (tier: internal iff availability: internal).
|
|
* Exits 0 on valid, exits 1 with diagnostic lines on invalid.
|
|
*/
|
|
import * as yaml from 'js-yaml';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { fileURLToPath } from 'url';
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const CATALOG_FILE = path.resolve(__dirname, '../../docs/feature-catalog.yaml');
|
|
|
|
const VALID_TIERS = new Set(['community', 'admiral', 'internal']);
|
|
const VALID_AVAILABILITY = new Set(['shipped', 'planned', 'internal']);
|
|
const VALID_CATEGORIES = new Set([
|
|
'compose-deploy', 'fleet-orchestration', 'security-foundation',
|
|
'automation-operations', 'recovery', 'identity-access',
|
|
'governance', 'assurance', 'internal',
|
|
]);
|
|
|
|
const errors = [];
|
|
|
|
function error(msg) { errors.push('ERROR: ' + msg); }
|
|
|
|
function readCatalog() {
|
|
const text = fs.readFileSync(CATALOG_FILE, 'utf8');
|
|
const doc = yaml.load(text, { schema: yaml.CORE_SCHEMA });
|
|
if (!doc || typeof doc !== 'object') {
|
|
throw new Error('catalog is not a YAML mapping');
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
function main() {
|
|
const doc = readCatalog();
|
|
if (!Array.isArray(doc.entries)) {
|
|
console.error('FAIL: catalog has no entries array.');
|
|
process.exit(1);
|
|
}
|
|
const entries = doc.entries;
|
|
const ids = new Set();
|
|
|
|
for (const [i, entry] of entries.entries()) {
|
|
const prefix = `entry[${i}].id=${entry?.id ?? '(missing)'}`;
|
|
|
|
if (!entry || typeof entry !== 'object') {
|
|
error(`${prefix}: entry is not an object`);
|
|
continue;
|
|
}
|
|
|
|
if (!entry.id) error(`${prefix}: missing id`);
|
|
else if (ids.has(entry.id)) error(`${prefix}: duplicate id "${entry.id}"`);
|
|
else ids.add(entry.id);
|
|
|
|
if (!entry.name) error(`${prefix}: missing name`);
|
|
if (!VALID_TIERS.has(entry.tier))
|
|
error(`${prefix}: invalid tier "${entry.tier}"; must be one of community/admiral/internal`);
|
|
if (!VALID_AVAILABILITY.has(entry.availability))
|
|
error(`${prefix}: invalid availability "${entry.availability}"; must be one of shipped/planned/internal`);
|
|
|
|
// Cross-field invariant: tier: internal iff availability: internal
|
|
if (entry.tier === 'internal' && entry.availability !== 'internal')
|
|
error(`${prefix}: tier: internal requires availability: internal`);
|
|
if (entry.tier !== 'internal' && entry.availability === 'internal')
|
|
error(`${prefix}: non-internal tier requires non-internal availability`);
|
|
|
|
if (entry.availability === 'planned') {
|
|
if (!entry.publicRoadmapKey) error(`${prefix}: planned entry must have publicRoadmapKey`);
|
|
}
|
|
|
|
if (!VALID_CATEGORIES.has(entry.category))
|
|
error(`${prefix}: unknown category "${entry.category}"`);
|
|
|
|
// Internal-only fields must not leak into committed catalog.
|
|
// The canonical file IS public, so we enforce: no linear, no evidence with internal identifiers,
|
|
// no internalNote. Public-name fields only.
|
|
if (entry.linear) {
|
|
// Reject any internal Linear identifier in committed file.
|
|
if (/SEN-[0-9]/.test(String(entry.linear)))
|
|
error(`${prefix}: committed catalog contains internal Linear identifier in linear field ("${entry.linear}"); use publicRoadmapKey instead`);
|
|
}
|
|
// No evidence field allowed in canonical committed file (evidence stays internal).
|
|
if (entry.evidence)
|
|
error(`${prefix}: evidence field must not appear in committed canonical catalog (use Linear/non-public record for evidence); got: ${entry.evidence}`);
|
|
if (entry.internalNote)
|
|
error(`${prefix}: internalNote field must not appear in committed canonical catalog`);
|
|
}
|
|
|
|
if (errors.length === 0) {
|
|
console.log(`VALID: catalog has ${entries.length} entries; all invariants pass.`);
|
|
process.exit(0);
|
|
} else {
|
|
for (const msg of errors) console.error(msg);
|
|
console.error(`FAIL: ${errors.length} error(s) found.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (e) {
|
|
console.error('FAIL: ' + (e.message || e));
|
|
process.exit(1);
|
|
}
|