@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
@@ -3,50 +3,29 @@
3
3
  /**
4
4
  * Standalone golden-fixture generator for tests/golden-install-parity.
5
5
  *
6
- * This is a BUILD-TIME generation script — NOT a test run. It replicates the
7
- * buildParityManifest logic from tests/golden-install-parity.test.cjs and
8
- * captures the zcode fixture so the parity test (which the gsd-test gate runs)
9
- * has a committed artifact to compare against. The authoritative test gate
10
- * remains `gsd-test run`, never a local `node --test`.
6
+ * This is a BUILD-TIME generation script — NOT a test run. It imports the
7
+ * canonical buildParityManifest builder from tests/helpers/install-shared.cjs
8
+ * (issue #2266 — single source of truth shared with
9
+ * tests/golden-install-parity.test.cjs) and captures the zcode fixture so the
10
+ * parity test (which the gsd-test gate runs) has a committed artifact to
11
+ * compare against. The authoritative test gate remains `gsd-test run`, never
12
+ * a local `node --test`.
11
13
  *
12
14
  * Usage: node scripts/gen-golden-install-parity-zcode.cjs
13
15
  */
14
16
  const fs = require('node:fs');
15
17
  const path = require('node:path');
16
- const crypto = require('node:crypto');
18
+ const { execFileSync } = require('node:child_process');
17
19
 
18
20
  const ROOT = path.resolve(__dirname, '..');
19
- const { walk, runMinimalInstall, RUNTIME_META } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
20
- const PKG_VERSION = require(path.join(ROOT, 'package.json')).version;
21
+ // buildParityManifest (and its exclusion constants) is the canonical single
22
+ // source of truth in tests/helpers/install-shared.cjs (issue #2266) — the
23
+ // generator no longer keeps its own inline copy, which had drifted from the
24
+ // test harness's copy (missing the realpath/`<HOME>` normalization) and
25
+ // mis-generated the claude-local fixture (#2100).
26
+ const { runMinimalInstall, RUNTIME_META, buildParityManifest, BUILD_SCRIPT } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
21
27
  const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'golden-install-parity');
22
28
 
23
- const VOLATILE_FILES = new Set([
24
- 'gsd-file-manifest.json',
25
- 'gsd-install-state.json',
26
- '.gsd-source',
27
- 'gsd-core/CHANGELOG.md',
28
- ]);
29
- const HOOK_CONFIG_FILES = new Set(['settings.json', 'hooks.json']);
30
- const EXCLUDED_PREFIXES = ['gsd-core/bin/lib/'];
31
-
32
- function buildParityManifest(configDir, root) {
33
- const allFiles = walk(configDir);
34
- const unsorted = {};
35
- for (const full of allFiles) {
36
- const rel = path.relative(configDir, full).split(path.sep).join('/');
37
- if (VOLATILE_FILES.has(rel)) continue;
38
- if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
39
- if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
40
- const content = fs.readFileSync(full);
41
- const normalized = content.toString('utf8').split(root).join('<HOME>').split(PKG_VERSION).join('<VERSION>');
42
- const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
43
- unsorted[rel] = hash;
44
- }
45
- const sorted = {};
46
- for (const key of Object.keys(unsorted).sort()) sorted[key] = unsorted[key];
47
- return sorted;
48
- }
49
-
50
29
  function cleanup(root) {
51
30
  try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
52
31
  }
