@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.
- package/dist/cli.cjs +116 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +13 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +86 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1 -7
- 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 -7
- package/dist/index.js.map +1 -1
- package/package.json +9 -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
|
@@ -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
|
+
}
|