@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-statusline.js +164 -14
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- 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
|
|
7
|
-
* buildParityManifest
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
18
|
+
const { execFileSync } = require('node:child_process');
|
|
17
19
|
|
|
18
20
|
const ROOT = path.resolve(__dirname, '..');
|
|
19
|
-
|
|
20
|
-
|
|
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": "
|
|
58
|
+
"issue": "2198"
|
|
58
59
|
},
|
|
59
60
|
"state": {
|
|
60
61
|
"files": [
|