@@ -59,6 +38,13 @@ function cleanup(root) {
59
38
  // With no args, regenerates ALL runtimes. With args, only the named runtimes.
60
39
  const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
61
40
  fs.mkdirSync(FIXTURE_DIR, { recursive: true });
41
+
42
+ // Build hooks/dist before capturing so fixtures are complete even in a clean
43
+ // checkout (hooks/dist is gitignored + built; DEFECT.HOOKS-DIST-SCOPED-CI). The
44
+ // test harness's before() hook does the same; without it a fresh checkout omits
45
+ // every hooks/* path and this generator silently writes short fixtures.
46
+ execFileSync(process.execPath, [BUILD_SCRIPT], { stdio: 'pipe' });
47
+
62
48
  for (const runtime of targets) {
63
49
  if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
64
50
  process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
@@ -75,3 +61,17 @@ for (const runtime of targets) {
75
61
  fs.writeFileSync(fixturePath, JSON.stringify(actual, null, 2) + '\n', 'utf8');
76
62
  process.stdout.write(`[gen] ${runtime}: wrote ${Object.keys(actual).length} file hashes -> ${fixturePath}\n`);
77
63
  }
64
+
65
+ // Also regenerate the claude LOCAL legacy-layout fixture (claude-local.json).
66
+ // This layout is distinct from the global install (commands/gsd-*.md +
67
+ // agents/gsd-*.md) and has its own parity assertion in the test harness.
68
+ const { configDir: localConfigDir, root: localRoot } = runMinimalInstall({ runtime: 'claude', scope: 'local' });
69
+ let localActual;
70
+ try {
71
+ localActual = buildParityManifest(localConfigDir, localRoot);
72
+ } finally {
73
+ cleanup(localRoot);
74
+ }
75
+ const localFixturePath = path.join(FIXTURE_DIR, 'claude-local.json');
76
+ fs.writeFileSync(localFixturePath, JSON.stringify(localActual, null, 2) + '\n', 'utf8');
77
+ process.stdout.write(`[gen] claude-local: wrote ${Object.keys(localActual).length} file hashes -> ${localFixturePath}\n`);
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Standalone golden-fixture generator for tests/golden-install-tree (#2267 Phase 2).
5
+ *
6
+ * This is a BUILD-TIME generation script — NOT a test run. It imports the
7
+ * canonical buildInstallTree builder from tests/helpers/install-shared.cjs,
8
+ * which in turn reuses buildParityManifest's exact exclusion set (issue
9
+ * #2266) so the file-set fixtures here and the golden-install-parity content
10
+ * fixtures never diverge on which files they cover. The authoritative test
11
+ * gate remains `gsd-test run`, never a local `node --test`.
12
+ *
13
+ * Usage: node scripts/gen-install-tree-fixtures.cjs [runtime ...]
14
+ */
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const { execFileSync } = require('node:child_process');
18
+
19
+ const ROOT = path.resolve(__dirname, '..');
20
+ // buildInstallTree (and the exclusion constants it reuses from
21
+ // buildParityManifest) is the canonical single source of truth in
22
+ // tests/helpers/install-shared.cjs (issue #2266/#2267) — this generator does
23
+ // not keep its own inline copy of the walk/exclusion logic.
24
+ const { runMinimalInstall, RUNTIME_META, buildInstallTree, BUILD_SCRIPT } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
25
+ const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'install-tree');
26
+
27
+ function cleanup(root) {
28
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
29
+ }
30
+
31
+ // Regenerate the fixture for every runtime in RUNTIME_META. Needed when a
32
+ // SHARED gsd-core payload file (e.g. model-catalog.json, capability-registry)
33
+ // changes content — its path appears in every runtime's manifest, so all
34
+ // fixtures must be recaptured together. Usage:
35
+ // node scripts/gen-install-tree-fixtures.cjs [runtime ...]
36
+ // With no args, regenerates ALL runtimes. With args, only the named runtimes.
37
+ const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
38
+ fs.mkdirSync(FIXTURE_DIR, { recursive: true });
39
+
40
+ // Build hooks/dist before capturing so fixtures are complete even in a clean
41
+ // checkout (hooks/dist is gitignored + built; DEFECT.HOOKS-DIST-SCOPED-CI). The
42
+ // test harness's before() hook does the same; without it a fresh checkout omits
43
+ // every hooks/* path and this generator silently writes short fixtures.
44
+ execFileSync(process.execPath, [BUILD_SCRIPT], { stdio: 'pipe' });
45
+
46
+ for (const runtime of targets) {
47
+ if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
48
+ process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
49
+ continue;
50
+ }
51
+ const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
52
+ let actual;
53
+ try {
54
+ actual = buildInstallTree(configDir, root);
55
+ } finally {
56
+ cleanup(root);
57
+ }
58
+ const fixturePath = path.join(FIXTURE_DIR, `${runtime}.json`);
59
+ fs.writeFileSync(fixturePath, JSON.stringify(actual, null, 2) + '\n', 'utf8');
60
+ process.stdout.write(`[gen] ${runtime}: wrote ${actual.length} paths -> ${fixturePath}\n`);
61
+ }
62
+
63
+ // Also regenerate the claude LOCAL legacy-layout fixture (claude-local.json).
64
+ // This layout is distinct from the global install (commands/gsd-*.md +
65
+ // agents/gsd-*.md) and has its own snapshot assertion in the test harness.
66
+ const { configDir: localConfigDir, root: localRoot } = runMinimalInstall({ runtime: 'claude', scope: 'local' });
67
+ let localActual;
68
+ try {
69
+ localActual = buildInstallTree(localConfigDir, localRoot);
70
+ } finally {
71
+ cleanup(localRoot);
72
+ }
73
+ const localFixturePath = path.join(FIXTURE_DIR, 'claude-local.json');
74
+ fs.writeFileSync(localFixturePath, JSON.stringify(localActual, null, 2) + '\n', 'utf8');
75
+ process.stdout.write(`[gen] claude-local: wrote ${localActual.length} paths -> ${localFixturePath}\n`);
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
6
+ * (and, once PR2 ships docs/registries/eos.json, docs/registries/eos-registry.md)
7
+ * from the corresponding source JSON, via registry-schema.cjs#renderMarkdown.
8
+ * Issue #2182.
9
+ *
10
+ * NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
11
+ * generates the RUNTIME capability manifest consumed by the host at runtime
12
+ * (`gsd-core/bin/lib/capability-registry.cjs`, built from every
13
+ * `capabilities/<id>/capability.json` declaration). THIS script instead
14
+ * generates the human-facing DOCUMENTATION catalog pages
15
+ * (`docs/registries/*.md`) from the third-party discoverability registry
16
+ * source JSON (`docs/registries/{capabilities,eos}.json`). The two pipelines
17
+ * are independent — do not conflate them.
18
+ *
19
+ * Usage:
20
+ * node scripts/gen-registry.cjs # print rendered markdown(s) to stdout
21
+ * node scripts/gen-registry.cjs --write # write the *-registry.md file(s)
22
+ * node scripts/gen-registry.cjs --check # exit 1 if a committed *-registry.md is stale
23
+ *
24
+ * Root is resolved from process.cwd() (not __dirname) — mirrors
25
+ * scripts/validate-registry.cjs so both are drivable as subprocesses against
26
+ * isolated temp-fixture directories via `cwd`.
27
+ */
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+
32
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
33
+ const { renderMarkdown } = require('./registry-schema.cjs');
34
+
35
+ const SOURCES = [
36
+ { type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
37
+ { type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
38
+ ];
39
+
40
+ /**
41
+ * The generator always writes LF; a Windows checkout (autocrlf) may present
42
+ * committed files with CRLF. Normalize before comparing so `--check` only
43
+ * fails on real content drift, not checkout-introduced line-ending noise.
44
+ *
45
+ * @param {string} content
46
+ * @returns {string}
47
+ */
48
+ function normalizeLineEndings(content) {
49
+ return content.replace(/\r/g, '');
50
+ }
51
+
52
+ function getRegistriesDir() {
53
+ return path.join(process.cwd(), 'docs', 'registries');
54
+ }
55
+
56
+ /**
57
+ * Render the markdown for a single registry type from its committed source
58
+ * JSON.
59
+ *
60
+ * Only `eos.json` is optional (pre-PR2, before that source JSON ships) —
61
+ * an absent `eos.json` returns null and callers treat that as "nothing to
62
+ * do". `capabilities.json` is the primary registry source: a missing
63
+ * `capabilities.json` is ALWAYS an error (never a silent "up to date"
64
+ * pass), mirroring the type distinction in `scripts/validate-registry.cjs`
65
+ * (`type === 'eos' && !exists → continue`).
66
+ *
67
+ * @param {'capability'|'eos'} type
68
+ * @returns {string|null}
69
+ */
70
+ function renderFor(type) {
71
+ const source = SOURCES.find((s) => s.type === type);
72
+ if (!source) throw new Error(`gen-registry: unknown registry type "${type}"`);
73
+
74
+ const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
75
+ if (!fs.existsSync(jsonPath)) {
76
+ if (type === 'eos') return null;
77
+ throw new ExitError(
78
+ 1,
79
+ `${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
80
+ );
81
+ }
82
+
83
+ const entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
84
+ return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
85
+ }
86
+
87
+ function main() {
88
+ const [, , flag] = process.argv;
89
+ const registriesDir = getRegistriesDir();
90
+ let anyDrift = false;
91
+
92
+ for (const { type, mdFile } of SOURCES) {
93
+ const rendered = renderFor(type);
94
+ if (rendered === null) continue; // source JSON absent (eos.json before PR2)
95
+
96
+ const mdPath = path.join(registriesDir, mdFile);
97
+
98
+ if (flag === '--check') {
99
+ if (!fs.existsSync(mdPath)) {
100
+ process.stderr.write(`${mdFile} does not exist. Run:\n node scripts/gen-registry.cjs --write\n`);
101
+ anyDrift = true;
102
+ continue;
103
+ }
104
+ const committed = fs.readFileSync(mdPath, 'utf8');
105
+ if (normalizeLineEndings(committed) !== normalizeLineEndings(rendered)) {
106
+ process.stderr.write(`${mdFile} is stale. Run:\n node scripts/gen-registry.cjs --write\n`);
107
+ anyDrift = true;
108
+ }
109
+ } else if (flag === '--write') {
110
+ fs.mkdirSync(registriesDir, { recursive: true });
111
+ fs.writeFileSync(mdPath, rendered);
112
+ process.stdout.write(`Wrote ${mdPath}\n`);
113
+ } else {
114
+ process.stdout.write(rendered + '\n');
115
+ }
116
+ }
117
+
118
+ if (flag === '--check') {
119
+ if (anyDrift) throw new ExitError(1, 'registry markdown is stale');
120
+ process.stdout.write('docs/registries/*.md are up to date.\n');
121
+ }
122
+
123
+ return 0;
124
+ }
125
+
126
+ if (require.main === module) runMain(main);
127
+
128
+ module.exports = { main, renderFor, SOURCES, normalizeLineEndings };
@@ -99,7 +99,6 @@
99
99
  "tests/mcp-tool-inheritance.test.cjs :: source-text-is-the-product",
100
100
  "tests/milestone-summary.test.cjs :: source-text-is-the-product",
101
101
  "tests/milestone.test.cjs :: source-text-is-the-product",
102
- "tests/milestone.test.cjs :: structural-regression-guard",
103
102
  "tests/model-catalog-runtime-defaults.test.cjs :: source-text-is-the-product",
104
103
  "tests/next-safety-gates.test.cjs :: source-text-is-the-product",
105
104
  "tests/next-up-clear-order.test.cjs :: source-text-is-the-product",
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Table-schema drift lint (ADR-2143 §3 Generative-Fix-Divergence guard, §7
6
+ * "prohibition with teeth", epic #2143).
7
+ *
8
+ * `TABLE_SCHEMAS` (`src/markdown-table.cts`, compiled to
9
+ * `gsd-core/bin/lib/markdown-table.cjs`) is the single-source registry naming
10
+ * every canonical GSD pipe-table's column-header shape. Each registered
11
+ * variant's exact `| col | col |` header string MUST appear verbatim in the
12
+ * ONE template/workflow file that emits that table — otherwise the writer and
13
+ * the registry have silently drifted apart, the exact bug class this ADR
14
+ * closes (#2137 — reader hard-codes a shape the writer doesn't emit; #2133 —
15
+ * the `quick.md` writer and its shell guard disagree on column count; #2119 —
16
+ * dual SECURITY.md writers with conflicting shapes).
17
+ *
18
+ * Modeled on scripts/lint-phase-id-drift.cjs / scripts/lint-package-identity-drift.cjs:
19
+ * a standalone node script (not a node:test), wired into `lint:ci`, exit 0 clean /
20
+ * exit 1 + message on drift.
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+
26
+ // Schema id -> the ONE canonical template/workflow file that must emit every
27
+ // variant's header verbatim (ADR-2143 §3).
28
+ const SCHEMA_SOURCE_FILES = {
29
+ RoadmapProgress: path.join('gsd-core', 'templates', 'roadmap.md'),
30
+ RequirementsTraceability: path.join('gsd-core', 'templates', 'requirements.md'),
31
+ QuickTasks: path.join('gsd-core', 'workflows', 'quick.md'),
32
+ Security: path.join('gsd-core', 'templates', 'SECURITY.md'),
33
+ };
34
+
35
+ /** Build the exact `| a | b | c |` header line for one schema variant. */
36
+ function buildHeader(variant) {
37
+ return `| ${variant.columns.join(' | ')} |`;
38
+ }
39
+
40
+ /** Normalize whitespace around pipes so template formatting quirks (extra
41
+ * spaces added by an editor, etc.) don't produce a false drift report. */
42
+ function normalize(line) {
43
+ return line.replace(/[ \t]*\|[ \t]*/g, '|').trim();
44
+ }
45
+
46
+ /**
47
+ * Pure: given `schemas` (a `TABLE_SCHEMAS`-shaped object) and a
48
+ * `readFile(relPath) -> string|null` accessor, find every variant whose exact
49
+ * header does not appear verbatim (pipe-whitespace normalized) in its
50
+ * schema's registered source file. Returns
51
+ * `[{ schemaId, label, header, file, reason }]`; empty when clean.
52
+ */
53
+ function findTableSchemaDrift(schemas, readFile, sourceFiles = SCHEMA_SOURCE_FILES) {
54
+ const violations = [];
55
+ for (const [schemaId, variants] of Object.entries(schemas)) {
56
+ const relPath = sourceFiles[schemaId];
57
+ if (!relPath) {
58
+ violations.push({
59
+ schemaId,
60
+ label: null,
61
+ header: null,
62
+ file: null,
63
+ reason: 'no canonical source file registered for this schema id in SCHEMA_SOURCE_FILES',
64
+ });
65
+ continue;
66
+ }
67
+
68
+ const content = readFile(relPath);
69
+ if (content == null) {
70
+ for (const variant of variants) {
71
+ violations.push({
72
+ schemaId,
73
+ label: variant.label,
74
+ header: buildHeader(variant),
75
+ file: relPath,
76
+ reason: 'source file not found or unreadable',
77
+ });
78
+ }
79
+ continue;
80
+ }
81
+
82
+ const normalizedLines = content.split(/\r?\n/).map(normalize);
83
+ for (const variant of variants) {
84
+ const expected = normalize(buildHeader(variant));
85
+ if (!normalizedLines.includes(expected)) {
86
+ violations.push({
87
+ schemaId,
88
+ label: variant.label,
89
+ header: buildHeader(variant),
90
+ file: relPath,
91
+ reason: 'header not found verbatim in source file',
92
+ });
93
+ }
94
+ }
95
+ }
96
+ return violations;
97
+ }
98
+
99
+ /**
100
+ * Load the built seam and scan the real repo tree. Returns the same shape as
101
+ * `findTableSchemaDrift`. If the seam hasn't been built yet (`npm run
102
+ * build:lib`), reports a single actionable violation rather than throwing.
103
+ */
104
+ function scanRepo(root) {
105
+ const seamPath = path.join(root, 'gsd-core', 'bin', 'lib', 'markdown-table.cjs');
106
+ let seam;
107
+ try {
108
+ seam = require(seamPath);
109
+ } catch (e) {
110
+ return [{
111
+ schemaId: null,
112
+ label: null,
113
+ header: null,
114
+ file: null,
115
+ reason: `cannot load the markdown-table seam at ${path.relative(root, seamPath)} — run 'npm run build:lib' first (${e.message})`,
116
+ }];
117
+ }
118
+
119
+ const readFile = (relPath) => {
120
+ try {
121
+ return fs.readFileSync(path.join(root, relPath), 'utf8');
122
+ } catch {
123
+ return null;
124
+ }
125
+ };
126
+
127
+ return findTableSchemaDrift(seam.TABLE_SCHEMAS, readFile);
128
+ }
129
+
130
+ function main() {
131
+ const root = path.join(__dirname, '..');
132
+ const violations = scanRepo(root);
133
+ if (violations.length === 0) {
134
+ process.stdout.write(
135
+ 'ok table-schema-drift: every TABLE_SCHEMAS variant header appears verbatim in its canonical template/workflow\n',
136
+ );
137
+ return;
138
+ }
139
+ process.stderr.write(
140
+ 'table-schema-drift: TABLE_SCHEMAS variant(s) whose header is absent from their canonical source file (ADR-2143 §3).\n',
141
+ );
142
+ process.stderr.write(
143
+ 'Either update the template/workflow to emit the registered header verbatim, or update\n' +
144
+ 'TABLE_SCHEMAS in src/markdown-table.cts to match — the two must never drift.\n',
145
+ );
146
+ for (const v of violations) {
147
+ const id = v.label ? `${v.schemaId}.${v.label}` : (v.schemaId ?? '?');
148
+ const loc = v.file ? ` (${v.file})` : '';
149
+ const expected = v.header ? ` — expected ${JSON.stringify(v.header)}` : '';
150
+ process.stderr.write(` ${id}${loc}: ${v.reason}${expected}\n`);
151
+ }
152
+ process.exitCode = 1;
153
+ }
154
+
155
+ if (require.main === module) main();
156
+
157
+ module.exports = { findTableSchemaDrift, scanRepo, buildHeader, normalize, SCHEMA_SOURCE_FILES };
@@ -50,11 +50,12 @@
50
50
  },
51
51
  "security": {
52
52
  "files": [
53
+ "security-dead-exports.regression.test.cjs",
53
54
  "security-prompt-injection.security.test.cjs",
54
55
  "security-scan.security.test.cjs",
55
56
  "security.test.cjs"
56
57
  ],
57
- "issue": "TBD"
58
+ "issue": "2198"
58
59
  },
59
60
  "state": {
60
61
  "files": [