gh-inari 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/artifact.d.ts +82 -0
  4. package/dist/artifact.js +472 -0
  5. package/dist/artifact.js.map +1 -0
  6. package/dist/cli.d.ts +14 -0
  7. package/dist/cli.js +395 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/contract/index.d.ts +4 -0
  10. package/dist/contract/index.js +5 -0
  11. package/dist/contract/index.js.map +1 -0
  12. package/dist/contract/ir.d.ts +178 -0
  13. package/dist/contract/ir.js +1071 -0
  14. package/dist/contract/ir.js.map +1 -0
  15. package/dist/contract/issue-form.d.ts +38 -0
  16. package/dist/contract/issue-form.js +662 -0
  17. package/dist/contract/issue-form.js.map +1 -0
  18. package/dist/contract/schema.d.ts +48 -0
  19. package/dist/contract/schema.js +162 -0
  20. package/dist/contract/schema.js.map +1 -0
  21. package/dist/contract/validation.d.ts +24 -0
  22. package/dist/contract/validation.js +212 -0
  23. package/dist/contract/validation.js.map +1 -0
  24. package/dist/github/adapter.d.ts +48 -0
  25. package/dist/github/adapter.js +463 -0
  26. package/dist/github/adapter.js.map +1 -0
  27. package/dist/github/errors.d.ts +43 -0
  28. package/dist/github/errors.js +65 -0
  29. package/dist/github/errors.js.map +1 -0
  30. package/dist/github/index.d.ts +4 -0
  31. package/dist/github/index.js +5 -0
  32. package/dist/github/index.js.map +1 -0
  33. package/dist/github/transport.d.ts +19 -0
  34. package/dist/github/transport.js +54 -0
  35. package/dist/github/transport.js.map +1 -0
  36. package/dist/github/types.d.ts +62 -0
  37. package/dist/github/types.js +16 -0
  38. package/dist/github/types.js.map +1 -0
  39. package/dist/github.d.ts +1 -0
  40. package/dist/github.js +2 -0
  41. package/dist/github.js.map +1 -0
  42. package/dist/index.d.ts +10 -0
  43. package/dist/index.js +32 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/pr-policy.d.ts +39 -0
  46. package/dist/pr-policy.js +313 -0
  47. package/dist/pr-policy.js.map +1 -0
  48. package/dist/pull-request-template.d.ts +42 -0
  49. package/dist/pull-request-template.js +481 -0
  50. package/dist/pull-request-template.js.map +1 -0
  51. package/dist/template-discovery.d.ts +68 -0
  52. package/dist/template-discovery.js +431 -0
  53. package/dist/template-discovery.js.map +1 -0
  54. package/gh-inari +18 -0
  55. package/package.json +92 -0
