@oddessentials/repo-standards 4.1.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.
@@ -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
+ }