@oddessentials/repo-standards 4.2.0 → 4.3.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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/scripts/build.ts +172 -0
- package/scripts/detect-bazel.test.ts +61 -0
- package/scripts/detect-bazel.ts +82 -0
- package/scripts/generate-instructions.ts +174 -0
- package/scripts/generate-standards.ts +247 -0
- package/scripts/sync-manifest-version.cjs +32 -0
- package/scripts/validate-schema.ts +289 -0
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js","../src/version.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport type {\n MasterJson,\n StackChecklistJson,\n StackId,\n CiSystem,\n} from \"./types.js\";\nimport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION } from \"./version.js\";\n\n// Re-export types for consumers\nexport type { MasterJson, StackChecklistJson, StackId, CiSystem };\n\n// Re-export version info (stable API contract)\nexport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION };\n\n// ESM equivalent of __dirname\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Path to config directory:\n// - When running from src/ (dev/test): use repo root config/\n// - When running from dist/ (installed): use dist/config/\nconst isDevMode = __dirname.includes(\"src\");\nconst configDir = isDevMode\n ? join(__dirname, \"..\", \"config\")\n : join(__dirname, \"config\");\n\n/** Load the master spec JSON from the packaged dist directory */\nexport function loadMasterSpec(): MasterJson {\n const filePath = join(configDir, \"standards.json\");\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** Load a stack-specific checklist (optionally filtered by CI system) */\nexport function loadBaseline(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n const suffix = ci ? `.${ci}` : \"\";\n const file = `standards.${stack}${suffix}.json`;\n const filePath = join(configDir, file);\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** List all supported stacks (derived from the master spec) */\nexport function listSupportedStacks(): readonly StackId[] {\n const spec = loadMasterSpec();\n return Object.keys(spec.stacks) as StackId[];\n}\n\n/** List all supported CI systems (derived from the master spec) */\nexport function listSupportedCiSystems(): readonly CiSystem[] {\n const spec = loadMasterSpec();\n return spec.ciSystems as CiSystem[];\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadBaseline - loads stack-specific standards checklist.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getStandards(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n return loadBaseline(stack, ci);\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadMasterSpec - loads the master standards schema.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getSchema(): MasterJson {\n return loadMasterSpec();\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * AUTO-GENERATED at build time by scripts/build.ts\n * DO NOT EDIT MANUALLY\n *\n * This module provides version information for the repo-standards package.\n * Consumers should import from here instead of package.json to avoid\n * ESM/CJS interop issues.\n */\n\nexport const STANDARDS_VERSION = '4.
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js","../src/version.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport type {\n MasterJson,\n StackChecklistJson,\n StackId,\n CiSystem,\n} from \"./types.js\";\nimport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION } from \"./version.js\";\n\n// Re-export types for consumers\nexport type { MasterJson, StackChecklistJson, StackId, CiSystem };\n\n// Re-export version info (stable API contract)\nexport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION };\n\n// ESM equivalent of __dirname\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Path to config directory:\n// - When running from src/ (dev/test): use repo root config/\n// - When running from dist/ (installed): use dist/config/\nconst isDevMode = __dirname.includes(\"src\");\nconst configDir = isDevMode\n ? join(__dirname, \"..\", \"config\")\n : join(__dirname, \"config\");\n\n/** Load the master spec JSON from the packaged dist directory */\nexport function loadMasterSpec(): MasterJson {\n const filePath = join(configDir, \"standards.json\");\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** Load a stack-specific checklist (optionally filtered by CI system) */\nexport function loadBaseline(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n const suffix = ci ? `.${ci}` : \"\";\n const file = `standards.${stack}${suffix}.json`;\n const filePath = join(configDir, file);\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** List all supported stacks (derived from the master spec) */\nexport function listSupportedStacks(): readonly StackId[] {\n const spec = loadMasterSpec();\n return Object.keys(spec.stacks) as StackId[];\n}\n\n/** List all supported CI systems (derived from the master spec) */\nexport function listSupportedCiSystems(): readonly CiSystem[] {\n const spec = loadMasterSpec();\n return spec.ciSystems as CiSystem[];\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadBaseline - loads stack-specific standards checklist.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getStandards(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n return loadBaseline(stack, ci);\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadMasterSpec - loads the master standards schema.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getSchema(): MasterJson {\n return loadMasterSpec();\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * AUTO-GENERATED at build time by scripts/build.ts\n * DO NOT EDIT MANUALLY\n *\n * This module provides version information for the repo-standards package.\n * Consumers should import from here instead of package.json to avoid\n * ESM/CJS interop issues.\n */\n\nexport const STANDARDS_VERSION = '4.2.0';\nexport const STANDARDS_SCHEMA_VERSION = 4;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,QAAQ,YAAY,MAAM,WAC1E,SAAS,cAAc,MACvB,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEtC,IAAM,gBAAgC,iCAAiB;;;ADZ9D,sBAA8B;AAC9B,qBAA6B;AAC7B,uBAA8B;;;AEOvB,IAAM,oBAAoB;AAC1B,IAAM,2BAA2B;;;AFQxC,IAAMA,kBAAa,+BAAc,aAAe;AAChD,IAAM,gBAAY,0BAAQA,WAAU;AAKpC,IAAM,YAAY,UAAU,SAAS,KAAK;AAC1C,IAAM,YAAY,gBACd,uBAAK,WAAW,MAAM,QAAQ,QAC9B,uBAAK,WAAW,QAAQ;AAGrB,SAAS,iBAA6B;AAC3C,QAAM,eAAW,uBAAK,WAAW,gBAAgB;AACjD,SAAO,KAAK,UAAM,6BAAa,UAAU,MAAM,CAAC;AAClD;AAGO,SAAS,aACd,OACA,IACoB;AACpB,QAAM,SAAS,KAAK,IAAI,EAAE,KAAK;AAC/B,QAAM,OAAO,aAAa,KAAK,GAAG,MAAM;AACxC,QAAM,eAAW,uBAAK,WAAW,IAAI;AACrC,SAAO,KAAK,UAAM,6BAAa,UAAU,MAAM,CAAC;AAClD;AAGO,SAAS,sBAA0C;AACxD,QAAM,OAAO,eAAe;AAC5B,SAAO,OAAO,KAAK,KAAK,MAAM;AAChC;AAGO,SAAS,yBAA8C;AAC5D,QAAM,OAAO,eAAe;AAC5B,SAAO,KAAK;AACd;AAOO,SAAS,aACd,OACA,IACoB;AACpB,SAAO,aAAa,OAAO,EAAE;AAC/B;AAOO,SAAS,YAAwB;AACtC,SAAO,eAAe;AACxB;","names":["__filename"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -156,7 +156,7 @@ interface StackChecklistJson {
|
|
|
156
156
|
* Consumers should import from here instead of package.json to avoid
|
|
157
157
|
* ESM/CJS interop issues.
|
|
158
158
|
*/
|
|
159
|
-
declare const STANDARDS_VERSION = "4.
|
|
159
|
+
declare const STANDARDS_VERSION = "4.2.0";
|
|
160
160
|
declare const STANDARDS_SCHEMA_VERSION = 4;
|
|
161
161
|
|
|
162
162
|
/** Load the master spec JSON from the packaged dist directory */
|
package/dist/index.d.ts
CHANGED
|
@@ -156,7 +156,7 @@ interface StackChecklistJson {
|
|
|
156
156
|
* Consumers should import from here instead of package.json to avoid
|
|
157
157
|
* ESM/CJS interop issues.
|
|
158
158
|
*/
|
|
159
|
-
declare const STANDARDS_VERSION = "4.
|
|
159
|
+
declare const STANDARDS_VERSION = "4.2.0";
|
|
160
160
|
declare const STANDARDS_SCHEMA_VERSION = 4;
|
|
161
161
|
|
|
162
162
|
/** Load the master spec JSON from the packaged dist directory */
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/version.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport type {\n MasterJson,\n StackChecklistJson,\n StackId,\n CiSystem,\n} from \"./types.js\";\nimport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION } from \"./version.js\";\n\n// Re-export types for consumers\nexport type { MasterJson, StackChecklistJson, StackId, CiSystem };\n\n// Re-export version info (stable API contract)\nexport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION };\n\n// ESM equivalent of __dirname\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Path to config directory:\n// - When running from src/ (dev/test): use repo root config/\n// - When running from dist/ (installed): use dist/config/\nconst isDevMode = __dirname.includes(\"src\");\nconst configDir = isDevMode\n ? join(__dirname, \"..\", \"config\")\n : join(__dirname, \"config\");\n\n/** Load the master spec JSON from the packaged dist directory */\nexport function loadMasterSpec(): MasterJson {\n const filePath = join(configDir, \"standards.json\");\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** Load a stack-specific checklist (optionally filtered by CI system) */\nexport function loadBaseline(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n const suffix = ci ? `.${ci}` : \"\";\n const file = `standards.${stack}${suffix}.json`;\n const filePath = join(configDir, file);\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** List all supported stacks (derived from the master spec) */\nexport function listSupportedStacks(): readonly StackId[] {\n const spec = loadMasterSpec();\n return Object.keys(spec.stacks) as StackId[];\n}\n\n/** List all supported CI systems (derived from the master spec) */\nexport function listSupportedCiSystems(): readonly CiSystem[] {\n const spec = loadMasterSpec();\n return spec.ciSystems as CiSystem[];\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadBaseline - loads stack-specific standards checklist.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getStandards(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n return loadBaseline(stack, ci);\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadMasterSpec - loads the master standards schema.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getSchema(): MasterJson {\n return loadMasterSpec();\n}\n","/**\n * AUTO-GENERATED at build time by scripts/build.ts\n * DO NOT EDIT MANUALLY\n *\n * This module provides version information for the repo-standards package.\n * Consumers should import from here instead of package.json to avoid\n * ESM/CJS interop issues.\n */\n\nexport const STANDARDS_VERSION = '4.
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/version.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport type {\n MasterJson,\n StackChecklistJson,\n StackId,\n CiSystem,\n} from \"./types.js\";\nimport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION } from \"./version.js\";\n\n// Re-export types for consumers\nexport type { MasterJson, StackChecklistJson, StackId, CiSystem };\n\n// Re-export version info (stable API contract)\nexport { STANDARDS_VERSION, STANDARDS_SCHEMA_VERSION };\n\n// ESM equivalent of __dirname\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Path to config directory:\n// - When running from src/ (dev/test): use repo root config/\n// - When running from dist/ (installed): use dist/config/\nconst isDevMode = __dirname.includes(\"src\");\nconst configDir = isDevMode\n ? join(__dirname, \"..\", \"config\")\n : join(__dirname, \"config\");\n\n/** Load the master spec JSON from the packaged dist directory */\nexport function loadMasterSpec(): MasterJson {\n const filePath = join(configDir, \"standards.json\");\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** Load a stack-specific checklist (optionally filtered by CI system) */\nexport function loadBaseline(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n const suffix = ci ? `.${ci}` : \"\";\n const file = `standards.${stack}${suffix}.json`;\n const filePath = join(configDir, file);\n return JSON.parse(readFileSync(filePath, \"utf8\"));\n}\n\n/** List all supported stacks (derived from the master spec) */\nexport function listSupportedStacks(): readonly StackId[] {\n const spec = loadMasterSpec();\n return Object.keys(spec.stacks) as StackId[];\n}\n\n/** List all supported CI systems (derived from the master spec) */\nexport function listSupportedCiSystems(): readonly CiSystem[] {\n const spec = loadMasterSpec();\n return spec.ciSystems as CiSystem[];\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadBaseline - loads stack-specific standards checklist.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getStandards(\n stack: StackId,\n ci?: CiSystem,\n): StackChecklistJson {\n return loadBaseline(stack, ci);\n}\n\n/**\n * PUBLIC API CONTRACT (semver-governed)\n * Alias for loadMasterSpec - loads the master standards schema.\n * Breaking changes to this function signature require a major version bump.\n */\nexport function getSchema(): MasterJson {\n return loadMasterSpec();\n}\n","/**\n * AUTO-GENERATED at build time by scripts/build.ts\n * DO NOT EDIT MANUALLY\n *\n * This module provides version information for the repo-standards package.\n * Consumers should import from here instead of package.json to avoid\n * ESM/CJS interop issues.\n */\n\nexport const STANDARDS_VERSION = '4.2.0';\nexport const STANDARDS_SCHEMA_VERSION = 4;\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,MAAM,eAAe;;;ACOvB,IAAM,oBAAoB;AAC1B,IAAM,2BAA2B;;;ADQxC,IAAMA,cAAa,cAAc,YAAY,GAAG;AAChD,IAAMC,aAAY,QAAQD,WAAU;AAKpC,IAAM,YAAYC,WAAU,SAAS,KAAK;AAC1C,IAAM,YAAY,YACd,KAAKA,YAAW,MAAM,QAAQ,IAC9B,KAAKA,YAAW,QAAQ;AAGrB,SAAS,iBAA6B;AAC3C,QAAM,WAAW,KAAK,WAAW,gBAAgB;AACjD,SAAO,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;AAClD;AAGO,SAAS,aACd,OACA,IACoB;AACpB,QAAM,SAAS,KAAK,IAAI,EAAE,KAAK;AAC/B,QAAM,OAAO,aAAa,KAAK,GAAG,MAAM;AACxC,QAAM,WAAW,KAAK,WAAW,IAAI;AACrC,SAAO,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;AAClD;AAGO,SAAS,sBAA0C;AACxD,QAAM,OAAO,eAAe;AAC5B,SAAO,OAAO,KAAK,KAAK,MAAM;AAChC;AAGO,SAAS,yBAA8C;AAC5D,QAAM,OAAO,eAAe;AAC5B,SAAO,KAAK;AACd;AAOO,SAAS,aACd,OACA,IACoB;AACpB,SAAO,aAAa,OAAO,EAAE;AAC/B;AAOO,SAAS,YAAwB;AACtC,SAAO,eAAe;AACxB;","names":["__filename","__dirname"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/repo-standards",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"description": "Standards and CI filtering utilities for multi-stack repository governance.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"prepare": "husky",
|
|
20
|
+
"postinstall": "git config core.hooksPath .husky || true",
|
|
20
21
|
"ci": "npm run lint && npm run format:check && npm run typecheck && npm run test && npm run build",
|
|
21
22
|
"generate:standards": "tsx scripts/generate-standards.ts",
|
|
22
23
|
"validate:schema": "tsx scripts/validate-schema.ts",
|
|
@@ -53,10 +54,10 @@
|
|
|
53
54
|
"node": ">=22 <23"
|
|
54
55
|
},
|
|
55
56
|
"lint-staged": {
|
|
56
|
-
"*.{js,ts,mjs,json,md}": [
|
|
57
|
+
"*.{js,cjs,ts,mjs,json,md}": [
|
|
57
58
|
"prettier --write"
|
|
58
59
|
],
|
|
59
|
-
"*.{js,ts,mjs}": [
|
|
60
|
+
"*.{js,cjs,ts,mjs}": [
|
|
60
61
|
"eslint --fix"
|
|
61
62
|
]
|
|
62
63
|
},
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"files": [
|
|
93
94
|
"dist/",
|
|
94
95
|
"README.md",
|
|
95
|
-
"LICENSE"
|
|
96
|
+
"LICENSE",
|
|
97
|
+
"scripts/"
|
|
96
98
|
]
|
|
97
99
|
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
// scripts/build.ts
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build script for @oddessentials/repo-standards
|
|
6
|
+
* - Copies master config/standards.json to dist/config/
|
|
7
|
+
* - Generates stack-specific JSON artifacts into dist/config/
|
|
8
|
+
* - Ensures deterministic output (sorted keys, no timestamps)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import {
|
|
13
|
+
writeFileSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
copyFileSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { resolve, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
|
|
24
|
+
// Helper to sort object keys recursively for deterministic output
|
|
25
|
+
function sortObject(obj: unknown): unknown {
|
|
26
|
+
if (Array.isArray(obj)) return obj.map(sortObject);
|
|
27
|
+
if (obj && typeof obj === "object") {
|
|
28
|
+
const sorted: Record<string, unknown> = {};
|
|
29
|
+
Object.keys(obj as Record<string, unknown>)
|
|
30
|
+
.sort()
|
|
31
|
+
.forEach((key) => {
|
|
32
|
+
sorted[key] = sortObject((obj as Record<string, unknown>)[key]);
|
|
33
|
+
});
|
|
34
|
+
return sorted;
|
|
35
|
+
}
|
|
36
|
+
return obj;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ensure schema version matches package.json major version.
|
|
40
|
+
* - Auto-upgrades schema when package.json major is higher (for semantic-release)
|
|
41
|
+
* - Fails if schema is ahead of package (prevents manual drift)
|
|
42
|
+
*/
|
|
43
|
+
function syncSchemaVersion(rootDir: string): void {
|
|
44
|
+
const pkgPath = join(rootDir, "package.json");
|
|
45
|
+
const standardsPath = join(rootDir, "config", "standards.json");
|
|
46
|
+
|
|
47
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
48
|
+
const standards = JSON.parse(readFileSync(standardsPath, "utf8"));
|
|
49
|
+
|
|
50
|
+
// Extract major version from package.json (e.g., "2.1.0" -> 2)
|
|
51
|
+
const pkgMajor = parseInt(pkg.version.split(".")[0], 10);
|
|
52
|
+
|
|
53
|
+
if (pkgMajor > standards.version) {
|
|
54
|
+
// Auto-upgrade schema version when semantic-release bumps package.json
|
|
55
|
+
console.log(
|
|
56
|
+
`Upgrading schema version: ${standards.version} -> ${pkgMajor} (from package.json)`,
|
|
57
|
+
);
|
|
58
|
+
standards.version = pkgMajor;
|
|
59
|
+
writeFileSync(standardsPath, JSON.stringify(standards, null, 2) + "\n");
|
|
60
|
+
} else if (standards.version > pkgMajor) {
|
|
61
|
+
// Schema ahead of package = manual drift, fail build
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Schema version drift: standards.json version=${standards.version} ` +
|
|
64
|
+
`is ahead of package.json major=${pkgMajor}. ` +
|
|
65
|
+
`This should not happen - schema version is set by CI.`,
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(
|
|
69
|
+
`Schema version ${standards.version} matches package.json major`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run the existing generator for each stack (and optional CI system)
|
|
75
|
+
function generateStack(stack: string, ci?: string) {
|
|
76
|
+
const args = ["scripts/generate-standards.ts", stack];
|
|
77
|
+
if (ci) args.push(ci);
|
|
78
|
+
const cmd = `npx tsx ${args.join(" ")}`;
|
|
79
|
+
console.log(`Generating ${stack}${ci ? " for " + ci : ""}`);
|
|
80
|
+
execSync(cmd, { stdio: "inherit" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate src/version.ts with current package version
|
|
85
|
+
* This runs before TypeScript compilation so the values are baked in
|
|
86
|
+
*/
|
|
87
|
+
function generateVersionFile(rootDir: string): void {
|
|
88
|
+
const pkgPath = join(rootDir, "package.json");
|
|
89
|
+
const versionPath = join(rootDir, "src", "version.ts");
|
|
90
|
+
|
|
91
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
92
|
+
const standards = JSON.parse(
|
|
93
|
+
readFileSync(join(rootDir, "config", "standards.json"), "utf8"),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const content = `/**
|
|
97
|
+
* AUTO-GENERATED at build time by scripts/build.ts
|
|
98
|
+
* DO NOT EDIT MANUALLY
|
|
99
|
+
*
|
|
100
|
+
* This module provides version information for the repo-standards package.
|
|
101
|
+
* Consumers should import from here instead of package.json to avoid
|
|
102
|
+
* ESM/CJS interop issues.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
export const STANDARDS_VERSION = '${pkg.version}';
|
|
106
|
+
export const STANDARDS_SCHEMA_VERSION = ${standards.version};
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
writeFileSync(versionPath, content);
|
|
110
|
+
console.log(`Generated src/version.ts with version ${pkg.version}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main() {
|
|
114
|
+
const rootDir = process.cwd();
|
|
115
|
+
const configSrc = resolve(rootDir, "config");
|
|
116
|
+
const configDest = resolve(rootDir, "dist", "config");
|
|
117
|
+
|
|
118
|
+
// Generate version.ts before TypeScript compilation
|
|
119
|
+
generateVersionFile(rootDir);
|
|
120
|
+
|
|
121
|
+
// Sync schema version with package.json major version (before generation)
|
|
122
|
+
syncSchemaVersion(rootDir);
|
|
123
|
+
|
|
124
|
+
// Ensure dist/config exists
|
|
125
|
+
mkdirSync(configDest, { recursive: true });
|
|
126
|
+
|
|
127
|
+
// Copy master standards.json first
|
|
128
|
+
copyFileSync(
|
|
129
|
+
join(configSrc, "standards.json"),
|
|
130
|
+
join(configDest, "standards.json"),
|
|
131
|
+
);
|
|
132
|
+
console.log("Copied master standards.json to dist/config/");
|
|
133
|
+
|
|
134
|
+
const stacks = ["typescript-js", "csharp-dotnet", "python", "rust", "go"];
|
|
135
|
+
const ciSystems = ["azure-devops", "github-actions"];
|
|
136
|
+
|
|
137
|
+
// Generate each stack without CI suffix
|
|
138
|
+
for (const stack of stacks) {
|
|
139
|
+
generateStack(stack);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Generate CI-specific views
|
|
143
|
+
for (const stack of stacks) {
|
|
144
|
+
for (const ci of ciSystems) {
|
|
145
|
+
generateStack(stack, ci);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Copy all generated files from config/ to dist/config/
|
|
150
|
+
const generatedFiles = readdirSync(configSrc).filter(
|
|
151
|
+
(f) => f.startsWith("standards.") && f.endsWith(".json"),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
for (const file of generatedFiles) {
|
|
155
|
+
const srcPath = join(configSrc, file);
|
|
156
|
+
const destPath = join(configDest, file);
|
|
157
|
+
|
|
158
|
+
// Read, sort for determinism, and write
|
|
159
|
+
const data = JSON.parse(readFileSync(srcPath, "utf8"));
|
|
160
|
+
const sorted = sortObject(data);
|
|
161
|
+
writeFileSync(destPath, JSON.stringify(sorted, null, 2) + "\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(
|
|
165
|
+
`Build complete – ${generatedFiles.length} artifacts written to dist/config/`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ESM entrypoint check
|
|
170
|
+
if (process.argv[1] === __filename) {
|
|
171
|
+
main();
|
|
172
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// scripts/detect-bazel.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { detectBazel } from "./detect-bazel.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const fixtures = path.resolve(__dirname, "..", "test", "fixtures");
|
|
10
|
+
|
|
11
|
+
describe("detectBazel", () => {
|
|
12
|
+
it("detects bzlmod repo via MODULE.bazel", () => {
|
|
13
|
+
const result = detectBazel(path.join(fixtures, "bzlmod-repo"));
|
|
14
|
+
expect(result.detected).toBe(true);
|
|
15
|
+
expect(result.mode).toBe("bzlmod");
|
|
16
|
+
expect(result.markers).toContain("MODULE.bazel");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("detects .bazelversion as optional marker", () => {
|
|
20
|
+
const result = detectBazel(path.join(fixtures, "bzlmod-repo"));
|
|
21
|
+
expect(result.markers).toContain(".bazelversion");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("detects workspace repo via WORKSPACE.bazel", () => {
|
|
25
|
+
const result = detectBazel(path.join(fixtures, "workspace-repo"));
|
|
26
|
+
expect(result.detected).toBe(true);
|
|
27
|
+
expect(result.mode).toBe("workspace");
|
|
28
|
+
expect(result.markers).toContain("WORKSPACE.bazel");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does NOT detect non-Bazel repo (regression)", () => {
|
|
32
|
+
const result = detectBazel(path.join(fixtures, "no-bazel-repo"));
|
|
33
|
+
expect(result.detected).toBe(false);
|
|
34
|
+
expect(result.mode).toBeUndefined();
|
|
35
|
+
expect(result.markers).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("detects hybrid monorepo at root level only", () => {
|
|
39
|
+
const result = detectBazel(path.join(fixtures, "hybrid-monorepo"));
|
|
40
|
+
expect(result.detected).toBe(true);
|
|
41
|
+
expect(result.mode).toBe("bzlmod");
|
|
42
|
+
expect(result.markers).toContain("MODULE.bazel");
|
|
43
|
+
// Does NOT include nested BUILD files in markers
|
|
44
|
+
expect(result.markers).not.toContain("apps/web/BUILD.bazel");
|
|
45
|
+
expect(result.markers).not.toContain("libs/vendored/BUILD.bazel");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prefers bzlmod over workspace when MODULE.bazel exists", () => {
|
|
49
|
+
// The bzlmod-repo fixture has MODULE.bazel, should be detected as bzlmod
|
|
50
|
+
const result = detectBazel(path.join(fixtures, "bzlmod-repo"));
|
|
51
|
+
expect(result.mode).toBe("bzlmod");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does NOT detect Bazel when only nested BUILD files exist (no root markers)", () => {
|
|
55
|
+
// This validates the documented contract: detection uses repo-root markers only
|
|
56
|
+
const result = detectBazel(path.join(fixtures, "nested-build-only"));
|
|
57
|
+
expect(result.detected).toBe(false);
|
|
58
|
+
expect(result.mode).toBeUndefined();
|
|
59
|
+
expect(result.markers).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// scripts/detect-bazel.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone Bazel detection utility.
|
|
5
|
+
* Does NOT require Bazel to be installed.
|
|
6
|
+
* Only checks repo-root markers to avoid false positives from vendored deps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export interface BazelDetectionResult {
|
|
13
|
+
/** Whether Bazel markers were detected at repo root */
|
|
14
|
+
detected: boolean;
|
|
15
|
+
/** Bazel mode: bzlmod (MODULE.bazel) or workspace (WORKSPACE*) */
|
|
16
|
+
mode?: "bzlmod" | "workspace";
|
|
17
|
+
/** List of detected marker files */
|
|
18
|
+
markers: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Root-level markers that indicate a Bazel repo */
|
|
22
|
+
const ROOT_MARKERS = {
|
|
23
|
+
bzlmod: ["MODULE.bazel"],
|
|
24
|
+
workspace: ["WORKSPACE.bazel", "WORKSPACE"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Optional markers that support Bazel but don't trigger detection alone */
|
|
28
|
+
const OPTIONAL_MARKERS = [".bazelrc", ".bazelversion"];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect Bazel at repo root only.
|
|
32
|
+
* Does NOT scan subdirectories for BUILD files (avoids vendored deps false positives).
|
|
33
|
+
*
|
|
34
|
+
* @param repoRoot - Absolute path to repository root
|
|
35
|
+
* @returns Detection result with mode and found markers
|
|
36
|
+
*/
|
|
37
|
+
export function detectBazel(repoRoot: string): BazelDetectionResult {
|
|
38
|
+
const foundMarkers: string[] = [];
|
|
39
|
+
let mode: "bzlmod" | "workspace" | undefined;
|
|
40
|
+
|
|
41
|
+
// Check bzlmod first (preferred)
|
|
42
|
+
for (const marker of ROOT_MARKERS.bzlmod) {
|
|
43
|
+
if (fs.existsSync(path.join(repoRoot, marker))) {
|
|
44
|
+
foundMarkers.push(marker);
|
|
45
|
+
mode = "bzlmod";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check workspace if no bzlmod
|
|
50
|
+
if (!mode) {
|
|
51
|
+
for (const marker of ROOT_MARKERS.workspace) {
|
|
52
|
+
if (fs.existsSync(path.join(repoRoot, marker))) {
|
|
53
|
+
foundMarkers.push(marker);
|
|
54
|
+
mode = "workspace";
|
|
55
|
+
break; // Only need one workspace marker
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check optional markers (don't affect detection, just informational)
|
|
61
|
+
for (const marker of OPTIONAL_MARKERS) {
|
|
62
|
+
if (fs.existsSync(path.join(repoRoot, marker))) {
|
|
63
|
+
foundMarkers.push(marker);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
detected: mode !== undefined,
|
|
69
|
+
mode,
|
|
70
|
+
markers: foundMarkers,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// CLI entrypoint for manual testing
|
|
75
|
+
if (
|
|
76
|
+
import.meta.url.startsWith("file:") &&
|
|
77
|
+
process.argv[1]?.includes("detect-bazel")
|
|
78
|
+
) {
|
|
79
|
+
const targetDir = process.argv[2] || process.cwd();
|
|
80
|
+
const result = detectBazel(path.resolve(targetDir));
|
|
81
|
+
console.log(JSON.stringify(result, null, 2));
|
|
82
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// scripts/generate-instructions.ts
|
|
2
|
+
//
|
|
3
|
+
// Generates instructions.md from a stack+CI specific JSON file.
|
|
4
|
+
// Usage: npx ts-node-esm scripts/generate-instructions.ts [config-file]
|
|
5
|
+
// Example: npm run generate:instructions standards.python.github-actions.json
|
|
6
|
+
// Default: config/standards.typescript-js.github-actions.json
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
interface StackHints {
|
|
12
|
+
exampleTools?: string[];
|
|
13
|
+
exampleConfigFiles?: string[];
|
|
14
|
+
notes?: string;
|
|
15
|
+
verification?: string;
|
|
16
|
+
requiredFiles?: string[];
|
|
17
|
+
optionalFiles?: string[];
|
|
18
|
+
requiredScripts?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ChecklistItem {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description: string;
|
|
25
|
+
ciHints?: Record<string, { job?: string; stage?: string }>;
|
|
26
|
+
stack?: StackHints;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface StackChecklistJson {
|
|
30
|
+
version: number;
|
|
31
|
+
stack: string;
|
|
32
|
+
stackLabel: string;
|
|
33
|
+
ciSystems: string[];
|
|
34
|
+
checklist: {
|
|
35
|
+
core: ChecklistItem[];
|
|
36
|
+
recommended: ChecklistItem[];
|
|
37
|
+
optionalEnhancements: ChecklistItem[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate 2-5 high-level bullets from a checklist item.
|
|
43
|
+
*/
|
|
44
|
+
function generateBullets(item: ChecklistItem): string[] {
|
|
45
|
+
const bullets: string[] = [];
|
|
46
|
+
const stack = item.stack;
|
|
47
|
+
|
|
48
|
+
// Bullet 1: Core purpose from description
|
|
49
|
+
bullets.push(item.description);
|
|
50
|
+
|
|
51
|
+
// Bullet 2: Required files if any
|
|
52
|
+
if (stack?.requiredFiles?.length) {
|
|
53
|
+
const files = stack.requiredFiles.join(", ");
|
|
54
|
+
bullets.push(`Ensure ${files} exists in the repository.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bullet 3: Optional files mention
|
|
58
|
+
if (stack?.optionalFiles?.length) {
|
|
59
|
+
const files = stack.optionalFiles.slice(0, 3).join(", ");
|
|
60
|
+
const more = stack.optionalFiles.length > 3 ? " and others" : "";
|
|
61
|
+
bullets.push(`Consider adding ${files}${more} if applicable.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Bullet 4: Required scripts
|
|
65
|
+
if (stack?.requiredScripts?.length) {
|
|
66
|
+
const scripts = stack.requiredScripts.map((s) => `\`${s}\``).join(", ");
|
|
67
|
+
bullets.push(`Define a ${scripts} script or equivalent command.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Bullet 5: Notes-based guidance (if short enough)
|
|
71
|
+
if (stack?.notes && stack.notes.length < 150) {
|
|
72
|
+
bullets.push(stack.notes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Limit to 5 bullets
|
|
76
|
+
return bullets.slice(0, 5);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function generateSection(title: string, items: ChecklistItem[]): string {
|
|
80
|
+
if (items.length === 0) return "";
|
|
81
|
+
|
|
82
|
+
const lines: string[] = [];
|
|
83
|
+
lines.push(`## ${title}`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
lines.push(`### ${item.label}`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
|
|
90
|
+
const bullets = generateBullets(item);
|
|
91
|
+
for (const bullet of bullets) {
|
|
92
|
+
lines.push(`- ${bullet}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function main() {
|
|
101
|
+
const rootDir = process.cwd();
|
|
102
|
+
|
|
103
|
+
// Accept config file as CLI argument, default to TypeScript+GitHub Actions
|
|
104
|
+
const configArg = process.argv[2];
|
|
105
|
+
let inputPath: string;
|
|
106
|
+
|
|
107
|
+
if (configArg) {
|
|
108
|
+
// If argument provided, resolve relative to config/ or as absolute
|
|
109
|
+
if (path.isAbsolute(configArg)) {
|
|
110
|
+
inputPath = configArg;
|
|
111
|
+
} else if (
|
|
112
|
+
configArg.startsWith("config/") ||
|
|
113
|
+
configArg.startsWith("config\\")
|
|
114
|
+
) {
|
|
115
|
+
inputPath = path.join(rootDir, configArg);
|
|
116
|
+
} else {
|
|
117
|
+
inputPath = path.join(rootDir, "config", configArg);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
inputPath = path.join(
|
|
121
|
+
rootDir,
|
|
122
|
+
"config",
|
|
123
|
+
"standards.typescript-js.github-actions.json",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Derive output filename from input (e.g., standards.python.json -> instructions.python.md)
|
|
128
|
+
const inputBasename = path.basename(inputPath, ".json");
|
|
129
|
+
const outputBasename = inputBasename.replace(/^standards\./, "instructions.");
|
|
130
|
+
const outputPath = path.join(rootDir, `${outputBasename}.md`);
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(inputPath)) {
|
|
133
|
+
console.error(`Input file not found: ${inputPath}`);
|
|
134
|
+
console.error("Usage: npm run generate:instructions [config-file]");
|
|
135
|
+
console.error(
|
|
136
|
+
"Example: npm run generate:instructions standards.python.github-actions.json",
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const raw = fs.readFileSync(inputPath, "utf8");
|
|
142
|
+
const data: StackChecklistJson = JSON.parse(raw);
|
|
143
|
+
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
|
|
146
|
+
// Header
|
|
147
|
+
lines.push("# Repository Standards Instructions");
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(`> Auto-generated from \`${path.relative(rootDir, inputPath)}\``);
|
|
150
|
+
lines.push(`> Stack: ${data.stackLabel} | CI: ${data.ciSystems.join(", ")}`);
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push(
|
|
153
|
+
"This document provides high-level guidance for an autonomous coding agent to bring a repository into compliance with the defined standards.",
|
|
154
|
+
);
|
|
155
|
+
lines.push("");
|
|
156
|
+
|
|
157
|
+
// Generate sections
|
|
158
|
+
lines.push(generateSection("Core Requirements", data.checklist.core));
|
|
159
|
+
lines.push(
|
|
160
|
+
generateSection("Recommended Practices", data.checklist.recommended),
|
|
161
|
+
);
|
|
162
|
+
lines.push(
|
|
163
|
+
generateSection(
|
|
164
|
+
"Optional Enhancements",
|
|
165
|
+
data.checklist.optionalEnhancements,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Write output
|
|
170
|
+
fs.writeFileSync(outputPath, lines.join("\n"));
|
|
171
|
+
console.log(`Wrote ${outputPath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// scripts/generate-standards.ts
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
type StackId = "typescript-js" | "csharp-dotnet" | "python" | "rust" | "go";
|
|
7
|
+
type CiSystem = "azure-devops" | "github-actions";
|
|
8
|
+
|
|
9
|
+
interface StackMeta {
|
|
10
|
+
label: string;
|
|
11
|
+
languageFamily: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CiHintsForSystem {
|
|
15
|
+
stage?: string;
|
|
16
|
+
job?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CiHints = Partial<Record<CiSystem, CiHintsForSystem>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bazel execution hints for individual checklist items.
|
|
23
|
+
* Commands are actual Bazel invocations (e.g., "bazel test //..."),
|
|
24
|
+
* NOT assumed pattern labels.
|
|
25
|
+
*/
|
|
26
|
+
interface BazelHints {
|
|
27
|
+
/** Bazel commands to run (e.g., "bazel test //...", "bazel run //tools/lint") */
|
|
28
|
+
commands?: string[];
|
|
29
|
+
/** Recommended target conventions (documentation only, not assumed to exist) */
|
|
30
|
+
recommendedTargets?: string[];
|
|
31
|
+
/** Usage notes for this check in Bazel context */
|
|
32
|
+
notes?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface StackHints {
|
|
36
|
+
exampleTools?: string[];
|
|
37
|
+
exampleConfigFiles?: string[];
|
|
38
|
+
notes?: string;
|
|
39
|
+
// Human‑readable verification instructions
|
|
40
|
+
verification?: string;
|
|
41
|
+
// Machine‑readable additions (all optional)
|
|
42
|
+
requiredFiles?: string[];
|
|
43
|
+
optionalFiles?: string[];
|
|
44
|
+
requiredScripts?: string[];
|
|
45
|
+
// Either-or file compliance: at least one of these files must exist
|
|
46
|
+
anyOfFiles?: string[];
|
|
47
|
+
// Version pinning guidance for deterministic CI
|
|
48
|
+
pinningNotes?: string;
|
|
49
|
+
machineCheck?: {
|
|
50
|
+
command: string;
|
|
51
|
+
expectExitCode?: number;
|
|
52
|
+
description?: string;
|
|
53
|
+
};
|
|
54
|
+
// Bazel execution hints (v3+)
|
|
55
|
+
bazelHints?: BazelHints;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ChecklistItemMaster {
|
|
59
|
+
id: string;
|
|
60
|
+
label: string;
|
|
61
|
+
description: string;
|
|
62
|
+
appliesTo: {
|
|
63
|
+
stacks: StackId[];
|
|
64
|
+
ciSystems?: CiSystem[];
|
|
65
|
+
};
|
|
66
|
+
ciHints?: CiHints;
|
|
67
|
+
stackHints?: Partial<Record<StackId, StackHints>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ChecklistSection {
|
|
71
|
+
core: ChecklistItemMaster[];
|
|
72
|
+
recommended: ChecklistItemMaster[];
|
|
73
|
+
optionalEnhancements: ChecklistItemMaster[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface MigrationStep {
|
|
77
|
+
step: number;
|
|
78
|
+
title: string;
|
|
79
|
+
description: string;
|
|
80
|
+
focusIds?: string[];
|
|
81
|
+
notes?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Meta {
|
|
85
|
+
defaultCoverageThreshold?: number;
|
|
86
|
+
complexityChecks?: {
|
|
87
|
+
enabledByDefault?: boolean;
|
|
88
|
+
description?: string;
|
|
89
|
+
};
|
|
90
|
+
qualityGatePolicy?: {
|
|
91
|
+
preferSoftFailOnLegacy?: boolean;
|
|
92
|
+
description?: string;
|
|
93
|
+
};
|
|
94
|
+
migrationGuide?: MigrationStep[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface MasterJson {
|
|
98
|
+
version: number;
|
|
99
|
+
meta?: Meta;
|
|
100
|
+
ciSystems: CiSystem[];
|
|
101
|
+
stacks: Record<StackId, StackMeta>;
|
|
102
|
+
checklist: ChecklistSection;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface StackItem {
|
|
106
|
+
id: string;
|
|
107
|
+
label: string;
|
|
108
|
+
description: string;
|
|
109
|
+
ciHints?: CiHints;
|
|
110
|
+
// For the filtered file, this is the single stack’s hints including verification
|
|
111
|
+
stack?: StackHints;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface StackChecklistJson {
|
|
115
|
+
version: number;
|
|
116
|
+
stack: StackId;
|
|
117
|
+
stackLabel: string;
|
|
118
|
+
ciSystems: CiSystem[];
|
|
119
|
+
meta?: Meta;
|
|
120
|
+
checklist: {
|
|
121
|
+
core: StackItem[];
|
|
122
|
+
recommended: StackItem[];
|
|
123
|
+
optionalEnhancements: StackItem[];
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function filterSectionForStackAndCi(
|
|
128
|
+
items: ChecklistItemMaster[],
|
|
129
|
+
stack: StackId,
|
|
130
|
+
ciSystem?: CiSystem,
|
|
131
|
+
): StackItem[] {
|
|
132
|
+
return items
|
|
133
|
+
.filter((item) => {
|
|
134
|
+
if (!item.appliesTo.stacks.includes(stack)) return false;
|
|
135
|
+
|
|
136
|
+
if (ciSystem && item.appliesTo.ciSystems) {
|
|
137
|
+
return item.appliesTo.ciSystems.includes(ciSystem);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
})
|
|
141
|
+
.map((item) => {
|
|
142
|
+
const stackHint = item.stackHints?.[stack];
|
|
143
|
+
|
|
144
|
+
const result: StackItem = {
|
|
145
|
+
id: item.id,
|
|
146
|
+
label: item.label,
|
|
147
|
+
description: item.description,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (item.ciHints) {
|
|
151
|
+
if (ciSystem) {
|
|
152
|
+
const perSystem = item.ciHints[ciSystem];
|
|
153
|
+
if (perSystem) {
|
|
154
|
+
result.ciHints = { [ciSystem]: perSystem };
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
result.ciHints = item.ciHints;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (stackHint) {
|
|
162
|
+
// Includes exampleTools, exampleConfigFiles, notes, verification
|
|
163
|
+
result.stack = stackHint;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function generateStackJson(
|
|
171
|
+
master: MasterJson,
|
|
172
|
+
stack: StackId,
|
|
173
|
+
ciSystem?: CiSystem,
|
|
174
|
+
): StackChecklistJson {
|
|
175
|
+
const stackMeta = master.stacks[stack];
|
|
176
|
+
const ciSystems = ciSystem ? [ciSystem] : master.ciSystems;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
version: master.version,
|
|
180
|
+
stack,
|
|
181
|
+
stackLabel: stackMeta?.label ?? stack,
|
|
182
|
+
ciSystems,
|
|
183
|
+
meta: master.meta,
|
|
184
|
+
checklist: {
|
|
185
|
+
core: filterSectionForStackAndCi(master.checklist.core, stack, ciSystem),
|
|
186
|
+
recommended: filterSectionForStackAndCi(
|
|
187
|
+
master.checklist.recommended,
|
|
188
|
+
stack,
|
|
189
|
+
ciSystem,
|
|
190
|
+
),
|
|
191
|
+
optionalEnhancements: filterSectionForStackAndCi(
|
|
192
|
+
master.checklist.optionalEnhancements,
|
|
193
|
+
stack,
|
|
194
|
+
ciSystem,
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- entrypoint ---
|
|
201
|
+
const rootDir = path.join(process.cwd());
|
|
202
|
+
const masterPath = path.join(rootDir, "config", "standards.json");
|
|
203
|
+
|
|
204
|
+
const raw = fs.readFileSync(masterPath, "utf8");
|
|
205
|
+
const master: MasterJson = JSON.parse(raw);
|
|
206
|
+
|
|
207
|
+
// args: stack [ciSystem]
|
|
208
|
+
// args: stack [ciSystem]
|
|
209
|
+
const STACK_ALIASES: Record<string, StackId> = {
|
|
210
|
+
dotnet: "csharp-dotnet",
|
|
211
|
+
csharp: "csharp-dotnet",
|
|
212
|
+
ts: "typescript-js",
|
|
213
|
+
js: "typescript-js",
|
|
214
|
+
python: "python",
|
|
215
|
+
py: "python",
|
|
216
|
+
"typescript-js": "typescript-js",
|
|
217
|
+
"csharp-dotnet": "csharp-dotnet",
|
|
218
|
+
rust: "rust",
|
|
219
|
+
rs: "rust",
|
|
220
|
+
go: "go",
|
|
221
|
+
golang: "go",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const rawArg = process.argv[2] || "typescript-js";
|
|
225
|
+
const targetStack = STACK_ALIASES[rawArg.toLowerCase()];
|
|
226
|
+
|
|
227
|
+
if (!targetStack) {
|
|
228
|
+
console.error(`Unknown stack: ${rawArg}`);
|
|
229
|
+
console.error(
|
|
230
|
+
`Available stacks: ${["typescript-js", "csharp-dotnet", "python", "rust", "go"].join(", ")}`,
|
|
231
|
+
);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const targetCiSystem = process.argv[3] as CiSystem | undefined;
|
|
236
|
+
|
|
237
|
+
const stackJson = generateStackJson(master, targetStack, targetCiSystem);
|
|
238
|
+
|
|
239
|
+
const outDir = path.join(rootDir, "config");
|
|
240
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
241
|
+
|
|
242
|
+
const ciSuffix = targetCiSystem ? `.${targetCiSystem}` : "";
|
|
243
|
+
const outPath = path.join(outDir, `standards.${targetStack}${ciSuffix}.json`);
|
|
244
|
+
|
|
245
|
+
fs.writeFileSync(outPath, JSON.stringify(stackJson, null, 2) + "\n");
|
|
246
|
+
|
|
247
|
+
console.log(`Wrote ${outPath}`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syncs manifest.json version with package.json
|
|
3
|
+
* Called by semantic-release exec plugin during release
|
|
4
|
+
*
|
|
5
|
+
* @see https://github.com/semantic-release/exec
|
|
6
|
+
*/
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
12
|
+
const manifestPath = path.join(process.cwd(), "manifest.json");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(manifestPath)) {
|
|
15
|
+
console.log("[sync-manifest] No manifest.json found, skipping");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
20
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
21
|
+
|
|
22
|
+
manifest.version = pkg.version;
|
|
23
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
24
|
+
|
|
25
|
+
// Run prettier to ensure consistent formatting
|
|
26
|
+
try {
|
|
27
|
+
execSync("npx prettier --write manifest.json", { stdio: "inherit" });
|
|
28
|
+
} catch {
|
|
29
|
+
console.log("[sync-manifest] prettier not available, skipping format");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`[sync-manifest] Updated manifest.json to ${pkg.version}`);
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// scripts/validate-schema.ts
|
|
2
|
+
// Validates standards.json against JSON Schema and performs additional semantic checks
|
|
3
|
+
|
|
4
|
+
import Ajv, { ErrorObject } from "ajv";
|
|
5
|
+
import addFormats from "ajv-formats";
|
|
6
|
+
import stableStringify from "fast-json-stable-stringify";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
|
+
const configPath = path.join(rootDir, "config", "standards.json");
|
|
12
|
+
const schemaPath = path.join(rootDir, "config", "standards.schema.json");
|
|
13
|
+
|
|
14
|
+
interface ValidationResult {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ChecklistItem {
|
|
20
|
+
id: string;
|
|
21
|
+
appliesTo?: { stacks?: string[]; ciSystems?: string[] };
|
|
22
|
+
ciHints?: Record<string, unknown>;
|
|
23
|
+
stackHints?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MigrationStep {
|
|
27
|
+
focusIds?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Config {
|
|
31
|
+
version: number;
|
|
32
|
+
ciSystems: string[];
|
|
33
|
+
stacks: Record<string, unknown>;
|
|
34
|
+
meta?: {
|
|
35
|
+
defaultCoverageThreshold?: number;
|
|
36
|
+
coverageThresholdUnit?: string;
|
|
37
|
+
migrationGuide?: MigrationStep[];
|
|
38
|
+
};
|
|
39
|
+
checklist: {
|
|
40
|
+
core: ChecklistItem[];
|
|
41
|
+
recommended: ChecklistItem[];
|
|
42
|
+
optionalEnhancements: ChecklistItem[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate config against JSON Schema using Ajv
|
|
48
|
+
*/
|
|
49
|
+
function validateSchema(config: unknown, schema: unknown): ValidationResult {
|
|
50
|
+
const ajv = new Ajv.default({ allErrors: true, strict: true });
|
|
51
|
+
addFormats.default(ajv);
|
|
52
|
+
|
|
53
|
+
const validate = ajv.compile(schema as object);
|
|
54
|
+
const valid = validate(config);
|
|
55
|
+
|
|
56
|
+
if (!valid && validate.errors) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
errors: validate.errors.map(
|
|
60
|
+
(e: ErrorObject) =>
|
|
61
|
+
`${e.instancePath || "/"}: ${e.message} (${JSON.stringify(e.params)})`,
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { valid: true, errors: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate that all checklist IDs are unique across all sections
|
|
71
|
+
*/
|
|
72
|
+
function validateUniqueIds(config: Config): ValidationResult {
|
|
73
|
+
const allItems = [
|
|
74
|
+
...config.checklist.core,
|
|
75
|
+
...config.checklist.recommended,
|
|
76
|
+
...config.checklist.optionalEnhancements,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const ids = allItems.map((item) => item.id);
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
const duplicates: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const id of ids) {
|
|
84
|
+
if (seen.has(id)) {
|
|
85
|
+
duplicates.push(id);
|
|
86
|
+
}
|
|
87
|
+
seen.add(id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (duplicates.length > 0) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
errors: [`Duplicate checklist IDs found: ${duplicates.join(", ")}`],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { valid: true, errors: [] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate that migrationGuide focusIds reference existing checklist IDs
|
|
102
|
+
*/
|
|
103
|
+
function validateFocusIdReferences(config: Config): ValidationResult {
|
|
104
|
+
const allIds = new Set([
|
|
105
|
+
...config.checklist.core.map((i) => i.id),
|
|
106
|
+
...config.checklist.recommended.map((i) => i.id),
|
|
107
|
+
...config.checklist.optionalEnhancements.map((i) => i.id),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const errors: string[] = [];
|
|
111
|
+
const migrationGuide = config.meta?.migrationGuide ?? [];
|
|
112
|
+
|
|
113
|
+
for (const step of migrationGuide) {
|
|
114
|
+
for (const focusId of step.focusIds ?? []) {
|
|
115
|
+
if (!allIds.has(focusId)) {
|
|
116
|
+
errors.push(
|
|
117
|
+
`migrationGuide focusId "${focusId}" does not reference a valid checklist ID`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { valid: errors.length === 0, errors };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate that appliesTo.stacks only references known stack keys
|
|
128
|
+
*/
|
|
129
|
+
function validateStackReferences(config: Config): ValidationResult {
|
|
130
|
+
const validStacks = new Set(Object.keys(config.stacks));
|
|
131
|
+
const errors: string[] = [];
|
|
132
|
+
|
|
133
|
+
const allItems = [
|
|
134
|
+
...config.checklist.core,
|
|
135
|
+
...config.checklist.recommended,
|
|
136
|
+
...config.checklist.optionalEnhancements,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const item of allItems) {
|
|
140
|
+
for (const stack of item.appliesTo?.stacks ?? []) {
|
|
141
|
+
if (!validStacks.has(stack)) {
|
|
142
|
+
errors.push(
|
|
143
|
+
`Item "${item.id}" references unknown stack "${stack}" in appliesTo.stacks`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { valid: errors.length === 0, errors };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate that ciHints keys are a subset of ciSystems
|
|
154
|
+
*/
|
|
155
|
+
function validateCiHintKeys(config: Config): ValidationResult {
|
|
156
|
+
const validCiSystems = new Set(config.ciSystems);
|
|
157
|
+
const errors: string[] = [];
|
|
158
|
+
|
|
159
|
+
const allItems = [
|
|
160
|
+
...config.checklist.core,
|
|
161
|
+
...config.checklist.recommended,
|
|
162
|
+
...config.checklist.optionalEnhancements,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const item of allItems) {
|
|
166
|
+
for (const ciKey of Object.keys(item.ciHints ?? {})) {
|
|
167
|
+
if (!validCiSystems.has(ciKey)) {
|
|
168
|
+
errors.push(
|
|
169
|
+
`Item "${item.id}" has ciHints key "${ciKey}" not in ciSystems`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { valid: errors.length === 0, errors };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate coverage threshold semantics: if unit is "ratio", threshold must be 0-1
|
|
180
|
+
*/
|
|
181
|
+
function validateCoverageThreshold(config: Config): ValidationResult {
|
|
182
|
+
const threshold = config.meta?.defaultCoverageThreshold;
|
|
183
|
+
const unit = config.meta?.coverageThresholdUnit;
|
|
184
|
+
|
|
185
|
+
if (unit === "ratio" && threshold !== undefined) {
|
|
186
|
+
if (threshold < 0 || threshold > 1) {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
errors: [
|
|
190
|
+
`defaultCoverageThreshold is ${threshold} but coverageThresholdUnit is "ratio" (must be 0-1)`,
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { valid: true, errors: [] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate a normalized, deterministic string representation of the config.
|
|
201
|
+
* Uses deep stable key ordering at all depths.
|
|
202
|
+
*/
|
|
203
|
+
export function normalizeConfig(config: unknown): string {
|
|
204
|
+
return stableStringify(config);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Run all validations and return combined result
|
|
209
|
+
*/
|
|
210
|
+
export function validateStandardsConfig(
|
|
211
|
+
configRaw: string,
|
|
212
|
+
schemaRaw: string,
|
|
213
|
+
): ValidationResult {
|
|
214
|
+
let config: Config;
|
|
215
|
+
let schema: unknown;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
config = JSON.parse(configRaw);
|
|
219
|
+
} catch {
|
|
220
|
+
return { valid: false, errors: ["Failed to parse standards.json as JSON"] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
schema = JSON.parse(schemaRaw);
|
|
225
|
+
} catch {
|
|
226
|
+
return {
|
|
227
|
+
valid: false,
|
|
228
|
+
errors: ["Failed to parse standards.schema.json as JSON"],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const results: ValidationResult[] = [
|
|
233
|
+
validateSchema(config, schema),
|
|
234
|
+
validateUniqueIds(config),
|
|
235
|
+
validateFocusIdReferences(config),
|
|
236
|
+
validateStackReferences(config),
|
|
237
|
+
validateCiHintKeys(config),
|
|
238
|
+
validateCoverageThreshold(config),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const allErrors = results.flatMap((r) => r.errors);
|
|
242
|
+
return {
|
|
243
|
+
valid: allErrors.length === 0,
|
|
244
|
+
errors: allErrors,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Main entry point for CLI usage
|
|
250
|
+
*/
|
|
251
|
+
export function validateStandardsSchema(): void {
|
|
252
|
+
if (!fs.existsSync(configPath)) {
|
|
253
|
+
console.error(`Config file not found: ${configPath}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(schemaPath)) {
|
|
258
|
+
console.error(`Schema file not found: ${schemaPath}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const configRaw = fs.readFileSync(configPath, "utf8");
|
|
263
|
+
const schemaRaw = fs.readFileSync(schemaPath, "utf8");
|
|
264
|
+
|
|
265
|
+
const result = validateStandardsConfig(configRaw, schemaRaw);
|
|
266
|
+
|
|
267
|
+
if (!result.valid) {
|
|
268
|
+
console.error("Schema validation failed:");
|
|
269
|
+
for (const error of result.errors) {
|
|
270
|
+
console.error(` - ${error}`);
|
|
271
|
+
}
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log("✓ Schema validation passed");
|
|
276
|
+
console.log("✓ All checklist IDs are unique");
|
|
277
|
+
console.log("✓ All migrationGuide focusIds reference valid IDs");
|
|
278
|
+
console.log("✓ All appliesTo.stacks reference valid stack keys");
|
|
279
|
+
console.log("✓ All ciHints keys are valid ciSystems");
|
|
280
|
+
console.log("✓ Coverage threshold semantics are valid");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// CLI entry point
|
|
284
|
+
if (
|
|
285
|
+
import.meta.url.startsWith("file:") &&
|
|
286
|
+
process.argv[1]?.includes("validate-schema")
|
|
287
|
+
) {
|
|
288
|
+
validateStandardsSchema();
|
|
289
|
+
}
|