@@ -0,0 +1,1071 @@
1
+ /**
2
+ * Versioned, compiler-generated contract representation.
3
+ *
4
+ * This module deliberately describes the result of compiling a repository's
5
+ * native template. It is not a second template language for repository
6
+ * authors to maintain.
7
+ */
8
+ export const CANONICAL_IR_VERSION = "1.0.0";
9
+ export const CONTRACT_SCHEMA_VERSION = "1.0.0";
10
+ export const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
11
+ /** The only built-in linked-Issue rule accepted by the v1 PR overlay. */
12
+ export const LINKED_ISSUE_PATTERN = "(?:Closes|Fixes|Resolves)\\s+#\\d+";
13
+ export class CanonicalIrValidationError extends Error {
14
+ violations;
15
+ constructor(violations) {
16
+ super(violations.map((violation) => `${violation.path}: ${violation.message}`).join("\n"));
17
+ this.name = "CanonicalIrValidationError";
18
+ this.violations = violations;
19
+ }
20
+ }
21
+ const requiredStates = ["required", "optional", "unknown"];
22
+ const fieldTypes = ["string", "enum", "array", "checklist"];
23
+ const templateSources = ["issue_form", "pull_request_template"];
24
+ const sectionKinds = ["input", "documentation"];
25
+ const arraySelections = ["list", "multi_select"];
26
+ const nativeSectionElements = [
27
+ "input",
28
+ "textarea",
29
+ "dropdown",
30
+ "checkboxes",
31
+ "markdown",
32
+ "heading",
33
+ ];
34
+ const identifierPattern = /^[A-Za-z][A-Za-z0-9_-]*$/u;
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function hasOwn(record, key) {
39
+ return Object.prototype.hasOwnProperty.call(record, key);
40
+ }
41
+ function addViolation(violations, code, path, message) {
42
+ violations.push({ code, path, message });
43
+ }
44
+ function checkUnknownKeys(record, allowedKeys, path, violations) {
45
+ const allowed = new Set(allowedKeys);
46
+ for (const key of Object.keys(record)) {
47
+ if (!allowed.has(key)) {
48
+ addViolation(violations, "IR_UNKNOWN_PROPERTY", `${path}.${key}`, `Property "${key}" is not supported.`);
49
+ }
50
+ }
51
+ }
52
+ function requiredString(record, key, path, violations) {
53
+ if (!hasOwn(record, key)) {
54
+ addViolation(violations, "IR_MISSING_PROPERTY", `${path}.${key}`, `Property "${key}" is required.`);
55
+ return undefined;
56
+ }
57
+ const value = record[key];
58
+ if (typeof value !== "string" || value.trim().length === 0) {
59
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be a non-empty string.`);
60
+ return undefined;
61
+ }
62
+ return value;
63
+ }
64
+ function optionalString(record, key, path, violations) {
65
+ if (!hasOwn(record, key))
66
+ return undefined;
67
+ const value = record[key];
68
+ if (typeof value !== "string") {
69
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be a string when present.`);
70
+ return undefined;
71
+ }
72
+ return value;
73
+ }
74
+ function optionalBoolean(record, key, path, violations) {
75
+ if (!hasOwn(record, key))
76
+ return undefined;
77
+ const value = record[key];
78
+ if (typeof value !== "boolean") {
79
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be a boolean when present.`);
80
+ return undefined;
81
+ }
82
+ return value;
83
+ }
84
+ function optionalStringArray(record, key, path, violations) {
85
+ if (!hasOwn(record, key))
86
+ return undefined;
87
+ const value = record[key];
88
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
89
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be an array of strings.`);
90
+ return undefined;
91
+ }
92
+ return value;
93
+ }
94
+ function requiredArray(record, key, path, violations) {
95
+ if (!hasOwn(record, key)) {
96
+ addViolation(violations, "IR_MISSING_PROPERTY", `${path}.${key}`, `Property "${key}" is required.`);
97
+ return undefined;
98
+ }
99
+ const value = record[key];
100
+ if (!Array.isArray(value)) {
101
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be an array.`);
102
+ return undefined;
103
+ }
104
+ return value;
105
+ }
106
+ function requiredRecord(record, key, path, violations) {
107
+ if (!hasOwn(record, key)) {
108
+ addViolation(violations, "IR_MISSING_PROPERTY", `${path}.${key}`, `Property "${key}" is required.`);
109
+ return undefined;
110
+ }
111
+ const value = record[key];
112
+ if (!isRecord(value)) {
113
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.${key}`, `Property "${key}" must be an object.`);
114
+ return undefined;
115
+ }
116
+ return value;
117
+ }
118
+ function validateIdentifier(value, path, violations) {
119
+ if (value !== undefined && !identifierPattern.test(value)) {
120
+ addViolation(violations, "IR_INVALID_IDENTIFIER", path, "Identifiers must start with a letter and contain only letters, numbers, hyphens, or underscores.");
121
+ }
122
+ }
123
+ function validateEnumValue(value, path, violations) {
124
+ if (typeof value !== "string" || value.length === 0) {
125
+ addViolation(violations, "IR_INVALID_OPTIONS", path, "Option values must be non-empty strings.");
126
+ return undefined;
127
+ }
128
+ return value;
129
+ }
130
+ function validateOptionalDefault(record, path, violations) {
131
+ return hasOwn(record, "defaultValue") ? record.defaultValue : undefined;
132
+ }
133
+ function validateRenderMetadata(value, path, expectedOrder, violations, allowHeadingLevel) {
134
+ if (!isRecord(value)) {
135
+ addViolation(violations, "IR_INVALID_ORDER", path, "Render metadata must be an object.");
136
+ return;
137
+ }
138
+ checkUnknownKeys(value, allowHeadingLevel ? ["order", "headingLevel"] : ["order"], path, violations);
139
+ if (!hasOwn(value, "order") || !Number.isSafeInteger(value.order) || value.order !== expectedOrder) {
140
+ addViolation(violations, "IR_INVALID_ORDER", `${path}.order`, `Render order must be the zero-based position ${expectedOrder}.`);
141
+ }
142
+ if (allowHeadingLevel && hasOwn(value, "headingLevel")) {
143
+ if (typeof value.headingLevel !== "number" ||
144
+ !Number.isSafeInteger(value.headingLevel) ||
145
+ value.headingLevel < 1 ||
146
+ value.headingLevel > 6) {
147
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.headingLevel`, "Heading level must be an integer from 1 to 6.");
148
+ }
149
+ }
150
+ }
151
+ function validateNativeOption(value, path, violations) {
152
+ if (!isRecord(value)) {
153
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", path, "Native options must be objects.");
154
+ return undefined;
155
+ }
156
+ checkUnknownKeys(value, ["value", "label", "description", "required"], path, violations);
157
+ const optionValue = requiredString(value, "value", path, violations);
158
+ const label = optionalString(value, "label", path, violations);
159
+ const description = optionalString(value, "description", path, violations);
160
+ const required = optionalBoolean(value, "required", path, violations);
161
+ return optionValue === undefined
162
+ ? undefined
163
+ : {
164
+ value: optionValue,
165
+ ...(label === undefined ? {} : { label }),
166
+ ...(description === undefined ? {} : { description }),
167
+ ...(required === undefined ? {} : { required }),
168
+ };
169
+ }
170
+ function validateNativeOptions(value, path, violations) {
171
+ if (!Array.isArray(value)) {
172
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", path, "Native options must be an array.");
173
+ return undefined;
174
+ }
175
+ const options = [];
176
+ const values = new Set();
177
+ value.forEach((entry, index) => {
178
+ const option = validateNativeOption(entry, `${path}[${index}]`, violations);
179
+ if (option !== undefined) {
180
+ if (values.has(option.value)) {
181
+ addViolation(violations, "IR_DUPLICATE_ID", `${path}[${index}].value`, `Duplicate native option "${option.value}".`);
182
+ }
183
+ values.add(option.value);
184
+ options.push(option);
185
+ }
186
+ });
187
+ return options;
188
+ }
189
+ function validateNativeSectionMetadata(value, path, source, kind, violations) {
190
+ if (value === undefined) {
191
+ addViolation(violations, "IR_MISSING_PROPERTY", path, 'Property "nativeMetadata" is required.');
192
+ return;
193
+ }
194
+ if (!isRecord(value)) {
195
+ addViolation(violations, "IR_INVALID_VALUE", path, 'Property "nativeMetadata" must be an object.');
196
+ return;
197
+ }
198
+ const metadata = value;
199
+ checkUnknownKeys(metadata, ["elementType", "sourceId", "headingLevel", "markdown"], path, violations);
200
+ const elementType = requiredString(metadata, "elementType", path, violations);
201
+ const sourceId = optionalString(metadata, "sourceId", path, violations);
202
+ const markdown = optionalString(metadata, "markdown", path, violations);
203
+ if (sourceId !== undefined)
204
+ validateIdentifier(sourceId, `${path}.sourceId`, violations);
205
+ if (elementType !== undefined && !nativeSectionElements.includes(elementType)) {
206
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.elementType`, `Native section type "${elementType}" is not supported.`);
207
+ }
208
+ if (source === "issue_form") {
209
+ if (kind === "documentation" && elementType !== "markdown") {
210
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "Issue Form documentation sections must use native markdown metadata.");
211
+ }
212
+ if (kind === "input" && elementType === "markdown") {
213
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "Issue Form input sections cannot use native markdown metadata.");
214
+ }
215
+ }
216
+ else {
217
+ if (kind === "documentation" && elementType !== "markdown") {
218
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "PR documentation sections must use native markdown metadata.");
219
+ }
220
+ if (kind === "input" && elementType !== "heading") {
221
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "PR input sections must use native heading metadata.");
222
+ }
223
+ }
224
+ if (kind === "documentation" && (markdown === undefined || markdown.length === 0)) {
225
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.markdown`, "Documentation metadata must preserve its markdown content.");
226
+ }
227
+ if (kind === "input" && markdown !== undefined) {
228
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.markdown`, "Input sections cannot contain native markdown content.");
229
+ }
230
+ if (hasOwn(metadata, "headingLevel")) {
231
+ if (typeof metadata.headingLevel !== "number" ||
232
+ !Number.isSafeInteger(metadata.headingLevel) ||
233
+ metadata.headingLevel < 1 ||
234
+ metadata.headingLevel > 6) {
235
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.headingLevel`, "Heading level must be an integer from 1 to 6.");
236
+ }
237
+ if (elementType !== "heading") {
238
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", `${path}.headingLevel`, "Heading level is only valid for native headings.");
239
+ }
240
+ }
241
+ }
242
+ function validateNativeFieldMetadata(value, path, source, fieldType, violations) {
243
+ if (value === undefined) {
244
+ addViolation(violations, "IR_MISSING_PROPERTY", path, 'Property "nativeMetadata" is required.');
245
+ return undefined;
246
+ }
247
+ if (!isRecord(value)) {
248
+ addViolation(violations, "IR_INVALID_VALUE", path, 'Property "nativeMetadata" must be an object.');
249
+ return undefined;
250
+ }
251
+ const metadata = value;
252
+ checkUnknownKeys(metadata, ["elementType", "sourceId", "placeholder", "defaultValue", "multiple", "options"], path, violations);
253
+ const elementType = requiredString(metadata, "elementType", path, violations);
254
+ const sourceId = optionalString(metadata, "sourceId", path, violations);
255
+ const placeholder = optionalString(metadata, "placeholder", path, violations);
256
+ const defaultValue = hasOwn(metadata, "defaultValue") ? metadata.defaultValue : undefined;
257
+ const multiple = optionalBoolean(metadata, "multiple", path, violations);
258
+ const options = hasOwn(metadata, "options")
259
+ ? validateNativeOptions(metadata.options, `${path}.options`, violations)
260
+ : undefined;
261
+ if (sourceId !== undefined)
262
+ validateIdentifier(sourceId, `${path}.sourceId`, violations);
263
+ if (source === "issue_form" && elementType === "pr_section") {
264
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "Issue Form fields cannot use PR section metadata.");
265
+ }
266
+ if (source === "pull_request_template" && elementType !== "pr_section") {
267
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", path, "PR fields must use native PR section metadata.");
268
+ }
269
+ if (multiple !== undefined && elementType !== "dropdown") {
270
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}.multiple`, "The multiple flag is only valid for native dropdowns.");
271
+ }
272
+ const expectedElements = {
273
+ string: source === "issue_form" ? ["input", "textarea"] : ["pr_section"],
274
+ enum: source === "issue_form" ? ["dropdown"] : ["pr_section"],
275
+ array: source === "issue_form" ? ["dropdown"] : ["pr_section"],
276
+ checklist: source === "issue_form" ? ["checkboxes"] : ["pr_section"],
277
+ };
278
+ if (elementType !== undefined && !expectedElements[fieldType].includes(elementType)) {
279
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}.elementType`, `Native element type "${elementType}" cannot represent a ${fieldType} field.`);
280
+ }
281
+ if (defaultValue !== undefined && typeof defaultValue !== "string" && !Array.isArray(defaultValue)) {
282
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.defaultValue`, "Native default must be a string or string array.");
283
+ }
284
+ if (Array.isArray(defaultValue) && defaultValue.some((entry) => typeof entry !== "string")) {
285
+ addViolation(violations, "IR_INVALID_NATIVE_METADATA", `${path}.defaultValue`, "Native default arrays must contain strings.");
286
+ }
287
+ return elementType === undefined
288
+ ? undefined
289
+ : {
290
+ elementType: elementType,
291
+ ...(sourceId === undefined ? {} : { sourceId }),
292
+ ...(placeholder === undefined ? {} : { placeholder }),
293
+ ...(defaultValue === undefined ? {} : { defaultValue: defaultValue }),
294
+ ...(multiple === undefined ? {} : { multiple }),
295
+ ...(options === undefined ? {} : { options }),
296
+ };
297
+ }
298
+ function validateOptionList(value, path, violations) {
299
+ if (!Array.isArray(value) || value.length === 0) {
300
+ addViolation(violations, "IR_INVALID_OPTIONS", path, "Options must be a non-empty array.");
301
+ return undefined;
302
+ }
303
+ const options = [];
304
+ const values = new Set();
305
+ value.forEach((entry, index) => {
306
+ if (!isRecord(entry)) {
307
+ addViolation(violations, "IR_INVALID_OPTIONS", `${path}[${index}]`, "Options must be objects.");
308
+ return;
309
+ }
310
+ checkUnknownKeys(entry, ["value", "label", "description"], `${path}[${index}]`, violations);
311
+ const optionValue = requiredString(entry, "value", `${path}[${index}]`, violations);
312
+ const label = requiredString(entry, "label", `${path}[${index}]`, violations);
313
+ const description = optionalString(entry, "description", `${path}[${index}]`, violations);
314
+ if (optionValue !== undefined) {
315
+ if (values.has(optionValue)) {
316
+ addViolation(violations, "IR_DUPLICATE_ID", `${path}[${index}].value`, `Duplicate option value "${optionValue}".`);
317
+ }
318
+ values.add(optionValue);
319
+ }
320
+ if (optionValue !== undefined && label !== undefined) {
321
+ options.push({
322
+ value: optionValue,
323
+ label,
324
+ ...(description === undefined ? {} : { description }),
325
+ });
326
+ }
327
+ });
328
+ return options;
329
+ }
330
+ function validateChecklistItems(value, path, violations) {
331
+ if (!Array.isArray(value) || value.length === 0) {
332
+ addViolation(violations, "IR_INVALID_OPTIONS", path, "Checklist items must be a non-empty array.");
333
+ return undefined;
334
+ }
335
+ const items = [];
336
+ const ids = new Set();
337
+ value.forEach((entry, index) => {
338
+ const itemPath = `${path}[${index}]`;
339
+ if (!isRecord(entry)) {
340
+ addViolation(violations, "IR_INVALID_OPTIONS", itemPath, "Checklist items must be objects.");
341
+ return;
342
+ }
343
+ checkUnknownKeys(entry, ["id", "label", "required", "description"], itemPath, violations);
344
+ const id = requiredString(entry, "id", itemPath, violations);
345
+ const label = requiredString(entry, "label", itemPath, violations);
346
+ const required = hasOwn(entry, "required") ? entry.required : undefined;
347
+ const description = optionalString(entry, "description", itemPath, violations);
348
+ if (id !== undefined) {
349
+ validateIdentifier(id, `${itemPath}.id`, violations);
350
+ if (ids.has(id))
351
+ addViolation(violations, "IR_DUPLICATE_ID", `${itemPath}.id`, `Duplicate checklist item "${id}".`);
352
+ ids.add(id);
353
+ }
354
+ if (typeof required !== "boolean") {
355
+ addViolation(violations, "IR_INVALID_VALUE", `${itemPath}.required`, "Checklist item required must be a boolean.");
356
+ }
357
+ if (id !== undefined && label !== undefined && typeof required === "boolean") {
358
+ items.push({ id, label, required, ...(description === undefined ? {} : { description }) });
359
+ }
360
+ });
361
+ return items;
362
+ }
363
+ function validateConstraints(value, path, fieldType, violations) {
364
+ if (value === undefined)
365
+ return undefined;
366
+ if (!isRecord(value)) {
367
+ addViolation(violations, "IR_INVALID_CONSTRAINT", path, "Constraints must be an object.");
368
+ return undefined;
369
+ }
370
+ checkUnknownKeys(value, ["minLength", "maxLength", "pattern", "minItems", "maxItems", "uniqueItems"], path, violations);
371
+ const minLength = optionalNonNegativeInteger(value, "minLength", path, violations);
372
+ const maxLength = optionalNonNegativeInteger(value, "maxLength", path, violations);
373
+ const pattern = optionalString(value, "pattern", path, violations);
374
+ const minItems = optionalNonNegativeInteger(value, "minItems", path, violations);
375
+ const maxItems = optionalNonNegativeInteger(value, "maxItems", path, violations);
376
+ const uniqueItems = optionalBoolean(value, "uniqueItems", path, violations);
377
+ if (pattern !== undefined) {
378
+ try {
379
+ new RegExp(pattern, "u");
380
+ }
381
+ catch {
382
+ addViolation(violations, "IR_INVALID_CONSTRAINT", `${path}.pattern`, "Pattern must be a valid regular expression.");
383
+ }
384
+ }
385
+ const isStringLike = fieldType === "string" || fieldType === "enum";
386
+ const isArrayLike = fieldType === "array" || fieldType === "checklist";
387
+ if (!isStringLike && (minLength !== undefined || maxLength !== undefined || pattern !== undefined)) {
388
+ addViolation(violations, "IR_INVALID_CONSTRAINT", path, "String constraints are not supported for array-like fields.");
389
+ }
390
+ if (!isArrayLike && (minItems !== undefined || maxItems !== undefined || uniqueItems !== undefined)) {
391
+ addViolation(violations, "IR_INVALID_CONSTRAINT", path, "Array constraints are not supported for string-like fields.");
392
+ }
393
+ if (fieldType === "checklist" && uniqueItems === false) {
394
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${path}.uniqueItems`, "Checklist values must be unique.");
395
+ }
396
+ if (fieldType === "array" && uniqueItems === false) {
397
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${path}.uniqueItems`, "Array selection values must be unique.");
398
+ }
399
+ if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
400
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", path, "minLength cannot be greater than maxLength.");
401
+ }
402
+ if (minItems !== undefined && maxItems !== undefined && minItems > maxItems) {
403
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", path, "minItems cannot be greater than maxItems.");
404
+ }
405
+ return {
406
+ ...(minLength === undefined ? {} : { minLength }),
407
+ ...(maxLength === undefined ? {} : { maxLength }),
408
+ ...(pattern === undefined ? {} : { pattern }),
409
+ ...(minItems === undefined ? {} : { minItems }),
410
+ ...(maxItems === undefined ? {} : { maxItems }),
411
+ ...(uniqueItems === undefined ? {} : { uniqueItems }),
412
+ };
413
+ }
414
+ function optionalNonNegativeInteger(record, key, path, violations) {
415
+ if (!hasOwn(record, key))
416
+ return undefined;
417
+ const value = record[key];
418
+ if (!Number.isSafeInteger(value) || typeof value !== "number" || value < 0) {
419
+ addViolation(violations, "IR_INVALID_CONSTRAINT", `${path}.${key}`, `${key} must be a non-negative safe integer.`);
420
+ return undefined;
421
+ }
422
+ return value;
423
+ }
424
+ function validateDefaultString(value, path, constraints, violations) {
425
+ if (typeof value !== "string") {
426
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "String defaults must be strings.");
427
+ return;
428
+ }
429
+ if (constraints?.minLength !== undefined && value.length < constraints.minLength) {
430
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Default does not satisfy minLength.");
431
+ }
432
+ if (constraints?.maxLength !== undefined && value.length > constraints.maxLength) {
433
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Default does not satisfy maxLength.");
434
+ }
435
+ if (constraints?.pattern !== undefined) {
436
+ try {
437
+ if (!new RegExp(constraints.pattern, "u").test(value)) {
438
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Default does not satisfy pattern.");
439
+ }
440
+ }
441
+ catch {
442
+ // The invalid pattern is reported by validateConstraints; do not make
443
+ // validation itself throw while reporting the complete violation set.
444
+ }
445
+ }
446
+ }
447
+ function validateDefaultArray(value, path, allowedValues, requiredValues, constraints, violations) {
448
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
449
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Array defaults must be arrays of strings.");
450
+ return;
451
+ }
452
+ const values = value;
453
+ if (new Set(values).size !== values.length) {
454
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Array defaults must not contain duplicates.");
455
+ }
456
+ if (allowedValues !== undefined && values.some((entry) => !allowedValues.includes(entry))) {
457
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Array defaults must use declared option values.");
458
+ }
459
+ if (requiredValues.some((entry) => !values.includes(entry))) {
460
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Checklist defaults must include every required item.");
461
+ }
462
+ if (constraints?.minItems !== undefined && values.length < constraints.minItems) {
463
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Default does not satisfy minItems.");
464
+ }
465
+ if (constraints?.maxItems !== undefined && values.length > constraints.maxItems) {
466
+ addViolation(violations, "IR_INVALID_DEFAULT", path, "Default does not satisfy maxItems.");
467
+ }
468
+ }
469
+ function sameStringArray(left, right) {
470
+ return left.length === right.length && left.every((value, index) => value === right[index]);
471
+ }
472
+ function compareNativeOptions(semanticValues, nativeOptions, path, violations) {
473
+ if (nativeOptions === undefined)
474
+ return;
475
+ const nativeValues = nativeOptions.map((option) => option.value);
476
+ if (!sameStringArray(semanticValues, nativeValues)) {
477
+ addViolation(violations, "IR_INCONSISTENT_FIELD", path, "Native option values must match semantic option values in source order.");
478
+ }
479
+ }
480
+ function compareChecklistRequirements(items, nativeOptions, path, violations) {
481
+ if (nativeOptions === undefined)
482
+ return;
483
+ items.forEach((item, index) => {
484
+ const nativeRequired = nativeOptions[index]?.required;
485
+ if (nativeRequired !== undefined && nativeRequired !== item.required) {
486
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}[${index}].required`, "Native and semantic checklist required flags must match.");
487
+ }
488
+ });
489
+ }
490
+ function compareDefaults(fieldDefault, nativeDefault, path, violations) {
491
+ if (nativeDefault === undefined || fieldDefault === undefined)
492
+ return;
493
+ if (typeof fieldDefault === "string" && typeof nativeDefault === "string" && fieldDefault === nativeDefault)
494
+ return;
495
+ if (Array.isArray(fieldDefault) && Array.isArray(nativeDefault) && sameStringArray(fieldDefault, nativeDefault))
496
+ return;
497
+ addViolation(violations, "IR_INCONSISTENT_FIELD", path, "Native and semantic default values must match.");
498
+ }
499
+ function validateField(value, path, index, source, fieldIds, violations) {
500
+ if (!isRecord(value)) {
501
+ addViolation(violations, "IR_INVALID_FIELD", path, "Fields must be objects.");
502
+ return undefined;
503
+ }
504
+ const typeValue = requiredString(value, "type", path, violations);
505
+ if (typeValue === undefined || !fieldTypes.includes(typeValue)) {
506
+ if (typeValue !== undefined) {
507
+ addViolation(violations, "IR_UNSUPPORTED_FIELD_TYPE", `${path}.type`, `Field type "${typeValue}" is not supported.`);
508
+ }
509
+ return undefined;
510
+ }
511
+ const fieldType = typeValue;
512
+ const allowedKeys = [
513
+ "id",
514
+ "label",
515
+ "description",
516
+ "type",
517
+ "required",
518
+ "defaultValue",
519
+ "render",
520
+ "nativeMetadata",
521
+ "constraints",
522
+ ];
523
+ if (fieldType === "enum")
524
+ allowedKeys.push("options");
525
+ if (fieldType === "array")
526
+ allowedKeys.push("selection", "items");
527
+ if (fieldType === "checklist")
528
+ allowedKeys.push("items");
529
+ checkUnknownKeys(value, allowedKeys, path, violations);
530
+ const id = requiredString(value, "id", path, violations);
531
+ const label = requiredString(value, "label", path, violations);
532
+ const description = optionalString(value, "description", path, violations);
533
+ const required = requiredString(value, "required", path, violations);
534
+ const render = value.render;
535
+ validateRenderMetadata(render, `${path}.render`, index, violations, false);
536
+ const nativeMetadata = validateNativeFieldMetadata(value.nativeMetadata, `${path}.nativeMetadata`, source, fieldType, violations);
537
+ const constraints = validateConstraints(value.constraints, `${path}.constraints`, fieldType, violations);
538
+ if (id !== undefined) {
539
+ validateIdentifier(id, `${path}.id`, violations);
540
+ if (fieldIds.has(id))
541
+ addViolation(violations, "IR_DUPLICATE_ID", `${path}.id`, `Duplicate field id "${id}".`);
542
+ fieldIds.add(id);
543
+ }
544
+ if (required === undefined || !requiredStates.includes(required)) {
545
+ if (required !== undefined)
546
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.required`, `Required state "${required}" is not supported.`);
547
+ }
548
+ const requiredState = required;
549
+ const defaultValue = validateOptionalDefault(value, path, violations);
550
+ let allowedValues;
551
+ let requiredValues = [];
552
+ if (fieldType === "string") {
553
+ if (defaultValue !== undefined)
554
+ validateDefaultString(defaultValue, `${path}.defaultValue`, constraints, violations);
555
+ }
556
+ else if (fieldType === "enum") {
557
+ const options = validateOptionList(value.options, `${path}.options`, violations);
558
+ allowedValues = options?.map((option) => option.value);
559
+ if (defaultValue !== undefined) {
560
+ if (typeof defaultValue !== "string" || allowedValues === undefined || !allowedValues.includes(defaultValue)) {
561
+ addViolation(violations, "IR_INVALID_DEFAULT", `${path}.defaultValue`, "Enum defaults must match a declared option value.");
562
+ }
563
+ if (typeof defaultValue === "string")
564
+ validateDefaultString(defaultValue, `${path}.defaultValue`, constraints, violations);
565
+ }
566
+ compareNativeOptions(allowedValues ?? [], nativeMetadata?.options, `${path}.nativeMetadata.options`, violations);
567
+ if (nativeMetadata?.multiple === true) {
568
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}.nativeMetadata.multiple`, "Single-select enum fields cannot be native multi-selects.");
569
+ }
570
+ }
571
+ else if (fieldType === "array") {
572
+ const selection = requiredString(value, "selection", path, violations);
573
+ if (selection === undefined || !arraySelections.includes(selection)) {
574
+ if (selection !== undefined)
575
+ addViolation(violations, "IR_INVALID_VALUE", `${path}.selection`, `Array selection "${selection}" is not supported.`);
576
+ }
577
+ const items = requiredRecord(value, "items", path, violations);
578
+ if (items !== undefined) {
579
+ checkUnknownKeys(items, ["type", "options"], `${path}.items`, violations);
580
+ const itemType = requiredString(items, "type", `${path}.items`, violations);
581
+ if (itemType !== undefined && itemType !== "string") {
582
+ addViolation(violations, "IR_UNSUPPORTED_FIELD_TYPE", `${path}.items.type`, "Only string array items are supported.");
583
+ }
584
+ if (hasOwn(items, "options")) {
585
+ const options = validateOptionList(items.options, `${path}.items.options`, violations);
586
+ allowedValues = options?.map((option) => option.value);
587
+ }
588
+ }
589
+ if (selection === "multi_select" &&
590
+ source === "issue_form" &&
591
+ (allowedValues === undefined || allowedValues.length === 0)) {
592
+ addViolation(violations, "IR_INVALID_OPTIONS", `${path}.items.options`, "Issue Form multi-select fields require options.");
593
+ }
594
+ if (defaultValue !== undefined)
595
+ validateDefaultArray(defaultValue, `${path}.defaultValue`, allowedValues, [], constraints, violations);
596
+ compareNativeOptions(allowedValues ?? [], nativeMetadata?.options, `${path}.nativeMetadata.options`, violations);
597
+ if (source === "issue_form" && selection === "multi_select" && nativeMetadata?.multiple !== true) {
598
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}.nativeMetadata.multiple`, "Multi-select fields must preserve native multiple=true.");
599
+ }
600
+ if (source === "issue_form" && selection === "list" && nativeMetadata?.multiple === true) {
601
+ addViolation(violations, "IR_INCONSISTENT_FIELD", `${path}.nativeMetadata.multiple`, "List fields cannot preserve native multiple=true.");
602
+ }
603
+ }
604
+ else {
605
+ const items = validateChecklistItems(value.items, `${path}.items`, violations);
606
+ requiredValues = items?.filter((item) => item.required).map((item) => item.id) ?? [];
607
+ allowedValues = items?.map((item) => item.id);
608
+ if (requiredValues.length > 0 && requiredState !== "required") {
609
+ addViolation(violations, "IR_CHECKLIST_REQUIRED_MISMATCH", `${path}.required`, "A checklist with required items must have required field semantics.");
610
+ }
611
+ if (defaultValue !== undefined) {
612
+ validateDefaultArray(defaultValue, `${path}.defaultValue`, allowedValues, requiredValues, constraints, violations);
613
+ }
614
+ compareNativeOptions(allowedValues ?? [], nativeMetadata?.options, `${path}.nativeMetadata.options`, violations);
615
+ compareChecklistRequirements(items ?? [], nativeMetadata?.options, `${path}.nativeMetadata.options`, violations);
616
+ }
617
+ if (nativeMetadata !== undefined)
618
+ compareDefaults(defaultValue, nativeMetadata.defaultValue, `${path}.nativeMetadata.defaultValue`, violations);
619
+ return id === undefined || required === undefined || !requiredStates.includes(required)
620
+ ? undefined
621
+ : {
622
+ type: fieldType,
623
+ required: requiredState,
624
+ ...(constraints === undefined ? {} : { constraints }),
625
+ ...(fieldType === "checklist" && allowedValues !== undefined
626
+ ? { checklistItemCount: allowedValues.length }
627
+ : {}),
628
+ };
629
+ }
630
+ function validateSection(value, path, index, source, fieldIds, sectionIds, summaries, violations) {
631
+ if (!isRecord(value)) {
632
+ addViolation(violations, "IR_INVALID_SECTION", path, "Sections must be objects.");
633
+ return;
634
+ }
635
+ checkUnknownKeys(value, ["id", "title", "description", "kind", "content", "render", "nativeMetadata", "fields"], path, violations);
636
+ const id = requiredString(value, "id", path, violations);
637
+ const title = optionalString(value, "title", path, violations);
638
+ optionalString(value, "description", path, violations);
639
+ const kind = requiredString(value, "kind", path, violations);
640
+ if (kind !== undefined && !sectionKinds.includes(kind)) {
641
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.kind`, `Section kind "${kind}" is not supported.`);
642
+ }
643
+ const sectionKind = kind;
644
+ validateRenderMetadata(value.render, `${path}.render`, index, violations, true);
645
+ validateNativeSectionMetadata(value.nativeMetadata, `${path}.nativeMetadata`, source, sectionKind, violations);
646
+ const fields = requiredArray(value, "fields", path, violations);
647
+ if (id !== undefined) {
648
+ validateIdentifier(id, `${path}.id`, violations);
649
+ if (sectionIds.has(id))
650
+ addViolation(violations, "IR_DUPLICATE_ID", `${path}.id`, `Duplicate section id "${id}".`);
651
+ sectionIds.add(id);
652
+ }
653
+ const content = hasOwn(value, "content") ? value.content : undefined;
654
+ if (content !== undefined && typeof content !== "string") {
655
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.content`, "Section content must be a string when present.");
656
+ }
657
+ if (fields !== undefined && sectionKind === "documentation" && fields.length > 0) {
658
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.fields`, "Documentation sections cannot contain semantic fields.");
659
+ }
660
+ if (sectionKind === "documentation" && (typeof content !== "string" || content.length === 0)) {
661
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.content`, "Documentation sections must preserve non-empty content.");
662
+ }
663
+ if (fields !== undefined && sectionKind === "input" && fields.length === 0) {
664
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.fields`, "Input sections must contain at least one field.");
665
+ }
666
+ if (fields !== undefined) {
667
+ fields.forEach((field, fieldIndex) => {
668
+ const summary = validateField(field, `${path}.fields[${fieldIndex}]`, fieldIndex, source, fieldIds, violations);
669
+ if (summary !== undefined && isRecord(field)) {
670
+ const fieldId = typeof field.id === "string" ? field.id : undefined;
671
+ if (fieldId !== undefined)
672
+ summaries.set(fieldId, summary);
673
+ }
674
+ });
675
+ }
676
+ if (title === undefined && sectionKind === "input" && source === "pull_request_template") {
677
+ addViolation(violations, "IR_INVALID_SECTION", `${path}.title`, "PR input sections must preserve their heading title.");
678
+ }
679
+ }
680
+ function validateSupplementalConstraints(value, path, summaries, violations) {
681
+ if (value === undefined) {
682
+ addViolation(violations, "IR_MISSING_PROPERTY", path, 'Property "supplementalConstraints" is required.');
683
+ return;
684
+ }
685
+ if (!isRecord(value)) {
686
+ addViolation(violations, "IR_INVALID_VALUE", path, 'Property "supplementalConstraints" must be an object.');
687
+ return;
688
+ }
689
+ const record = value;
690
+ checkUnknownKeys(record, ["fields"], path, violations);
691
+ const fields = requiredArray(record, "fields", path, violations);
692
+ if (fields === undefined)
693
+ return;
694
+ const references = new Set();
695
+ fields.forEach((entry, index) => {
696
+ const entryPath = `${path}.fields[${index}]`;
697
+ if (!isRecord(entry)) {
698
+ addViolation(violations, "IR_INVALID_CONSTRAINT", entryPath, "Supplemental field constraints must be objects.");
699
+ return;
700
+ }
701
+ checkUnknownKeys(entry, [
702
+ "fieldId",
703
+ "required",
704
+ "minLength",
705
+ "maxLength",
706
+ "pattern",
707
+ "minItems",
708
+ "maxItems",
709
+ "linkedIssue",
710
+ "checklistMinCompleted",
711
+ "checklistRequireComplete",
712
+ ], entryPath, violations);
713
+ const fieldId = requiredString(entry, "fieldId", entryPath, violations);
714
+ if (fieldId === undefined)
715
+ return;
716
+ if (references.has(fieldId)) {
717
+ addViolation(violations, "IR_DUPLICATE_ID", `${entryPath}.fieldId`, `Duplicate supplemental constraint for "${fieldId}".`);
718
+ }
719
+ references.add(fieldId);
720
+ const summary = summaries.get(fieldId);
721
+ if (summary === undefined) {
722
+ addViolation(violations, "IR_UNKNOWN_FIELD_REFERENCE", `${entryPath}.fieldId`, `Field "${fieldId}" does not exist.`);
723
+ return;
724
+ }
725
+ const required = optionalBoolean(entry, "required", entryPath, violations);
726
+ const minLength = optionalNonNegativeInteger(entry, "minLength", entryPath, violations);
727
+ const maxLength = optionalNonNegativeInteger(entry, "maxLength", entryPath, violations);
728
+ const pattern = optionalString(entry, "pattern", entryPath, violations);
729
+ const minItems = optionalNonNegativeInteger(entry, "minItems", entryPath, violations);
730
+ const maxItems = optionalNonNegativeInteger(entry, "maxItems", entryPath, violations);
731
+ const linkedIssue = optionalBoolean(entry, "linkedIssue", entryPath, violations);
732
+ const checklistMinCompleted = optionalNonNegativeInteger(entry, "checklistMinCompleted", entryPath, violations);
733
+ const checklistRequireComplete = optionalBoolean(entry, "checklistRequireComplete", entryPath, violations);
734
+ if (pattern !== undefined) {
735
+ try {
736
+ new RegExp(pattern, "u");
737
+ }
738
+ catch {
739
+ addViolation(violations, "IR_INVALID_CONSTRAINT", `${entryPath}.pattern`, "Pattern must be a valid regular expression.");
740
+ }
741
+ }
742
+ const isStringLike = summary.type === "string" || summary.type === "enum";
743
+ const isArrayLike = summary.type === "array" || summary.type === "checklist";
744
+ if (!isStringLike && (minLength !== undefined || maxLength !== undefined || pattern !== undefined)) {
745
+ addViolation(violations, "IR_INVALID_CONSTRAINT", entryPath, "String constraints are not supported for array-like fields.");
746
+ }
747
+ if (!isArrayLike && (minItems !== undefined || maxItems !== undefined)) {
748
+ addViolation(violations, "IR_INVALID_CONSTRAINT", entryPath, "Array constraints are not supported for string-like fields.");
749
+ }
750
+ if (linkedIssue !== undefined && !isStringLike) {
751
+ addViolation(violations, "IR_INVALID_CONSTRAINT", `${entryPath}.linkedIssue`, "linkedIssue is supported only for string-like fields.");
752
+ }
753
+ if ((checklistMinCompleted !== undefined || checklistRequireComplete !== undefined) &&
754
+ summary.type !== "checklist") {
755
+ addViolation(violations, "IR_INVALID_CONSTRAINT", entryPath, "Checklist completion constraints are supported only for checklist fields.");
756
+ }
757
+ if (checklistMinCompleted !== undefined && maxItems !== undefined && checklistMinCompleted > maxItems) {
758
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", entryPath, "checklistMinCompleted cannot be greater than maxItems.");
759
+ }
760
+ if (checklistMinCompleted !== undefined &&
761
+ summary.checklistItemCount !== undefined &&
762
+ checklistMinCompleted > summary.checklistItemCount) {
763
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${entryPath}.checklistMinCompleted`, "checklistMinCompleted cannot exceed the checklist item count.");
764
+ }
765
+ if (checklistRequireComplete === true &&
766
+ summary.checklistItemCount !== undefined &&
767
+ maxItems !== undefined &&
768
+ maxItems < summary.checklistItemCount) {
769
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${entryPath}.checklistRequireComplete`, "checklistRequireComplete requires maxItems to allow all checklist items.");
770
+ }
771
+ if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
772
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", entryPath, "minLength cannot be greater than maxLength.");
773
+ }
774
+ if (minItems !== undefined && maxItems !== undefined && minItems > maxItems) {
775
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", entryPath, "minItems cannot be greater than maxItems.");
776
+ }
777
+ if (required === false && summary.required === "required") {
778
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${entryPath}.required`, "Supplemental required=false contradicts required field semantics.");
779
+ }
780
+ const nativeConstraints = summary.constraints;
781
+ if (nativeConstraints !== undefined) {
782
+ const conflictingKeys = [
783
+ "minLength",
784
+ "maxLength",
785
+ "pattern",
786
+ "minItems",
787
+ "maxItems",
788
+ ];
789
+ for (const key of conflictingKeys) {
790
+ const supplementalValue = entry[key];
791
+ const nativeValue = nativeConstraints[key];
792
+ if (supplementalValue !== undefined && nativeValue !== undefined && supplementalValue !== nativeValue) {
793
+ addViolation(violations, "IR_INCONSISTENT_CONSTRAINT", `${entryPath}.${key}`, `Supplemental ${key} contradicts the field constraint.`);
794
+ }
795
+ }
796
+ }
797
+ });
798
+ }
799
+ export function validateCanonicalContract(input) {
800
+ const violations = [];
801
+ if (!isRecord(input)) {
802
+ return {
803
+ valid: false,
804
+ violations: [{ code: "IR_NOT_OBJECT", path: "$", message: "Canonical IR must be a JSON object." }],
805
+ };
806
+ }
807
+ checkUnknownKeys(input, [
808
+ "irVersion",
809
+ "schemaVersion",
810
+ "artifactKind",
811
+ "templateIdentity",
812
+ "nativeMetadata",
813
+ "sections",
814
+ "supplementalConstraints",
815
+ ], "$", violations);
816
+ const irVersion = requiredString(input, "irVersion", "$", violations);
817
+ const schemaVersion = requiredString(input, "schemaVersion", "$", violations);
818
+ if (irVersion !== undefined && irVersion !== CANONICAL_IR_VERSION) {
819
+ addViolation(violations, "IR_UNSUPPORTED_VERSION", "$.irVersion", `Only IR version ${CANONICAL_IR_VERSION} is supported.`);
820
+ }
821
+ if (schemaVersion !== undefined && schemaVersion !== CONTRACT_SCHEMA_VERSION) {
822
+ addViolation(violations, "IR_UNSUPPORTED_VERSION", "$.schemaVersion", `Only schema version ${CONTRACT_SCHEMA_VERSION} is supported.`);
823
+ }
824
+ const artifactKind = requiredString(input, "artifactKind", "$", violations);
825
+ if (artifactKind !== undefined && artifactKind !== "issue" && artifactKind !== "pull_request") {
826
+ addViolation(violations, "IR_UNSUPPORTED_ARTIFACT_KIND", "$.artifactKind", `Artifact kind "${artifactKind}" is not supported.`);
827
+ }
828
+ const template = requiredRecord(input, "templateIdentity", "$", violations);
829
+ let templateSource;
830
+ let templatePath;
831
+ if (template !== undefined) {
832
+ checkUnknownKeys(template, ["id", "name", "path", "source"], "$.templateIdentity", violations);
833
+ const id = requiredString(template, "id", "$.templateIdentity", violations);
834
+ const name = requiredString(template, "name", "$.templateIdentity", violations);
835
+ templatePath = requiredString(template, "path", "$.templateIdentity", violations);
836
+ const source = requiredString(template, "source", "$.templateIdentity", violations);
837
+ validateIdentifier(id, "$.templateIdentity.id", violations);
838
+ if (name !== undefined && name.trim().length === 0) {
839
+ addViolation(violations, "IR_INVALID_VALUE", "$.templateIdentity.name", "Template name cannot be empty.");
840
+ }
841
+ if (templatePath !== undefined &&
842
+ (templatePath.startsWith("/") || templatePath.includes("\\") || templatePath.split("/").includes(".."))) {
843
+ addViolation(violations, "IR_INVALID_VALUE", "$.templateIdentity.path", "Template path must be a safe repository-relative path.");
844
+ }
845
+ if (source !== undefined && !templateSources.includes(source)) {
846
+ addViolation(violations, "IR_UNSUPPORTED_SOURCE_FORMAT", "$.templateIdentity.source", `Template source "${source}" is not supported.`);
847
+ }
848
+ else {
849
+ templateSource = source;
850
+ }
851
+ }
852
+ const native = requiredRecord(input, "nativeMetadata", "$", violations);
853
+ if (native !== undefined) {
854
+ checkUnknownKeys(native, ["source", "path", "title", "description", "labels"], "$.nativeMetadata", violations);
855
+ const source = requiredString(native, "source", "$.nativeMetadata", violations);
856
+ const path = requiredString(native, "path", "$.nativeMetadata", violations);
857
+ optionalString(native, "title", "$.nativeMetadata", violations);
858
+ optionalString(native, "description", "$.nativeMetadata", violations);
859
+ const labels = optionalStringArray(native, "labels", "$.nativeMetadata", violations);
860
+ if (source !== undefined && !templateSources.includes(source)) {
861
+ addViolation(violations, "IR_UNSUPPORTED_SOURCE_FORMAT", "$.nativeMetadata.source", `Template source "${source}" is not supported.`);
862
+ }
863
+ if (source !== undefined && templateSource !== undefined && source !== templateSource) {
864
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", "$.nativeMetadata.source", "Native source must match templateIdentity.source.");
865
+ }
866
+ if (path !== undefined && templatePath !== undefined && path !== templatePath) {
867
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", "$.nativeMetadata.path", "Native path must match templateIdentity.path.");
868
+ }
869
+ if (labels !== undefined && new Set(labels).size !== labels.length) {
870
+ addViolation(violations, "IR_DUPLICATE_ID", "$.nativeMetadata.labels", "Native labels must not contain duplicates.");
871
+ }
872
+ }
873
+ if (artifactKind === "issue" && templateSource !== undefined && templateSource !== "issue_form") {
874
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", "$.templateIdentity.source", "Issue contracts must use Issue Form sources.");
875
+ }
876
+ if (artifactKind === "pull_request" && templateSource !== undefined && templateSource !== "pull_request_template") {
877
+ addViolation(violations, "IR_INCONSISTENT_SOURCE", "$.templateIdentity.source", "Pull request contracts must use pull request template sources.");
878
+ }
879
+ const sections = requiredArray(input, "sections", "$", violations);
880
+ const fieldIds = new Set();
881
+ const sectionIds = new Set();
882
+ const summaries = new Map();
883
+ if (sections !== undefined) {
884
+ if (sections.length === 0)
885
+ addViolation(violations, "IR_INVALID_SECTION", "$.sections", "A contract must contain at least one section.");
886
+ if (templateSource === undefined) {
887
+ addViolation(violations, "IR_INVALID_SECTION", "$.sections", "Sections cannot be validated without a supported template source.");
888
+ }
889
+ else {
890
+ sections.forEach((section, index) => {
891
+ validateSection(section, `$.sections[${index}]`, index, templateSource, fieldIds, sectionIds, summaries, violations);
892
+ });
893
+ }
894
+ }
895
+ validateSupplementalConstraints(input.supplementalConstraints, "$.supplementalConstraints", summaries, violations);
896
+ return { valid: violations.length === 0, violations };
897
+ }
898
+ export function isCanonicalContract(input) {
899
+ return validateCanonicalContract(input).valid;
900
+ }
901
+ export function assertCanonicalContract(input) {
902
+ const result = validateCanonicalContract(input);
903
+ if (!result.valid)
904
+ throw new CanonicalIrValidationError(result.violations);
905
+ }
906
+ function canonicalizeNativeOptions(options) {
907
+ return options?.map((option) => ({
908
+ value: option.value,
909
+ ...(option.label === undefined ? {} : { label: option.label }),
910
+ ...(option.description === undefined ? {} : { description: option.description }),
911
+ ...(option.required === undefined ? {} : { required: option.required }),
912
+ }));
913
+ }
914
+ function canonicalizeNativeFieldMetadata(metadata) {
915
+ return {
916
+ elementType: metadata.elementType,
917
+ ...(metadata.sourceId === undefined ? {} : { sourceId: metadata.sourceId }),
918
+ ...(metadata.placeholder === undefined ? {} : { placeholder: metadata.placeholder }),
919
+ ...(metadata.defaultValue === undefined
920
+ ? {}
921
+ : { defaultValue: Array.isArray(metadata.defaultValue) ? [...metadata.defaultValue] : metadata.defaultValue }),
922
+ ...(metadata.multiple === undefined ? {} : { multiple: metadata.multiple }),
923
+ ...(metadata.options === undefined ? {} : { options: canonicalizeNativeOptions(metadata.options) }),
924
+ };
925
+ }
926
+ function canonicalizeFieldConstraints(constraints) {
927
+ return {
928
+ ...(constraints.minLength === undefined ? {} : { minLength: constraints.minLength }),
929
+ ...(constraints.maxLength === undefined ? {} : { maxLength: constraints.maxLength }),
930
+ ...(constraints.pattern === undefined ? {} : { pattern: constraints.pattern }),
931
+ ...(constraints.minItems === undefined ? {} : { minItems: constraints.minItems }),
932
+ ...(constraints.maxItems === undefined ? {} : { maxItems: constraints.maxItems }),
933
+ ...(constraints.uniqueItems === undefined ? {} : { uniqueItems: constraints.uniqueItems }),
934
+ };
935
+ }
936
+ function canonicalizeField(field) {
937
+ const base = {
938
+ id: field.id,
939
+ label: field.label,
940
+ ...(field.description === undefined ? {} : { description: field.description }),
941
+ type: field.type,
942
+ required: field.required,
943
+ render: { order: field.render.order },
944
+ nativeMetadata: canonicalizeNativeFieldMetadata(field.nativeMetadata),
945
+ ...(field.constraints === undefined ? {} : { constraints: canonicalizeFieldConstraints(field.constraints) }),
946
+ };
947
+ if (field.type === "string") {
948
+ if (field.defaultValue !== undefined)
949
+ base.defaultValue = field.defaultValue;
950
+ }
951
+ else if (field.type === "enum") {
952
+ base.options = field.options.map((option) => ({
953
+ value: option.value,
954
+ label: option.label,
955
+ ...(option.description === undefined ? {} : { description: option.description }),
956
+ }));
957
+ if (field.defaultValue !== undefined)
958
+ base.defaultValue = field.defaultValue;
959
+ }
960
+ else if (field.type === "array") {
961
+ base.selection = field.selection;
962
+ base.items = {
963
+ type: field.items.type,
964
+ ...(field.items.options === undefined
965
+ ? {}
966
+ : {
967
+ options: field.items.options.map((option) => ({
968
+ value: option.value,
969
+ label: option.label,
970
+ ...(option.description === undefined ? {} : { description: option.description }),
971
+ })),
972
+ }),
973
+ };
974
+ if (field.defaultValue !== undefined)
975
+ base.defaultValue = [...field.defaultValue];
976
+ }
977
+ else {
978
+ base.items = field.items.map((item) => ({
979
+ id: item.id,
980
+ label: item.label,
981
+ required: item.required,
982
+ ...(item.description === undefined ? {} : { description: item.description }),
983
+ }));
984
+ if (field.defaultValue !== undefined)
985
+ base.defaultValue = [...field.defaultValue];
986
+ }
987
+ return base;
988
+ }
989
+ function canonicalizeSection(section) {
990
+ return {
991
+ id: section.id,
992
+ ...(section.title === undefined ? {} : { title: section.title }),
993
+ ...(section.description === undefined ? {} : { description: section.description }),
994
+ kind: section.kind,
995
+ ...(section.content === undefined ? {} : { content: section.content }),
996
+ render: {
997
+ order: section.render.order,
998
+ ...(section.render.headingLevel === undefined ? {} : { headingLevel: section.render.headingLevel }),
999
+ },
1000
+ nativeMetadata: {
1001
+ elementType: section.nativeMetadata.elementType,
1002
+ ...(section.nativeMetadata.sourceId === undefined ? {} : { sourceId: section.nativeMetadata.sourceId }),
1003
+ ...(section.nativeMetadata.headingLevel === undefined
1004
+ ? {}
1005
+ : { headingLevel: section.nativeMetadata.headingLevel }),
1006
+ ...(section.nativeMetadata.markdown === undefined ? {} : { markdown: section.nativeMetadata.markdown }),
1007
+ },
1008
+ fields: section.fields.map(canonicalizeField),
1009
+ };
1010
+ }
1011
+ function canonicalizeContract(contract) {
1012
+ return {
1013
+ irVersion: contract.irVersion,
1014
+ schemaVersion: contract.schemaVersion,
1015
+ artifactKind: contract.artifactKind,
1016
+ templateIdentity: {
1017
+ id: contract.templateIdentity.id,
1018
+ name: contract.templateIdentity.name,
1019
+ path: contract.templateIdentity.path,
1020
+ source: contract.templateIdentity.source,
1021
+ },
1022
+ nativeMetadata: {
1023
+ source: contract.nativeMetadata.source,
1024
+ path: contract.nativeMetadata.path,
1025
+ ...(contract.nativeMetadata.title === undefined ? {} : { title: contract.nativeMetadata.title }),
1026
+ ...(contract.nativeMetadata.description === undefined
1027
+ ? {}
1028
+ : { description: contract.nativeMetadata.description }),
1029
+ ...(contract.nativeMetadata.labels === undefined ? {} : { labels: [...contract.nativeMetadata.labels] }),
1030
+ },
1031
+ sections: contract.sections.map(canonicalizeSection),
1032
+ supplementalConstraints: {
1033
+ fields: contract.supplementalConstraints.fields.map((constraint) => ({
1034
+ fieldId: constraint.fieldId,
1035
+ ...(constraint.required === undefined ? {} : { required: constraint.required }),
1036
+ ...(constraint.minLength === undefined ? {} : { minLength: constraint.minLength }),
1037
+ ...(constraint.maxLength === undefined ? {} : { maxLength: constraint.maxLength }),
1038
+ ...(constraint.pattern === undefined ? {} : { pattern: constraint.pattern }),
1039
+ ...(constraint.minItems === undefined ? {} : { minItems: constraint.minItems }),
1040
+ ...(constraint.maxItems === undefined ? {} : { maxItems: constraint.maxItems }),
1041
+ ...(constraint.linkedIssue === undefined ? {} : { linkedIssue: constraint.linkedIssue }),
1042
+ ...(constraint.checklistMinCompleted === undefined
1043
+ ? {}
1044
+ : { checklistMinCompleted: constraint.checklistMinCompleted }),
1045
+ ...(constraint.checklistRequireComplete === undefined
1046
+ ? {}
1047
+ : { checklistRequireComplete: constraint.checklistRequireComplete }),
1048
+ })),
1049
+ },
1050
+ };
1051
+ }
1052
+ export function serializeCanonicalContract(input) {
1053
+ assertCanonicalContract(input);
1054
+ const serialized = JSON.stringify(canonicalizeContract(input));
1055
+ if (serialized === undefined)
1056
+ throw new Error("Canonical IR could not be serialized.");
1057
+ return serialized;
1058
+ }
1059
+ export function deserializeCanonicalContract(serialized) {
1060
+ let parsed;
1061
+ try {
1062
+ parsed = JSON.parse(serialized);
1063
+ }
1064
+ catch (error) {
1065
+ const message = error instanceof Error ? error.message : "Invalid JSON.";
1066
+ throw new CanonicalIrValidationError([{ code: "IR_INVALID_JSON", path: "$", message }]);
1067
+ }
1068
+ assertCanonicalContract(parsed);
1069
+ return parsed;
1070
+ }
1071
+ //# sourceMappingURL=ir.js.map