mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Option A — ghost-entry guard (INVENTORY ⊆ actual): tests/inventory-source-parity.test.cjs parses every declared row in INVENTORY.md and asserts the source file exists. Catches deletions and renames that leave ghost entries behind. Option B — auto-generated structural manifest: scripts/gen-inventory-manifest.cjs walks all six family dirs and emits docs/INVENTORY-MANIFEST.json. tests/inventory-manifest-sync.test.cjs fails CI when a new surface ships without a manifest update, surfacing exactly which entries are missing. Option C — schema-driven config validation + docs parity: get-shit-done/bin/lib/config-schema.cjs extracted from config.cjs as the single source of truth for VALID_CONFIG_KEYS and dynamic patterns. config.cjs now imports from it. tests/config-schema-docs-parity.test.cjs asserts every exact-match key appears in docs/CONFIGURATION.md, surfacing 14 previously undocumented keys (planning.sub_repos, workflow.ai_integration_phase, git.base_branch, learnings.max_inject, and 10 others) — all now documented in their appropriate sections. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
55 lines
2.6 KiB
JavaScript
55 lines
2.6 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Asserts docs/INVENTORY-MANIFEST.json is in sync with the filesystem.
|
|
* A stale manifest means a surface shipped without updating INVENTORY.md.
|
|
* Fix by running: node scripts/gen-inventory-manifest.cjs --write
|
|
* then adding the corresponding row(s) in docs/INVENTORY.md.
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
|
|
|
|
const FAMILIES = [
|
|
{ name: 'agents', dir: path.join(ROOT, 'agents'), filter: (f) => /^gsd-.*\.md$/.test(f), toName: (f) => f.replace(/\.md$/, '') },
|
|
{ name: 'commands', dir: path.join(ROOT, 'commands', 'gsd'), filter: (f) => f.endsWith('.md'), toName: (f) => '/gsd-' + f.replace(/\.md$/, '') },
|
|
{ name: 'workflows', dir: path.join(ROOT, 'get-shit-done', 'workflows'), filter: (f) => f.endsWith('.md'), toName: (f) => f },
|
|
{ name: 'references', dir: path.join(ROOT, 'get-shit-done', 'references'), filter: (f) => f.endsWith('.md'), toName: (f) => f },
|
|
{ name: 'cli_modules', dir: path.join(ROOT, 'get-shit-done', 'bin', 'lib'), filter: (f) => f.endsWith('.cjs'), toName: (f) => f },
|
|
{ name: 'hooks', dir: path.join(ROOT, 'hooks'), filter: (f) => /\.(js|sh)$/.test(f), toName: (f) => f },
|
|
];
|
|
|
|
test('docs/INVENTORY-MANIFEST.json matches the filesystem', () => {
|
|
const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
const additions = [];
|
|
const removals = [];
|
|
|
|
for (const { name, dir, filter, toName } of FAMILIES) {
|
|
const live = new Set(
|
|
fs.readdirSync(dir)
|
|
.filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
|
|
.map(toName),
|
|
);
|
|
const recorded = new Set((committed.families || {})[name] || []);
|
|
|
|
for (const entry of live) {
|
|
if (!recorded.has(entry)) additions.push(name + '/' + entry);
|
|
}
|
|
for (const entry of recorded) {
|
|
if (!live.has(entry)) removals.push(name + '/' + entry);
|
|
}
|
|
}
|
|
|
|
const msg = [
|
|
additions.length ? 'New surfaces not in manifest (run node scripts/gen-inventory-manifest.cjs --write):\n' + additions.map((e) => ' + ' + e).join('\n') : '',
|
|
removals.length ? 'Manifest entries with no matching file:\n' + removals.map((e) => ' - ' + e).join('\n') : '',
|
|
].filter(Boolean).join('\n');
|
|
|
|
assert.ok(additions.length === 0 && removals.length === 0, msg);
|
|
});
|