diff --git a/tests/agents-doc-parity.test.cjs b/tests/agents-doc-parity.test.cjs deleted file mode 100644 index 6b8ede03d..000000000 --- a/tests/agents-doc-parity.test.cjs +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -/** - * For every `agents/gsd-*.md`, assert its agent name appears as a row - * in docs/INVENTORY.md's Agents table. AGENTS.md card presence is NOT - * enforced — that file is allowed to be a curated subset (primary - * cards + advanced stubs). - * - * Related: docs readiness refresh, lane-12 recommendation. - */ - -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 AGENTS_DIR = path.join(ROOT, 'agents'); -const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8'); - -const agentFiles = fs - .readdirSync(AGENTS_DIR) - .filter((f) => /^gsd-.*\.md$/.test(f)); - -function mentionedInInventoryAgents(name) { - // Row form in the Agents table: `| agent-name | role | ... |` - // The Agents table uses the raw name (no code fence) in column 1. - const rowRe = new RegExp(`^\\|\\s*${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*\\|`, 'm'); - return rowRe.test(INVENTORY_MD); -} - -describe('every shipped agent has a row in INVENTORY.md', () => { - for (const file of agentFiles) { - const name = file.replace(/\.md$/, ''); - test(name, () => { - assert.ok( - mentionedInInventoryAgents(name), - `agents/${file} has no row in docs/INVENTORY.md Agents table — add one`, - ); - }); - } -}); diff --git a/tests/bug-2687-config-read-warning-parity.test.cjs b/tests/bug-2687-config-read-warning-parity.test.cjs deleted file mode 100644 index 68ed961c6..000000000 --- a/tests/bug-2687-config-read-warning-parity.test.cjs +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -/** - * Regression test for #2687 — loadConfig must not emit "unknown config key" - * warnings for keys that are registered in DYNAMIC_KEY_PATTERNS (e.g. review, - * model_profile_overrides, claude_md_assembly). These keys were absent from - * the hand-maintained KNOWN_TOP_LEVEL set in core.cjs, causing false-positive - * warnings on every read. - * - * We trigger loadConfig via `resolve-model` (which calls loadConfig internally) - * and assert that stderr is EMPTY on success — a typed-IR equivalent of - * "no warning was emitted" without grepping for specific warning text. The - * absence of any stderr output IS the contract: loadConfig prints nothing - * to stderr when every top-level key in config.json is recognized. - * - * Migrated from substring `.includes('unknown config key')` text matching - * to typed empty-stderr assertions per #2974. - */ - -const { describe, test, afterEach } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const { spawnSync } = require('node:child_process'); -const { createTempProject, cleanup, TOOLS_PATH } = require('./helpers.cjs'); - -const TEST_ENV_BASE = { - GSD_SESSION_KEY: '', - CODEX_THREAD_ID: '', - CLAUDE_SESSION_ID: '', - CLAUDE_CODE_SSE_PORT: '', - OPENCODE_SESSION_ID: '', - GEMINI_SESSION_ID: '', - CURSOR_SESSION_ID: '', - WINDSURF_SESSION_ID: '', - TERM_SESSION_ID: '', - WT_SESSION: '', - TMUX_PANE: '', - ZELLIJ_SESSION_NAME: '', - TTY: '', - SSH_TTY: '', -}; - -/** - * Run gsd-tools and return { stdout, stderr, status }. - * Captures stderr even when the process exits 0 (unlike runGsdTools which only - * surfaces stderr via result.error on non-zero exit). - */ -function runWithStderr(args, cwd) { - const result = spawnSync(process.execPath, [TOOLS_PATH, ...args], { - cwd, - encoding: 'utf-8', - env: { ...process.env, ...TEST_ENV_BASE }, - }); - return { - stdout: result.stdout || '', - stderr: result.stderr || '', - status: result.status, - }; -} - -describe('bug-2687 — no warning for dynamic-pattern containers in loadConfig', () => { - let tmpDir; - - afterEach(() => { - if (tmpDir) cleanup(tmpDir); - tmpDir = null; - }); - - test('review — loadConfig emits no warning when config.json contains review key', () => { - tmpDir = createTempProject('gsd-2687-review-'); - const configPath = path.join(tmpDir, '.planning', 'config.json'); - fs.writeFileSync( - configPath, - JSON.stringify({ review: { models: { 'test-cli': 'test-command' } } }, null, 2), - 'utf-8' - ); - - // resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check - const result = runWithStderr(['resolve-model', 'planner'], tmpDir); - - assert.equal( - result.stderr.trim(), - '', - `loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}` - ); - }); - - test('model_profile_overrides — loadConfig emits no warning when config.json contains model_profile_overrides key', () => { - tmpDir = createTempProject('gsd-2687-mpo-'); - const configPath = path.join(tmpDir, '.planning', 'config.json'); - fs.writeFileSync( - configPath, - JSON.stringify({ model_profile_overrides: { codex: { sonnet: 'claude-sonnet-4' } } }, null, 2), - 'utf-8' - ); - - // resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check - const result = runWithStderr(['resolve-model', 'planner'], tmpDir); - - assert.equal( - result.stderr.trim(), - '', - `loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}` - ); - }); - - test('claude_md_assembly — loadConfig emits no warning when config.json contains claude_md_assembly key', () => { - tmpDir = createTempProject('gsd-2687-cma-'); - const configPath = path.join(tmpDir, '.planning', 'config.json'); - fs.writeFileSync( - configPath, - JSON.stringify({ claude_md_assembly: { mode: 'custom', blocks: { identity: true } } }, null, 2), - 'utf-8' - ); - - // resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check - const result = runWithStderr(['resolve-model', 'planner'], tmpDir); - - assert.equal( - result.stderr.trim(), - '', - `loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}` - ); - }); -}); diff --git a/tests/cli-modules-doc-parity.test.cjs b/tests/cli-modules-doc-parity.test.cjs deleted file mode 100644 index 7c8c9dcd5..000000000 --- a/tests/cli-modules-doc-parity.test.cjs +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/** - * For every `get-shit-done/bin/lib/*.cjs`, assert the module name - * appears as a row in docs/INVENTORY.md's CLI Modules table. - * docs/CLI-TOOLS.md is allowed to describe a subset (narrative doc); - * INVENTORY.md is the authoritative module roster. - * - * Related: docs readiness refresh, lane-12 recommendation. - */ - -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 LIB_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib'); -const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8'); - -const moduleFiles = fs - .readdirSync(LIB_DIR) - .filter((f) => f.endsWith('.cjs')); - -function mentionedInInventoryCliModules(filename) { - // Row form: | `filename.cjs` | responsibility | - const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm'); - return rowRe.test(INVENTORY_MD); -} - -describe('every CLI module has a row in INVENTORY.md', () => { - for (const file of moduleFiles) { - test(file, () => { - assert.ok( - mentionedInInventoryCliModules(file), - `get-shit-done/bin/lib/${file} has no row in docs/INVENTORY.md CLI Modules table — add one`, - ); - }); - } -}); diff --git a/tests/commands-doc-parity.test.cjs b/tests/commands-doc-parity.test.cjs deleted file mode 100644 index 7ed72a05c..000000000 --- a/tests/commands-doc-parity.test.cjs +++ /dev/null @@ -1,78 +0,0 @@ -// allow-test-rule: source-text-is-the-product -'use strict'; - -/** - * For every `commands/gsd/*.md`, assert its `/gsd-` slash command - * appears either (a) as a `### /gsd-...` heading in docs/COMMANDS.md or - * (b) as a row in docs/INVENTORY.md's Commands table. At least one of - * these must be true so every shipped command is reachable from docs. - * - * The slug is derived from the `name:` frontmatter field (e.g. `gsd-workflow`) - * rather than the filename (e.g. `ns-workflow.md`), so the test stays aligned - * with the actual deployed command token even when the file has a legacy name. - * - * Related: docs readiness refresh, lane-12 recommendation. - */ - -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 COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd'); -const COMMANDS_MD = fs.readFileSync(path.join(ROOT, 'docs', 'COMMANDS.md'), 'utf8'); -const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8'); - -const commandFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md')); - -/** - * Extract the slug from the `name:` frontmatter field. - * Accepts both `gsd:slug` and `gsd-slug` forms. - * Returns the slug portion only (e.g. `workflow`, `plan-phase`). - * Throws if frontmatter is missing or malformed. - */ -function parseSlugFromFrontmatter(content, filePath) { - // allow-test-rule: validating YAML frontmatter delimiter structure, not application source - if (!content.startsWith('---')) { - throw new Error('commands-doc-parity: missing YAML frontmatter in ' + filePath); - } - const closingIdx = content.indexOf('\n---', 3); - if (closingIdx < 0) { - throw new Error('commands-doc-parity: unclosed YAML frontmatter in ' + filePath); - } - const frontmatter = content.slice(0, closingIdx); - const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:-])([a-z0-9][a-z0-9-]*)"?\s*$/m); - if (!nameMatch) { - throw new Error('commands-doc-parity: could not extract slug from name: field in ' + filePath); - } - return nameMatch[2]; -} - -function mentionedInCommandsDoc(slug) { - // Match a heading like: ### /gsd- or ## /gsd- - const headingRe = new RegExp(`^#{2,4}\\s+\\\`?/gsd-${slug}\\\`?(?:[\\s(]|$)`, 'm'); - return headingRe.test(COMMANDS_MD); -} - -function mentionedInInventory(slug) { - // Match a row like: | `/gsd-` | ... | - const rowRe = new RegExp(`\\|\\s*\\\`/gsd-${slug}\\\`\\s*\\|`, 'm'); - return rowRe.test(INVENTORY_MD); -} - -describe('every shipped command is documented somewhere', () => { - for (const file of commandFiles) { - const filePath = path.join(COMMANDS_DIR, file); - const content = fs.readFileSync(filePath, 'utf8'); - const slug = parseSlugFromFrontmatter(content, filePath); - test(`/gsd-${slug}`, () => { - const inCommandsDoc = mentionedInCommandsDoc(slug); - const inInventory = mentionedInInventory(slug); - assert.ok( - inCommandsDoc || inInventory, - `commands/gsd/${file} (name: gsd-${slug}) is not mentioned in docs/COMMANDS.md (as a heading) or docs/INVENTORY.md (as a Commands row) — add a one-line entry to at least one`, - ); - }); - } -}); diff --git a/tests/config-schema-docs-parity.test.cjs b/tests/config-schema-docs-parity.test.cjs deleted file mode 100644 index 35f235cdf..000000000 --- a/tests/config-schema-docs-parity.test.cjs +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -// allow-test-rule: source-text-is-the-product -// Reads .md/.json/.yml product files whose deployed text IS what the -// runtime loads — testing text content tests the deployed contract. - -/** - * Asserts every exact-match key in config-schema.cjs appears at least once - * in docs/CONFIGURATION.md. A key present in the validator but absent from - * the docs means users can set it but have no guidance. A key in the docs but - * absent from the validator means config-set silently rejects it. - * - * Dynamic patterns (agent_skills.*, review.models.*, features.*) are excluded - * from this check — they are documented by namespace in CONFIGURATION.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 { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs'); -const CONFIGURATION_MD = fs.readFileSync(path.join(ROOT, 'docs', 'CONFIGURATION.md'), 'utf8'); - -// Reserved for future internal keys; workflow._auto_chain_active removed from VALID_CONFIG_KEYS (#2530). -const INTERNAL_KEYS = new Set(); - -test('every key in VALID_CONFIG_KEYS is documented in docs/CONFIGURATION.md', () => { - const undocumented = []; - for (const key of VALID_CONFIG_KEYS) { - if (INTERNAL_KEYS.has(key)) continue; - if (!CONFIGURATION_MD.includes('`' + key + '`')) { - undocumented.push(key); - } - } - assert.deepStrictEqual( - undocumented, - [], - 'Keys in VALID_CONFIG_KEYS with no mention in docs/CONFIGURATION.md:\n' + - undocumented.map((k) => ' ' + k).join('\n') + - '\nAdd a row in the appropriate section of docs/CONFIGURATION.md.', - ); -}); diff --git a/tests/config-schema-sdk-parity.test.cjs b/tests/config-schema-sdk-parity.test.cjs deleted file mode 100644 index e61c7828e..000000000 --- a/tests/config-schema-sdk-parity.test.cjs +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -// allow-test-rule: structural source assertion — verifies that sdk/src/query/config-schema.ts -// is a re-export shell (no inline literals) rather than a redeclared Set. Runtime/IR comparison -// cannot distinguish a re-export from a redeclared `new Set([...])` with identical contents -// because both would deep-equal the manifest set; only source-shape inspection catches drift -// back to inline literals. See test "SDK config-schema.ts re-exports from configuration module". - -/** - * Manifest-as-source-of-truth guard (Phase 2, Cycle 5, #3536). - * - * Prior to Cycle 5, the CJS and SDK schema files each had independent inline - * literals. This test existed to prevent drift between them. After Cycle 5, - * BOTH sides derive their data from sdk/shared/config-schema.manifest.json, - * so there is nothing to drift — but the guard still serves a purpose: - * - * 1. Confirm that VALID_CONFIG_KEYS loaded at runtime from config-schema.cjs - * exactly matches the manifest's validKeys array. - * 2. Confirm that VALID_CONFIG_KEYS exported from sdk/src/query/config-schema.ts - * (via sdk/dist/) equals the same manifest. - * 3. Confirm that DYNAMIC_KEY_PATTERNS from config-schema.cjs have .source - * fields matching the manifest's dynamicKeyPatterns. - * 4. Confirm RUNTIME_STATE_KEYS from config-schema.cjs matches the manifest. - * - * This ensures neither side has accidentally disconnected from the manifest - * (e.g. reverted to inline literals or switched to a different data source). - */ - -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, 'sdk', 'shared', 'config-schema.manifest.json'); - -const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); -const manifestValidKeys = new Set(manifest.validKeys); -const manifestRuntimeKeys = new Set(manifest.runtimeStateKeys); -const manifestPatternSources = manifest.dynamicKeyPatterns.map((p) => p.source); - -const { - VALID_CONFIG_KEYS: CJS_KEYS, - RUNTIME_STATE_KEYS: CJS_RUNTIME_KEYS, - DYNAMIC_KEY_PATTERNS: CJS_PATTERNS, -} = require('../get-shit-done/bin/lib/config-schema.cjs'); - -// ─── CJS side: verify manifest-sourced values ───────────────────────────── - -test('CJS VALID_CONFIG_KEYS matches manifest validKeys exactly', () => { - const missingInCjs = [...manifestValidKeys].filter((k) => !CJS_KEYS.has(k)); - const extraInCjs = [...CJS_KEYS].filter((k) => !manifestValidKeys.has(k)); - assert.deepStrictEqual( - missingInCjs, - [], - 'Manifest keys missing from CJS VALID_CONFIG_KEYS:\n' + - missingInCjs.map((k) => ' ' + k).join('\n'), - ); - assert.deepStrictEqual( - extraInCjs, - [], - 'CJS VALID_CONFIG_KEYS has keys not in manifest:\n' + - extraInCjs.map((k) => ' ' + k).join('\n'), - ); -}); - -test('CJS RUNTIME_STATE_KEYS matches manifest runtimeStateKeys exactly', () => { - const missingInCjs = [...manifestRuntimeKeys].filter((k) => !CJS_RUNTIME_KEYS.has(k)); - const extraInCjs = [...CJS_RUNTIME_KEYS].filter((k) => !manifestRuntimeKeys.has(k)); - assert.deepStrictEqual(missingInCjs, [], 'Manifest runtime keys missing from CJS RUNTIME_STATE_KEYS'); - assert.deepStrictEqual(extraInCjs, [], 'CJS RUNTIME_STATE_KEYS has keys not in manifest'); -}); - -test('CJS DYNAMIC_KEY_PATTERNS .source fields match manifest dynamicKeyPatterns', () => { - assert.strictEqual( - CJS_PATTERNS.length, - manifestPatternSources.length, - `CJS has ${CJS_PATTERNS.length} patterns but manifest has ${manifestPatternSources.length}`, - ); - for (let i = 0; i < manifestPatternSources.length; i++) { - const expected = manifestPatternSources[i]; - const actual = CJS_PATTERNS[i].source; - assert.strictEqual( - actual, - expected, - `CJS pattern[${i}].source mismatch: expected "${expected}", got "${actual}"`, - ); - } -}); - -// ─── SDK side: verify config-schema.ts re-exports from configuration module ─ - -test('SDK config-schema.ts re-exports from config module (not inline literals)', () => { - const SDK_SCHEMA_PATH = path.join(ROOT, 'sdk', 'src', 'query', 'config-schema.ts'); - const src = fs.readFileSync(SDK_SCHEMA_PATH, 'utf8'); - - // After Cycle 5, the file must NOT contain inline key literals. - // It should import/re-export from '../config/index.js'. - assert.ok( - src.includes("from '../config/index.js'"), - 'sdk/src/query/config-schema.ts must re-export from ../config/index.js (not inline literals)', - ); - - // Must NOT contain a standalone new Set([...]) block with key literals. - // A minimal check: the file should not define VALID_CONFIG_KEYS as a Set literal. - assert.ok( - !src.includes("new Set([\n 'mode'") && !src.includes("new Set(['mode'"), - 'sdk/src/query/config-schema.ts must not contain an inline VALID_CONFIG_KEYS Set literal', - ); -}); - -// ─── Cross-check: CJS equals SDK via manifest ───────────────────────────── - -test('#2653 — CJS and SDK both source from the same manifest (set equality via manifest)', () => { - // Since both sides derive from sdk/shared/config-schema.manifest.json, - // the CJS runtime set must equal the manifest set (verified above). - // This test is the explicit statement of the invariant for audit purposes. - const cjsKeysSorted = [...CJS_KEYS].sort(); - const manifestKeysSorted = [...manifestValidKeys].sort(); - assert.deepStrictEqual( - cjsKeysSorted, - manifestKeysSorted, - 'CJS VALID_CONFIG_KEYS must equal manifest validKeys — both sides source from the manifest', - ); -}); - -test('#2653 — CJS DYNAMIC_KEY_PATTERNS test functions work correctly', () => { - // Verify that each pattern's test() function (reconstructed from manifest source) - // correctly accepts sample keys and rejects non-matching ones. - const samples = [ - ['agent_skills.gsd-planner', 0], - ['review.models.claude', 1], - ['features.some_feature', 2], - ['claude_md_assembly.blocks.intro', 3], - ['model_profile_overrides.codex.opus', 4], - ['models.planning', 5], - ['dynamic_routing.enabled', 6], - ['model_overrides.my-agent', 7], - ['review.max_prompt_tokens_per_reviewer.ollama', 8], - ]; - for (const [key, idx] of samples) { - assert.ok( - CJS_PATTERNS[idx].test(key), - `CJS pattern[${idx}] must accept "${key}"`, - ); - } -}); diff --git a/tests/hooks-doc-parity.test.cjs b/tests/hooks-doc-parity.test.cjs deleted file mode 100644 index 0c9218a78..000000000 --- a/tests/hooks-doc-parity.test.cjs +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * For every `hooks/*.(js|sh)`, assert the hook filename appears as a - * row in docs/INVENTORY.md's Hooks table. docs/ARCHITECTURE.md's hook - * table is allowed to lag — INVENTORY.md is authoritative. - * - * Related: docs readiness refresh, lane-12 recommendation. - */ - -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 HOOKS_DIR = path.join(ROOT, 'hooks'); -const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8'); - -const hookFiles = fs - .readdirSync(HOOKS_DIR) - .filter((f) => /\.(js|sh)$/.test(f)); - -function mentionedInInventoryHooks(filename) { - // Row form: | `filename.js` | event | purpose | - const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm'); - return rowRe.test(INVENTORY_MD); -} - -describe('every shipped hook has a row in INVENTORY.md', () => { - for (const file of hookFiles) { - test(file, () => { - assert.ok( - mentionedInInventoryHooks(file), - `hooks/${file} has no row in docs/INVENTORY.md Hooks table — add one`, - ); - }); - } -}); diff --git a/tests/inventory-source-parity.test.cjs b/tests/inventory-source-parity.test.cjs deleted file mode 100644 index 785331abb..000000000 --- a/tests/inventory-source-parity.test.cjs +++ /dev/null @@ -1,132 +0,0 @@ -'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', - ); - }); - } - }); -});