Files
get-shit-done/tests/inventory-source-parity.test.cjs
Tom Boucher 62eaa8dd7b docs: close doc drift vectors — bidirectional parity, manifest, schema-driven config (#2479)
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>
2026-04-20 09:39:05 -04:00

133 lines
4.5 KiB
JavaScript

'use strict';
/**
* Reverse-direction parity: every row declared in docs/INVENTORY.md must
* resolve to a real file on the filesystem. Complements the forward tests
* (actual ⊆ INVENTORY) with the reverse direction (INVENTORY ⊆ actual),
* catching ghost entries left behind when artifacts are deleted or renamed.
*/
const { describe, 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 INVENTORY = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
/** Extract the text of a named top-level section (## Header ... next ##). */
function section(header) {
const start = INVENTORY.indexOf('## ' + header);
if (start === -1) return '';
const next = INVENTORY.indexOf('\n## ', start + 1);
return next === -1 ? INVENTORY.slice(start) : INVENTORY.slice(start, next);
}
/** Extract backtick-quoted filenames from column-1 table cells. */
function backtickNames(text, ext) {
const re = new RegExp('\\|\\s*`([^`]+\\.' + ext + ')`\\s*\\|', 'gm');
const names = [];
let m;
while ((m = re.exec(text)) !== null) names.push(m[1]);
return names;
}
/** Extract agent names from `| gsd-xxx | ...` rows (no backticks). */
function agentNames(text) {
const re = /^\|\s*(gsd-[a-z0-9-]+)\s*\|/gm;
const names = [];
let m;
while ((m = re.exec(text)) !== null) names.push(m[1]);
return names;
}
/** Extract relative source paths from markdown links in Commands section. */
function commandSourcePaths(text) {
const re = /\[commands\/gsd\/[^\]]+\]\(\.\.\/(commands\/gsd\/[^)]+)\)/g;
const paths = [];
let m;
while ((m = re.exec(text)) !== null) paths.push(m[1]);
return paths;
}
describe('INVENTORY.md declared artifacts exist on the filesystem (ghost-entry guard)', () => {
describe('Agents', () => {
const names = agentNames(section('Agents'));
for (const name of names) {
test(name, () => {
const p = path.join(ROOT, 'agents', name + '.md');
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares agent "' + name + '" but agents/' + name + '.md does not exist — remove the ghost row or restore the file',
);
});
}
});
describe('Commands', () => {
const paths = commandSourcePaths(section('Commands'));
for (const rel of paths) {
test(rel, () => {
const p = path.join(ROOT, rel);
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares source "' + rel + '" but the file does not exist — remove the ghost row or restore the file',
);
});
}
});
describe('Workflows', () => {
const names = backtickNames(section('Workflows'), 'md');
for (const name of names) {
test(name, () => {
const p = path.join(ROOT, 'get-shit-done', 'workflows', name);
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares workflow "' + name + '" but get-shit-done/workflows/' + name + ' does not exist — remove the ghost row or restore the file',
);
});
}
});
describe('References', () => {
const names = backtickNames(section('References'), 'md');
for (const name of names) {
test(name, () => {
const p = path.join(ROOT, 'get-shit-done', 'references', name);
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares reference "' + name + '" but get-shit-done/references/' + name + ' does not exist — remove the ghost row or restore the file',
);
});
}
});
describe('CLI Modules', () => {
const names = backtickNames(section('CLI Modules'), 'cjs');
for (const name of names) {
test(name, () => {
const p = path.join(ROOT, 'get-shit-done', 'bin', 'lib', name);
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares CLI module "' + name + '" but get-shit-done/bin/lib/' + name + ' does not exist — remove the ghost row or restore the file',
);
});
}
});
describe('Hooks', () => {
const jsNames = backtickNames(section('Hooks'), 'js');
const shNames = backtickNames(section('Hooks'), 'sh');
for (const name of [...jsNames, ...shNames]) {
test(name, () => {
const p = path.join(ROOT, 'hooks', name);
assert.ok(
fs.existsSync(p),
'INVENTORY.md declares hook "' + name + '" but hooks/' + name + ' does not exist — remove the ghost row or restore the file',
);
});
}
});
});