gh-inari 0.7.0 → 0.9.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 (56) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +35 -10
  3. package/dist/artifact.d.ts +158 -15
  4. package/dist/artifact.js +834 -78
  5. package/dist/artifact.js.map +1 -1
  6. package/dist/cli.js +484 -65
  7. package/dist/cli.js.map +1 -1
  8. package/dist/contract/index.d.ts +2 -0
  9. package/dist/contract/index.js +2 -0
  10. package/dist/contract/index.js.map +1 -1
  11. package/dist/contract/ir.d.ts +15 -0
  12. package/dist/contract/ir.js +40 -2
  13. package/dist/contract/ir.js.map +1 -1
  14. package/dist/contract/issue-form.js +16 -3
  15. package/dist/contract/issue-form.js.map +1 -1
  16. package/dist/contract/issue-reference.d.ts +58 -0
  17. package/dist/contract/issue-reference.js +157 -0
  18. package/dist/contract/issue-reference.js.map +1 -0
  19. package/dist/contract/normalization.d.ts +31 -0
  20. package/dist/contract/normalization.js +73 -0
  21. package/dist/contract/normalization.js.map +1 -0
  22. package/dist/contract/schema.d.ts +4 -0
  23. package/dist/contract/schema.js +42 -1
  24. package/dist/contract/schema.js.map +1 -1
  25. package/dist/contract/validation.d.ts +120 -2
  26. package/dist/contract/validation.js +476 -3
  27. package/dist/contract/validation.js.map +1 -1
  28. package/dist/diagnostics.d.ts +133 -0
  29. package/dist/diagnostics.js +421 -0
  30. package/dist/diagnostics.js.map +1 -0
  31. package/dist/github/adapter.d.ts +2 -0
  32. package/dist/github/adapter.js +80 -32
  33. package/dist/github/adapter.js.map +1 -1
  34. package/dist/github/capability.js +1 -0
  35. package/dist/github/capability.js.map +1 -1
  36. package/dist/github/types.d.ts +7 -0
  37. package/dist/governance.d.ts +8 -2
  38. package/dist/governance.js +56 -4
  39. package/dist/governance.js.map +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/pr-policy.d.ts +9 -0
  44. package/dist/pr-policy.js +36 -16
  45. package/dist/pr-policy.js.map +1 -1
  46. package/dist/pr-sync-input.d.ts +74 -0
  47. package/dist/pr-sync-input.js +231 -0
  48. package/dist/pr-sync-input.js.map +1 -0
  49. package/dist/reconciliation.d.ts +21 -14
  50. package/dist/reconciliation.js +515 -42
  51. package/dist/reconciliation.js.map +1 -1
  52. package/dist/semantic-template.js +32 -5
  53. package/dist/semantic-template.js.map +1 -1
  54. package/dist/skill.js +55 -21
  55. package/dist/skill.js.map +1 -1
  56. package/package.json +3 -3
package/dist/artifact.js CHANGED
@@ -1,14 +1,18 @@
1
- import { SemanticValidationError, validateSemanticInput, } from "./contract/validation.js";
1
+ import { repairPartialSemanticInput, SemanticValidationError, validatePartialSemanticInput, validateSemanticInput, } from "./contract/validation.js";
2
+ import { createArtifactDiagnostic, createArtifactDiagnosticReport, createFieldEvidence, MAX_ARTIFACT_DIAGNOSTICS, } from "./diagnostics.js";
2
3
  import { assertCanonicalContract, } from "./contract/ir.js";
4
+ import { EMPTY_ISSUE_DEPENDENCIES, validateIssueDependencies, } from "./contract/issue-reference.js";
3
5
  import { createValidatedRenderedIssueArtifact, createValidatedRenderedPullRequestArtifact, } from "./github/capability.js";
4
6
  export class ArtifactInputError extends Error {
5
7
  code;
6
8
  path;
7
- constructor(code, message, path = "$") {
9
+ details;
10
+ constructor(code, message, path = "$", details) {
8
11
  super(message);
9
12
  this.name = "ArtifactInputError";
10
13
  this.code = code;
11
14
  this.path = path;
15
+ this.details = details;
12
16
  }
13
17
  }
14
18
  /** Stable failures raised before a mutation-capable artifact is created. */
@@ -19,10 +23,134 @@ export class ArtifactPreparationError extends Error {
19
23
  super(message);
20
24
  this.name = "ArtifactPreparationError";
21
25
  this.code = code;
22
- this.diagnostics = diagnostics;
26
+ this.diagnostics = createArtifactDiagnosticReport(diagnostics.slice(0, MAX_ARTIFACT_DIAGNOSTICS)).diagnostics;
23
27
  }
24
28
  }
25
29
  const GITHUB_NO_RESPONSE = "_No response_";
30
+ /** Explicit empty strings need a representation distinct from omitted values. */
31
+ const EXPLICIT_EMPTY_STRING_MARKER = "\u200B";
32
+ /**
33
+ * Bounded invisible template identity marker embedded in newly rendered
34
+ * artifacts. It is the primary template-selection signal for governed
35
+ * read/repair/validation; legacy artifacts without a marker (or with one
36
+ * that cannot be trusted) fall back to deterministic structural matching.
37
+ * The marker is metadata only: it never substitutes for the authoritative
38
+ * repository governance/provenance that resolves the actual contract.
39
+ */
40
+ export const TEMPLATE_IDENTITY_MARKER_VERSION = "1";
41
+ const TEMPLATE_IDENTITY_MARKER_PREFIX = "<!-- inari:template ";
42
+ const TEMPLATE_IDENTITY_MARKER_SUFFIX = " -->";
43
+ const TEMPLATE_IDENTITY_MARKER_LINE_PATTERN = /^<!-- inari:template (\{.*\}) -->$/u;
44
+ const TEMPLATE_IDENTITY_MARKER_MAX_LENGTH = 512;
45
+ export const ISSUE_DEPENDENCY_MARKER_VERSION = "1";
46
+ const ISSUE_DEPENDENCY_MARKER_PREFIX = "<!-- inari:issue-dependencies ";
47
+ const ISSUE_DEPENDENCY_MARKER_SUFFIX = " -->";
48
+ const ISSUE_DEPENDENCY_MARKER_LINE_PATTERN = /^<!-- inari:issue-dependencies (\{.*\}) -->$/u;
49
+ const ISSUE_DEPENDENCY_MARKER_MAX_LENGTH = 8_192;
50
+ function renderTemplateIdentityMarker(contract) {
51
+ const marker = {
52
+ version: TEMPLATE_IDENTITY_MARKER_VERSION,
53
+ kind: contract.artifactKind,
54
+ path: contract.templateIdentity.path,
55
+ };
56
+ return `${TEMPLATE_IDENTITY_MARKER_PREFIX}${JSON.stringify(marker)}${TEMPLATE_IDENTITY_MARKER_SUFFIX}`;
57
+ }
58
+ function renderIssueDependencyMarker(dependencies) {
59
+ return `${ISSUE_DEPENDENCY_MARKER_PREFIX}${JSON.stringify({
60
+ version: ISSUE_DEPENDENCY_MARKER_VERSION,
61
+ dependencies,
62
+ })}${ISSUE_DEPENDENCY_MARKER_SUFFIX}`;
63
+ }
64
+ /**
65
+ * Read only the reserved trailing dependency marker emitted by Inari. No
66
+ * ordinary Markdown is interpreted as a relationship declaration.
67
+ */
68
+ export function extractIssueDependencyMarker(body) {
69
+ const source = normalizeSource(body);
70
+ const lines = source.split("\n");
71
+ let end = lines.length;
72
+ while (end > 0 && (lines[end - 1] ?? "").trim().length === 0)
73
+ end -= 1;
74
+ const candidate = end > 0 ? (lines[end - 1] ?? "").trim() : undefined;
75
+ if (candidate === undefined || !candidate.startsWith(ISSUE_DEPENDENCY_MARKER_PREFIX)) {
76
+ return { status: "absent", body: source };
77
+ }
78
+ const remaining = lines.slice(0, end - 1);
79
+ while (remaining.at(-1) !== undefined && (remaining.at(-1) ?? "").trim().length === 0)
80
+ remaining.pop();
81
+ const strippedBody = remaining.length === 0 ? "" : `${remaining.join("\n")}\n`;
82
+ if (candidate.length > ISSUE_DEPENDENCY_MARKER_MAX_LENGTH || !candidate.endsWith(ISSUE_DEPENDENCY_MARKER_SUFFIX)) {
83
+ return { status: "malformed", body: strippedBody };
84
+ }
85
+ const match = ISSUE_DEPENDENCY_MARKER_LINE_PATTERN.exec(candidate);
86
+ if (match === null)
87
+ return { status: "malformed", body: strippedBody };
88
+ let payload;
89
+ try {
90
+ payload = JSON.parse(match[1]);
91
+ }
92
+ catch {
93
+ return { status: "malformed", body: strippedBody };
94
+ }
95
+ if (!isRecord(payload) || payload.version !== ISSUE_DEPENDENCY_MARKER_VERSION) {
96
+ return { status: "unsupported-version", body: strippedBody };
97
+ }
98
+ if (!Object.prototype.hasOwnProperty.call(payload, "dependencies")) {
99
+ return { status: "malformed", body: strippedBody };
100
+ }
101
+ return { status: "valid", dependencies: payload.dependencies, body: strippedBody };
102
+ }
103
+ /**
104
+ * Recognize and remove a trailing template identity marker line without
105
+ * applying semantic parsing. Only a line starting with the exact reserved
106
+ * marker prefix is treated as a marker attempt at all; ordinary trailing
107
+ * HTML comments (e.g. PR template scaffolding) are left untouched here and
108
+ * handled by the existing comment-stripping path. Once the reserved prefix
109
+ * is detected, the line is never silently ignored as "absent" again: an
110
+ * oversized, truncated, or otherwise broken marker attempt fails closed as
111
+ * "malformed" instead of falling through to structural matching.
112
+ */
113
+ export function extractTemplateIdentityMarker(body) {
114
+ const source = normalizeSource(body);
115
+ const lines = source.split("\n");
116
+ let end = lines.length;
117
+ while (end > 0 && (lines[end - 1] ?? "").trim().length === 0)
118
+ end -= 1;
119
+ const candidate = end > 0 ? (lines[end - 1] ?? "").trim() : undefined;
120
+ if (candidate === undefined || !candidate.startsWith(TEMPLATE_IDENTITY_MARKER_PREFIX)) {
121
+ return { status: "absent", body: source };
122
+ }
123
+ const remaining = lines.slice(0, end - 1);
124
+ while (remaining.at(-1) !== undefined && (remaining.at(-1) ?? "").trim().length === 0)
125
+ remaining.pop();
126
+ const strippedBody = remaining.length === 0 ? "" : `${remaining.join("\n")}\n`;
127
+ if (candidate.length > TEMPLATE_IDENTITY_MARKER_MAX_LENGTH || !candidate.endsWith(TEMPLATE_IDENTITY_MARKER_SUFFIX)) {
128
+ return { status: "malformed", body: strippedBody };
129
+ }
130
+ const match = TEMPLATE_IDENTITY_MARKER_LINE_PATTERN.exec(candidate);
131
+ if (match === null)
132
+ return { status: "malformed", body: strippedBody };
133
+ let payload;
134
+ try {
135
+ payload = JSON.parse(match[1]);
136
+ }
137
+ catch {
138
+ return { status: "malformed", body: strippedBody };
139
+ }
140
+ if (!isTemplateIdentityMarkerShape(payload))
141
+ return { status: "malformed", body: strippedBody };
142
+ if (payload.version !== TEMPLATE_IDENTITY_MARKER_VERSION) {
143
+ return { status: "unsupported-version", marker: payload, body: strippedBody };
144
+ }
145
+ return { status: "valid", marker: payload, body: strippedBody };
146
+ }
147
+ function isTemplateIdentityMarkerShape(value) {
148
+ return (isRecord(value) &&
149
+ typeof value.version === "string" &&
150
+ (value.kind === "issue" || value.kind === "pull_request") &&
151
+ typeof value.path === "string" &&
152
+ value.path.trim().length > 0);
153
+ }
26
154
  /** Parse the documented JSON input envelope while keeping field semantics adapter-independent. */
27
155
  export function parseArtifactInputDocument(input) {
28
156
  if (!isRecord(input))
@@ -36,10 +164,15 @@ export function parseArtifactInputDocument(input) {
36
164
  if (input[key] !== undefined)
37
165
  metadata[key] = input[key];
38
166
  }
39
- const unknown = Object.keys(input).filter((key) => key !== "fields" && !Object.prototype.hasOwnProperty.call(metadata, key));
167
+ const dependencies = parseInputDependencies(input.dependencies);
168
+ const unknown = Object.keys(input).filter((key) => key !== "fields" && key !== "dependencies" && !Object.prototype.hasOwnProperty.call(metadata, key));
40
169
  if (unknown.length > 0)
41
170
  throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", `Unknown input property "${unknown[0]}".`, `$.${unknown[0]}`);
42
- return { fields: input.fields, metadata: parseMetadata(metadata) };
171
+ return {
172
+ fields: input.fields,
173
+ metadata: parseMetadata(metadata),
174
+ ...(dependencies === undefined ? {} : { dependencies }),
175
+ };
43
176
  }
44
177
  const reservedInBare = Object.keys(input).find((key) => metadataKeys.includes(key));
45
178
  if (reservedInBare !== undefined) {
@@ -47,24 +180,256 @@ export function parseArtifactInputDocument(input) {
47
180
  }
48
181
  return { fields: input, metadata: {} };
49
182
  }
183
+ function parseInputDependencies(input) {
184
+ if (input === undefined)
185
+ return undefined;
186
+ const result = validateIssueDependencies(input);
187
+ if (!result.valid) {
188
+ const first = result.violations[0];
189
+ const violations = result.violations.map((violation) => ({
190
+ ...violation,
191
+ path: prefixDependencyPath(violation.path),
192
+ }));
193
+ throw new ArtifactInputError("INPUT_DEPENDENCIES_INVALID", first?.message ?? "Issue dependencies are invalid.", prefixDependencyPath(first?.path ?? "$.dependencies"), violations);
194
+ }
195
+ return result.dependencies;
196
+ }
197
+ /** Prefix dependency violations when they cross the public input envelope boundary. */
198
+ function prefixDependencyPath(path) {
199
+ if (path === "$")
200
+ return "$.dependencies";
201
+ return path.startsWith("$.") ? `$.dependencies${path.slice(1)}` : "$.dependencies";
202
+ }
203
+ /** Adapt a parsed JSON envelope without granting it canonical status. */
204
+ export function adaptJsonArtifactCandidate(input) {
205
+ const document = parseArtifactInputDocument(input);
206
+ return { ...document, source: "json" };
207
+ }
208
+ /** Adapt internal structured fields to the same candidate shape as JSON. */
209
+ export function adaptFieldArtifactCandidate(fields, metadata = {}) {
210
+ if (!isRecord(fields)) {
211
+ throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", "Candidate fields must be an object.", "$.fields");
212
+ }
213
+ return { fields, metadata, source: "fields" };
214
+ }
215
+ /** Alias used by command adapters that call this input the CLI field path. */
216
+ export const adaptCliFieldCandidate = adaptFieldArtifactCandidate;
217
+ /** Generic adapter spelling for callers that already hold structured fields. */
218
+ export const adaptArtifactCandidate = adaptFieldArtifactCandidate;
219
+ /** Adapt an existing native artifact body through the repository parser. */
220
+ export function adaptMarkdownArtifactCandidate(contractInput, body) {
221
+ assertCanonicalContract(contractInput);
222
+ const contract = contractInput;
223
+ const parse = contract.artifactKind === "issue"
224
+ ? parseExistingIssueArtifact(contract, body)
225
+ : parseExistingPullRequestArtifact(contract, body);
226
+ if (!parse.parsed)
227
+ return { parsed: false, diagnostics: parse.diagnostics };
228
+ return {
229
+ parsed: true,
230
+ candidate: { fields: parse.values, metadata: {}, source: "markdown", dependencies: parse.dependencies },
231
+ diagnostics: parse.diagnostics,
232
+ };
233
+ }
234
+ /** Existing GitHub bodies use the same native Markdown adapter by design. */
235
+ export function adaptExistingArtifactCandidate(contractInput, body) {
236
+ const adapted = adaptMarkdownArtifactCandidate(contractInput, body);
237
+ return adapted.candidate === undefined
238
+ ? adapted
239
+ : { ...adapted, candidate: { ...adapted.candidate, source: "existing" } };
240
+ }
241
+ /**
242
+ * Reload a candidate against the selected canonical contract. Complete input
243
+ * takes the normal one-pass validator (and therefore may materialize contract
244
+ * defaults); incomplete/invalid input uses the bounded partial contract and
245
+ * exposes only accepted semantic values.
246
+ */
247
+ export function loadCanonicalArtifact(contractInput, candidateInput) {
248
+ assertCanonicalContract(contractInput);
249
+ const candidate = normalizeArtifactCandidate(candidateInput);
250
+ const dependencyValidation = contractInput.artifactKind === "issue"
251
+ ? validateIssueDependencies(candidate.dependencies)
252
+ : candidate.dependencies === undefined
253
+ ? { valid: true, dependencies: EMPTY_ISSUE_DEPENDENCIES, violations: [] }
254
+ : {
255
+ valid: false,
256
+ dependencies: EMPTY_ISSUE_DEPENDENCIES,
257
+ violations: [
258
+ {
259
+ code: "DEPENDENCIES_UNKNOWN_PROPERTY",
260
+ path: "$.dependencies",
261
+ message: "Issue dependencies are supported only for Issue artifacts.",
262
+ },
263
+ ],
264
+ };
265
+ if (!dependencyValidation.valid) {
266
+ throw new ArtifactInputError("INPUT_DEPENDENCIES_INVALID", dependencyValidation.violations[0]?.message ?? "Issue dependencies are invalid.", dependencyValidation.violations[0]?.path ?? "$.dependencies", dependencyValidation.violations);
267
+ }
268
+ const validation = validateSemanticInput(contractInput, candidate.fields);
269
+ if (validation.valid) {
270
+ const acceptedFields = Object.keys(validation.values)
271
+ .sort(compareStrings)
272
+ .map((field) => `$.fields.${field}`);
273
+ const diagnostics = createArtifactDiagnosticReport([], acceptedFields);
274
+ return {
275
+ valid: true,
276
+ complete: true,
277
+ canonical: validation.values,
278
+ canonicalJson: validation.values,
279
+ values: validation.values,
280
+ candidate,
281
+ acceptedFields,
282
+ missingFields: [],
283
+ invalidFields: [],
284
+ diagnostics,
285
+ violations: [],
286
+ dependencies: dependencyValidation.dependencies,
287
+ };
288
+ }
289
+ const partial = validatePartialSemanticInput(contractInput, candidate.fields);
290
+ // Partial classification intentionally asks repair callers for explicit
291
+ // values, including fields whose defaults can complete a full input. The
292
+ // loader's missingFields output instead describes why the complete
293
+ // validation failed, so default-backed fields must only appear when the
294
+ // full validator actually reports them as required and absent.
295
+ const missingFields = partial.missingFields.filter((issue) => validation.violations.some((violation) => violation.code === "INPUT_REQUIRED" && violation.path === `$.${issue.field}`));
296
+ const missingFieldPaths = new Set(missingFields.map((issue) => issue.path));
297
+ const diagnostics = createArtifactDiagnosticReport(partial.diagnostics.diagnostics.filter((diagnostic) => diagnostic.state !== "missing" || (diagnostic.path !== undefined && missingFieldPaths.has(diagnostic.path))), partial.acceptedFields);
298
+ return {
299
+ valid: false,
300
+ complete: false,
301
+ canonical: partial.values,
302
+ canonicalJson: partial.values,
303
+ values: partial.values,
304
+ candidate,
305
+ acceptedFields: partial.acceptedFields,
306
+ missingFields,
307
+ invalidFields: partial.invalidFields,
308
+ diagnostics,
309
+ violations: validation.violations,
310
+ dependencies: dependencyValidation.dependencies,
311
+ };
312
+ }
313
+ /** Explicitly named alias for callers that pass a candidate object. */
314
+ export const loadCanonicalCandidate = loadCanonicalArtifact;
315
+ /** Load a JSON representation through the canonical contract boundary. */
316
+ export function loadCanonicalJsonArtifact(contractInput, input) {
317
+ return loadCanonicalArtifact(contractInput, adaptJsonArtifactCandidate(input));
318
+ }
319
+ /** Load native Markdown through the same parser and canonical contract. */
320
+ export function loadCanonicalMarkdownArtifact(contractInput, body) {
321
+ const adapted = adaptMarkdownArtifactCandidate(contractInput, body);
322
+ if (!adapted.parsed || adapted.candidate === undefined) {
323
+ const candidate = { fields: {}, metadata: {}, source: "markdown" };
324
+ const diagnostics = markdownDiagnostics(adapted.diagnostics);
325
+ return {
326
+ valid: false,
327
+ complete: false,
328
+ canonical: {},
329
+ canonicalJson: {},
330
+ values: {},
331
+ candidate,
332
+ acceptedFields: [],
333
+ missingFields: [],
334
+ invalidFields: [],
335
+ diagnostics,
336
+ violations: [],
337
+ };
338
+ }
339
+ return loadCanonicalArtifact(contractInput, adapted.candidate);
340
+ }
341
+ /** Existing-body spelling retained so read/repair callers share one boundary. */
342
+ export function loadCanonicalExistingArtifact(contractInput, body) {
343
+ const adapted = adaptExistingArtifactCandidate(contractInput, body);
344
+ if (!adapted.parsed || adapted.candidate === undefined) {
345
+ const candidate = { fields: {}, metadata: {}, source: "existing" };
346
+ const diagnostics = markdownDiagnostics(adapted.diagnostics);
347
+ return {
348
+ valid: false,
349
+ complete: false,
350
+ canonical: {},
351
+ canonicalJson: {},
352
+ values: {},
353
+ candidate,
354
+ acceptedFields: [],
355
+ missingFields: [],
356
+ invalidFields: [],
357
+ diagnostics,
358
+ violations: [],
359
+ };
360
+ }
361
+ return loadCanonicalArtifact(contractInput, adapted.candidate);
362
+ }
363
+ function normalizeArtifactCandidate(input) {
364
+ if (isArtifactCandidate(input))
365
+ return input;
366
+ if (isRecord(input) && input.parsed === true && isArtifactCandidate(input.candidate))
367
+ return input.candidate;
368
+ if (isArtifactInputDocument(input))
369
+ return { ...input, source: "json" };
370
+ return adaptJsonArtifactCandidate(input);
371
+ }
372
+ function isArtifactCandidate(input) {
373
+ return (isRecord(input) &&
374
+ (input.source === "json" ||
375
+ input.source === "markdown" ||
376
+ input.source === "existing" ||
377
+ input.source === "fields") &&
378
+ Object.prototype.hasOwnProperty.call(input, "fields") &&
379
+ isRecord(input.metadata));
380
+ }
381
+ function isArtifactInputDocument(input) {
382
+ return isRecord(input) && Object.prototype.hasOwnProperty.call(input, "fields") && isRecord(input.metadata);
383
+ }
384
+ function markdownDiagnostics(diagnostics) {
385
+ const projected = diagnostics.slice(0, 32).map((diagnostic) => createArtifactDiagnostic({
386
+ state: "unsupported",
387
+ code: "FIELD_UNSUPPORTED",
388
+ detailCode: diagnostic.code === "EXISTING_AMBIGUOUS_TEMPLATE" ? "TEMPLATE_AMBIGUOUS" : "TEMPLATE_UNPARSEABLE",
389
+ reason: "unsupported",
390
+ path: diagnostic.path,
391
+ message: diagnostic.message,
392
+ recovery: [
393
+ {
394
+ action: diagnostic.code === "EXISTING_AMBIGUOUS_TEMPLATE" ? "select-template" : "retry",
395
+ path: diagnostic.path,
396
+ },
397
+ ],
398
+ }));
399
+ return createArtifactDiagnosticReport(projected, []);
400
+ }
401
+ /** Classify an artifact input envelope without applying semantic defaults. */
402
+ export function validatePartialArtifactInput(contractInput, input) {
403
+ assertCanonicalContract(contractInput);
404
+ return validatePartialSemanticInput(contractInput, parseArtifactInputDocument(input).fields);
405
+ }
406
+ /** Terminology alias for callers that treat validation as classification. */
407
+ export const classifyPartialArtifactInput = validatePartialArtifactInput;
408
+ /** Merge only a targeted field patch into a prior stateless partial result. */
409
+ export function repairPartialArtifactInput(contractInput, previous, patch) {
410
+ assertCanonicalContract(contractInput);
411
+ return repairPartialSemanticInput(contractInput, previous, patch);
412
+ }
413
+ /** Terminology alias for callers that describe targeted repair as a merge. */
414
+ export const mergePartialArtifactInput = repairPartialArtifactInput;
50
415
  export function renderIssueArtifact(contractInput, input) {
51
416
  assertCanonicalContract(contractInput);
52
417
  if (contractInput.artifactKind !== "issue")
53
418
  throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", "An Issue contract is required.");
54
- const validation = validateSemanticInput(contractInput, input);
55
- if (!validation.valid)
56
- throw new SemanticValidationError(validation.violations);
57
- return renderIssueBody(contractInput, validation.values);
419
+ const loaded = loadCanonicalArtifact(contractInput, input);
420
+ if (!loaded.valid)
421
+ throw new SemanticValidationError(loaded.violations);
422
+ return renderIssueBody(contractInput, loaded.canonical, loaded.dependencies);
58
423
  }
59
424
  export function renderPullRequestArtifact(contractInput, input) {
60
425
  assertCanonicalContract(contractInput);
61
426
  if (contractInput.artifactKind !== "pull_request") {
62
427
  throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", "A pull request contract is required.");
63
428
  }
64
- const validation = validateSemanticInput(contractInput, input);
65
- if (!validation.valid)
66
- throw new SemanticValidationError(validation.violations);
67
- return renderPullRequestBody(contractInput, validation.values);
429
+ const loaded = loadCanonicalArtifact(contractInput, input);
430
+ if (!loaded.valid)
431
+ throw new SemanticValidationError(loaded.violations);
432
+ return renderPullRequestBody(contractInput, loaded.canonical);
68
433
  }
69
434
  /** Construct the only values accepted by the GitHub mutation adapter. */
70
435
  export function prepareIssueArtifact(contractInput, input) {
@@ -72,13 +437,18 @@ export function prepareIssueArtifact(contractInput, input) {
72
437
  if (contractInput.artifactKind !== "issue")
73
438
  throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", "An Issue contract is required.");
74
439
  requireTrustedProvenance(contractInput);
75
- const validation = validateSemanticInput(contractInput, input.fields);
76
- if (!validation.valid)
77
- throw new SemanticValidationError(validation.violations);
78
- const title = requiredMetadataString(input.metadata.title ?? contractInput.nativeMetadata.title, "title");
440
+ const loaded = loadCanonicalArtifact(contractInput, input);
441
+ if (!loaded.valid)
442
+ throw new SemanticValidationError(loaded.violations);
443
+ const title = requiredCreateTitle(input.metadata.title, contractInput.nativeMetadata.title);
79
444
  const labels = mergeIssueLabels(contractInput.nativeMetadata.labels, input.metadata.labels);
80
- const body = renderIssueBody(contractInput, validation.values);
81
- verifyRenderedRoundTrip(contractInput, validation.values, body, "issue");
445
+ const expectedMetadata = {
446
+ title,
447
+ ...(labels === undefined ? {} : { labels: [...labels] }),
448
+ ...(input.metadata.assignees === undefined ? {} : { assignees: [...input.metadata.assignees] }),
449
+ };
450
+ const body = renderIssueBody(contractInput, loaded.canonical, loaded.dependencies);
451
+ verifyRenderedRoundTrip(contractInput, loaded.canonical, body, "issue", loaded.dependencies);
82
452
  const artifact = createValidatedRenderedIssueArtifact({
83
453
  kind: "issue",
84
454
  title,
@@ -87,7 +457,8 @@ export function prepareIssueArtifact(contractInput, input) {
87
457
  ...(labels === undefined ? {} : { labels }),
88
458
  ...(input.metadata.assignees === undefined ? {} : { assignees: input.metadata.assignees }),
89
459
  });
90
- return { input, validation, artifact };
460
+ verifyIssueMetadataRoundTrip(expectedMetadata, artifact);
461
+ return { input, validation: semanticValidationFromLoad(loaded), artifact };
91
462
  }
92
463
  export function preparePullRequestArtifact(contractInput, input) {
93
464
  assertCanonicalContract(contractInput);
@@ -95,14 +466,14 @@ export function preparePullRequestArtifact(contractInput, input) {
95
466
  throw new ArtifactInputError("INPUT_DOCUMENT_INVALID", "A pull request contract is required.");
96
467
  }
97
468
  requireTrustedProvenance(contractInput);
98
- const validation = validateSemanticInput(contractInput, input.fields);
99
- if (!validation.valid)
100
- throw new SemanticValidationError(validation.violations);
101
- const title = requiredMetadataString(input.metadata.title, "title");
469
+ const loaded = loadCanonicalArtifact(contractInput, input);
470
+ if (!loaded.valid)
471
+ throw new SemanticValidationError(loaded.violations);
472
+ const title = requiredCreateTitle(input.metadata.title, contractInput.nativeMetadata.title);
102
473
  const head = requiredMetadataString(input.metadata.head, "head");
103
474
  const base = requiredMetadataString(input.metadata.base, "base");
104
- const body = renderPullRequestBody(contractInput, validation.values);
105
- verifyRenderedRoundTrip(contractInput, validation.values, body, "pull_request");
475
+ const body = renderPullRequestBody(contractInput, loaded.canonical);
476
+ verifyRenderedRoundTrip(contractInput, loaded.canonical, body, "pull_request");
106
477
  const artifact = createValidatedRenderedPullRequestArtifact({
107
478
  kind: "pull_request",
108
479
  title,
@@ -115,7 +486,8 @@ export function preparePullRequestArtifact(contractInput, input) {
115
486
  ? {}
116
487
  : { maintainerCanModify: input.metadata.maintainerCanModify }),
117
488
  });
118
- return { input, validation, artifact };
489
+ verifyPullRequestMetadataRoundTrip(input.metadata, artifact);
490
+ return { input, validation: semanticValidationFromLoad(loaded), artifact };
119
491
  }
120
492
  export function parseExistingIssueArtifact(contractInput, body) {
121
493
  assertCanonicalContract(contractInput);
@@ -130,10 +502,66 @@ export function parseExistingPullRequestArtifact(contractInput, body) {
130
502
  }
131
503
  return parseRenderedBody(contractInput, body ?? "", undefined, true);
132
504
  }
133
- export function validateExistingIssueArtifact(contractInput, body) {
505
+ /**
506
+ * Recover field values from a malformed or wrong-template body without
507
+ * weakening the strict existing-artifact parser. The section boundaries and
508
+ * field decoding are the same parser primitives used by strict parsing; only
509
+ * the order/complete-structure requirement is relaxed for an explicitly
510
+ * selected repair target.
511
+ */
512
+ export function recoverExistingArtifactValues(contractInput, body) {
513
+ assertCanonicalContract(contractInput);
514
+ const contract = contractInput;
515
+ const strict = contract.artifactKind === "issue"
516
+ ? parseExistingIssueArtifact(contract, body)
517
+ : parseExistingPullRequestArtifact(contract, body);
518
+ if (strict.parsed)
519
+ return { values: strict.values, dependencies: strict.dependencies, diagnostics: strict.diagnostics };
520
+ const dependencyMarker = contract.artifactKind === "issue" ? extractIssueDependencyMarker(body ?? "") : undefined;
521
+ const markerFreeBody = extractTemplateIdentityMarker(dependencyMarker?.body ?? body ?? "").body;
522
+ const stripComments = contract.artifactKind === "pull_request";
523
+ const source = normalizeSource(stripComments ? removeHtmlComments(markerFreeBody) : markerFreeBody);
524
+ const blocks = headingBlocks(source);
525
+ const values = {};
526
+ const expectedTitles = new Map();
527
+ for (const section of contract.sections) {
528
+ if (section.kind !== "input")
529
+ continue;
530
+ const field = section.fields[0];
531
+ const title = section.title ?? field?.label;
532
+ if (field === undefined || title === undefined)
533
+ continue;
534
+ const expectedTitle = escapeHeading(title);
535
+ expectedTitles.set(expectedTitle, (expectedTitles.get(expectedTitle) ?? 0) + 1);
536
+ }
537
+ for (const section of contract.sections) {
538
+ if (section.kind !== "input")
539
+ continue;
540
+ const field = section.fields[0];
541
+ const title = section.title ?? field?.label;
542
+ if (field === undefined || title === undefined)
543
+ continue;
544
+ const expectedTitle = escapeHeading(title);
545
+ if (expectedTitles.get(expectedTitle) !== 1)
546
+ continue;
547
+ const candidates = blocks.filter((block) => block.title === expectedTitle);
548
+ if (candidates.length !== 1)
549
+ continue;
550
+ const block = candidates[0];
551
+ const parsed = parseFieldLines(field, block.body, `$.${field.id}`, stripComments, contract.artifactKind === "issue");
552
+ // parseFieldLines may retain known checklist selections alongside a
553
+ // bounded structural diagnostic. The canonical loader below decides
554
+ // whether such a partial value is semantically usable.
555
+ if (parsed.value !== undefined)
556
+ values[field.id] = parsed.value;
557
+ }
558
+ const dependencies = dependencyMarker?.dependencies;
559
+ return { values, dependencies, diagnostics: strict.diagnostics };
560
+ }
561
+ export function validateExistingIssueArtifact(contractInput, body, subject) {
134
562
  assertCanonicalContract(contractInput);
135
563
  const parse = parseExistingIssueArtifact(contractInput, body);
136
- return validateParsedArtifact(contractInput, parse);
564
+ return validateParsedArtifact(contractInput, parse, subject);
137
565
  }
138
566
  export function validateExistingPullRequestArtifact(contractInput, body) {
139
567
  assertCanonicalContract(contractInput);
@@ -147,6 +575,7 @@ export function projectExistingArtifact(result) {
147
575
  projection: result.valid ? "canonical" : "unavailable",
148
576
  classification: result.classification,
149
577
  ...(result.valid ? { fields: result.parse.values } : {}),
578
+ ...(result.valid && result.parse.dependencies !== undefined ? { dependencies: result.parse.dependencies } : {}),
150
579
  diagnostics: result.parse.diagnostics,
151
580
  ...(result.classification === "semantic" ? { violations: result.violations } : {}),
152
581
  ...(result.attemptedTemplates === undefined ? {} : { attemptedTemplates: result.attemptedTemplates }),
@@ -210,7 +639,11 @@ export function validateRequiredMetadataString(value, key) {
210
639
  }
211
640
  export async function validateExistingIssueFromAdapter(reader, contract, issueNumber) {
212
641
  const issue = await reader.getIssue(issueNumber);
213
- return { number: issueNumber, url: issue.url, result: validateExistingIssueArtifact(contract, issue.body) };
642
+ return {
643
+ number: issueNumber,
644
+ url: issue.url,
645
+ result: validateExistingIssueArtifact(contract, issue.body, issueReferenceFromUrl(issue.url, issueNumber, issue.repositoryId, issue.repositoryHost)),
646
+ };
214
647
  }
215
648
  export async function validateExistingPullRequestFromAdapter(reader, contract, pullRequestNumber) {
216
649
  const pullRequest = await reader.getPullRequest(pullRequestNumber);
@@ -220,83 +653,292 @@ export async function validateExistingPullRequestFromAdapter(reader, contract, p
220
653
  result: validateExistingPullRequestArtifact(contract, pullRequest.body),
221
654
  };
222
655
  }
223
- function validateParsedArtifact(contract, parse) {
656
+ function validateParsedArtifact(contract, parse, subject) {
224
657
  if (!parse.parsed) {
225
658
  const classification = parse.diagnostics.some((diagnostic) => diagnostic.code === "EXISTING_WRONG_TEMPLATE")
226
659
  ? "wrong-template"
227
660
  : "unparseable";
228
661
  return { valid: false, classification, parse, violations: parse.diagnostics };
229
662
  }
230
- const semantic = validateSemanticInput(contract, parse.values);
663
+ const dependencyInput = parse.dependencyInput ?? parse.dependencies;
664
+ const dependencyValidation = contract.artifactKind === "issue" ? validateIssueDependencies(dependencyInput, subject) : undefined;
665
+ if (dependencyValidation !== undefined && !dependencyValidation.valid) {
666
+ return {
667
+ valid: false,
668
+ classification: "semantic",
669
+ parse,
670
+ violations: dependencyValidation.violations.map((violation) => ({
671
+ code: "INPUT_DEPENDENCY",
672
+ path: prefixDependencyPath(violation.path),
673
+ message: violation.message,
674
+ })),
675
+ };
676
+ }
677
+ const normalizedParse = dependencyValidation === undefined
678
+ ? parse
679
+ : { ...parse, dependencies: dependencyValidation.dependencies, dependencyInput: undefined };
680
+ const semantic = loadCanonicalArtifact(contract, {
681
+ fields: parse.values,
682
+ metadata: {},
683
+ source: "existing",
684
+ dependencies: dependencyValidation?.dependencies ?? parse.dependencies,
685
+ });
231
686
  return {
232
687
  valid: semantic.valid,
233
688
  classification: semantic.valid ? "valid" : "semantic",
234
- parse,
689
+ parse: normalizedParse,
235
690
  violations: semantic.violations,
236
691
  };
237
692
  }
238
693
  function requireTrustedProvenance(contract) {
239
694
  if (contract.provenance === undefined) {
240
695
  throw new ArtifactPreparationError("ARTIFACT_PROVENANCE_MISSING", "Mutation preparation requires a contract bound to trusted repository governance.", [
241
- {
242
- code: "ROUND_TRIP_PARSE",
696
+ createArtifactDiagnostic({
697
+ state: "unrecoverable",
698
+ code: "ARTIFACT_UNRECOVERABLE",
699
+ reason: "unrecoverable",
243
700
  path: "$.provenance",
244
701
  message: "The compiled contract has no trusted repository/ref provenance.",
245
- },
702
+ recovery: [{ action: "retry", path: "$.provenance" }],
703
+ }),
246
704
  ]);
247
705
  }
248
706
  }
249
- function verifyRenderedRoundTrip(contract, expectedValues, body, kind) {
707
+ function verifyPullRequestMetadataRoundTrip(input, artifact) {
708
+ const expected = {
709
+ title: input.title,
710
+ head: input.head,
711
+ base: input.base,
712
+ ...(input.draft === undefined ? {} : { draft: input.draft }),
713
+ ...(input.maintainerCanModify === undefined ? {} : { maintainerCanModify: input.maintainerCanModify }),
714
+ };
715
+ const actual = {
716
+ title: artifact.title,
717
+ head: artifact.head,
718
+ base: artifact.base,
719
+ ...(artifact.draft === undefined ? {} : { draft: artifact.draft }),
720
+ ...(artifact.maintainerCanModify === undefined ? {} : { maintainerCanModify: artifact.maintainerCanModify }),
721
+ };
722
+ const mismatches = [];
723
+ for (const key of Object.keys(expected).sort(compareStrings)) {
724
+ const path = `$.metadata.${key}`;
725
+ if (stableValue(expected[key]) === stableValue(actual[key]))
726
+ continue;
727
+ mismatches.push(createArtifactDiagnostic({
728
+ state: "conflicting",
729
+ code: "FIELD_CONFLICT",
730
+ detailCode: "FIELD_VALUE_CONFLICT",
731
+ reason: "conflict",
732
+ path,
733
+ message: "Prepared pull request metadata changed before the mutation boundary.",
734
+ expected: createFieldEvidence(path, expected[key]),
735
+ actual: createFieldEvidence(path, actual[key]),
736
+ recovery: [{ action: "repair", path, hint: "Repair the pull request metadata projection." }],
737
+ }));
738
+ }
739
+ if (mismatches.length > 0) {
740
+ throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", "Prepared pull request metadata did not preserve its validated values.", mismatches);
741
+ }
742
+ }
743
+ /** @internal Validate the canonical Issue metadata handoff before mutation. */
744
+ export function verifyIssueMetadataRoundTrip(expected, artifact) {
745
+ const actual = {
746
+ title: artifact.title,
747
+ ...(artifact.labels === undefined ? {} : { labels: artifact.labels }),
748
+ ...(artifact.assignees === undefined ? {} : { assignees: artifact.assignees }),
749
+ };
750
+ const mismatches = [];
751
+ const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort(compareStrings);
752
+ for (const key of keys) {
753
+ const expectedPresent = Object.prototype.hasOwnProperty.call(expected, key);
754
+ const actualPresent = Object.prototype.hasOwnProperty.call(actual, key);
755
+ const path = `$.metadata.${key}`;
756
+ if (expectedPresent && actualPresent && stableValue(expected[key]) === stableValue(actual[key]))
757
+ continue;
758
+ mismatches.push(createArtifactDiagnostic({
759
+ state: "conflicting",
760
+ code: "FIELD_CONFLICT",
761
+ detailCode: "FIELD_VALUE_CONFLICT",
762
+ reason: "conflict",
763
+ path,
764
+ message: "Prepared issue metadata changed before the mutation boundary.",
765
+ expected: createFieldEvidence(path, expectedPresent ? expected[key] : undefined),
766
+ actual: createFieldEvidence(path, actualPresent ? actual[key] : undefined),
767
+ recovery: [{ action: "repair", path, hint: "Repair the issue metadata projection." }],
768
+ }));
769
+ }
770
+ if (mismatches.length > 0) {
771
+ throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", "Prepared issue metadata did not preserve its validated values.", mismatches);
772
+ }
773
+ }
774
+ function verifyRenderedRoundTrip(contract, expectedValues, body, kind, expectedDependencies = undefined) {
250
775
  const parsed = kind === "issue" ? parseExistingIssueArtifact(contract, body) : parseExistingPullRequestArtifact(contract, body);
251
776
  if (!parsed.parsed) {
252
- throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", `Rendered ${kind} artifact did not reparse under the compiled contract.`, parsed.diagnostics.map((diagnostic) => ({
253
- code: "ROUND_TRIP_PARSE",
254
- path: diagnostic.path,
255
- message: diagnostic.message,
256
- })));
777
+ throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", `Rendered ${kind} artifact did not reparse under the compiled contract.`, roundTripParseDiagnostics(contract, expectedValues, parsed.diagnostics));
257
778
  }
258
779
  const reconstructed = validateSemanticInput(contract, parsed.values);
259
780
  if (!reconstructed.valid) {
260
- throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", `Rendered ${kind} artifact failed semantic validation after reparsing.`, reconstructed.violations.map((violation) => ({
261
- code: "ROUND_TRIP_SEMANTIC",
262
- path: violation.path,
263
- message: violation.message,
264
- })));
781
+ throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", `Rendered ${kind} artifact failed semantic validation after reparsing.`, roundTripSemanticDiagnostics(contract, expectedValues, parsed.values, reconstructed.violations));
782
+ }
783
+ const mismatches = [...compareMaterializedValues(expectedValues, reconstructed.values)];
784
+ if (kind === "issue") {
785
+ const actualDependencies = parsed.dependencies ?? EMPTY_ISSUE_DEPENDENCIES;
786
+ const expected = expectedDependencies ?? EMPTY_ISSUE_DEPENDENCIES;
787
+ if (stableValue(expected) !== stableValue(actualDependencies)) {
788
+ mismatches.push(createArtifactDiagnostic({
789
+ state: "conflicting",
790
+ code: "FIELD_CONFLICT",
791
+ detailCode: "FIELD_VALUE_CONFLICT",
792
+ reason: "conflict",
793
+ path: "$.dependencies",
794
+ message: "Rendered artifact changed its Issue dependency semantics.",
795
+ expected: createFieldEvidence("$.dependencies", expected),
796
+ actual: createFieldEvidence("$.dependencies", actualDependencies),
797
+ recovery: [{ action: "repair", path: "$.dependencies" }],
798
+ }));
799
+ }
265
800
  }
266
- const mismatches = compareMaterializedValues(expectedValues, reconstructed.values);
267
801
  if (mismatches.length > 0) {
268
802
  throw new ArtifactPreparationError("ARTIFACT_ROUND_TRIP_INVALID", `Rendered ${kind} artifact did not preserve its validated semantic values.`, mismatches);
269
803
  }
270
804
  }
805
+ function roundTripParseDiagnostics(contract, expectedValues, diagnostics) {
806
+ const projected = diagnostics.slice(0, MAX_ARTIFACT_DIAGNOSTICS).map((diagnostic) => {
807
+ const fieldId = semanticFieldId(contract, diagnostic.path);
808
+ const path = fieldId === undefined ? semanticDiagnosticPath(contract, diagnostic.path) : fieldPath(fieldId);
809
+ return createArtifactDiagnostic({
810
+ state: "unsupported",
811
+ code: "FIELD_UNSUPPORTED",
812
+ detailCode: "TEMPLATE_UNPARSEABLE",
813
+ reason: "unsupported",
814
+ ...(path === undefined ? {} : { path }),
815
+ message: roundTripParseMessage(diagnostic.code),
816
+ ...(fieldId === undefined
817
+ ? {}
818
+ : {
819
+ expected: createFieldEvidence(fieldPath(fieldId), expectedValues[fieldId]),
820
+ actual: createFieldEvidence(fieldPath(fieldId), undefined),
821
+ }),
822
+ recovery: path === undefined ? [{ action: "retry" }] : [{ action: "repair", path }],
823
+ });
824
+ });
825
+ return projected.length > 0
826
+ ? projected
827
+ : [
828
+ createArtifactDiagnostic({
829
+ state: "unrecoverable",
830
+ code: "ARTIFACT_UNRECOVERABLE",
831
+ reason: "unrecoverable",
832
+ message: "Rendered artifact could not be reparsed under the compiled contract.",
833
+ recovery: [{ action: "retry" }],
834
+ }),
835
+ ];
836
+ }
837
+ function roundTripSemanticDiagnostics(contract, expectedValues, actualValues, violations) {
838
+ const projected = violations.slice(0, MAX_ARTIFACT_DIAGNOSTICS).map((violation) => {
839
+ const fieldId = semanticFieldId(contract, violation.path);
840
+ const path = fieldId === undefined ? semanticDiagnosticPath(contract, violation.path) : fieldPath(fieldId);
841
+ const evidencePath = fieldId === undefined ? path : fieldPath(fieldId);
842
+ return createArtifactDiagnostic({
843
+ state: "invalid",
844
+ code: "FIELD_INVALID",
845
+ detailCode: violation.code === "INPUT_TYPE" ? "FIELD_TYPE_MISMATCH" : "FIELD_CONSTRAINT_VIOLATION",
846
+ reason: violation.code === "INPUT_TYPE" ? "type" : "constraint",
847
+ ...(path === undefined ? {} : { path }),
848
+ message: violation.code === "INPUT_TYPE"
849
+ ? "Reparsed semantic value has an unsupported type."
850
+ : "Reparsed semantic value violates a compiled constraint.",
851
+ ...(evidencePath === undefined
852
+ ? {}
853
+ : {
854
+ expected: createFieldEvidence(evidencePath, fieldId === undefined ? undefined : expectedValues[fieldId]),
855
+ actual: createFieldEvidence(evidencePath, fieldId === undefined ? undefined : actualValues[fieldId]),
856
+ }),
857
+ recovery: path === undefined ? [{ action: "repair" }] : [{ action: "repair", path }],
858
+ });
859
+ });
860
+ return projected.length > 0
861
+ ? projected
862
+ : [
863
+ createArtifactDiagnostic({
864
+ state: "unrecoverable",
865
+ code: "ARTIFACT_UNRECOVERABLE",
866
+ reason: "unrecoverable",
867
+ message: "Reparsed artifact failed semantic validation under the compiled contract.",
868
+ recovery: [{ action: "retry" }],
869
+ }),
870
+ ];
871
+ }
271
872
  function compareMaterializedValues(expected, actual) {
272
873
  const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort(compareStrings);
273
874
  const diagnostics = [];
274
875
  for (const key of keys) {
275
876
  const expectedPresent = Object.prototype.hasOwnProperty.call(expected, key);
276
877
  const actualPresent = Object.prototype.hasOwnProperty.call(actual, key);
277
- const path = `$.${key}`;
878
+ const path = fieldPath(key);
278
879
  if (!expectedPresent || !actualPresent) {
279
- diagnostics.push({
280
- code: "ROUND_TRIP_MISMATCH",
880
+ diagnostics.push(createArtifactDiagnostic({
881
+ state: "conflicting",
882
+ code: "FIELD_CONFLICT",
883
+ detailCode: "FIELD_VALUE_CONFLICT",
884
+ reason: "conflict",
281
885
  path,
282
- message: "Rendered artifact changed whether a semantic field was materialized.",
283
- ...(expectedPresent ? { expected: expected[key] } : {}),
284
- ...(actualPresent ? { actual: actual[key] } : {}),
285
- });
886
+ message: "Rendered artifact changed whether this semantic field was materialized.",
887
+ expected: createFieldEvidence(path, expectedPresent ? expected[key] : undefined),
888
+ actual: createFieldEvidence(path, actualPresent ? actual[key] : undefined),
889
+ recovery: [{ action: "repair", path, hint: "Repair the renderer/parser mapping for this field." }],
890
+ }));
286
891
  continue;
287
892
  }
288
893
  if (stableValue(expected[key]) !== stableValue(actual[key])) {
289
- diagnostics.push({
290
- code: "ROUND_TRIP_MISMATCH",
894
+ diagnostics.push(createArtifactDiagnostic({
895
+ state: "conflicting",
896
+ code: "FIELD_CONFLICT",
897
+ detailCode: "FIELD_VALUE_CONFLICT",
898
+ reason: "conflict",
291
899
  path,
292
- message: "Rendered artifact changed a materialized semantic value.",
293
- expected: expected[key],
294
- actual: actual[key],
295
- });
900
+ message: "Rendered artifact changed this materialized semantic value.",
901
+ expected: createFieldEvidence(path, expected[key]),
902
+ actual: createFieldEvidence(path, actual[key]),
903
+ recovery: [{ action: "repair", path, hint: "Repair the renderer/parser mapping for this field." }],
904
+ }));
296
905
  }
297
906
  }
298
907
  return diagnostics;
299
908
  }
909
+ function fieldPath(fieldId) {
910
+ return `$.fields.${fieldId}`;
911
+ }
912
+ function semanticFieldId(contract, path) {
913
+ const fields = contract.sections.flatMap((section) => section.fields);
914
+ for (const field of fields) {
915
+ if (path === `$.${field.id}` || path.startsWith(`$.${field.id}[`))
916
+ return field.id;
917
+ }
918
+ const sectionId = /^\$\.sections\.([^.[\]]+)/u.exec(path)?.[1];
919
+ return sectionId === undefined
920
+ ? undefined
921
+ : contract.sections.find((section) => section.id === sectionId)?.fields[0]?.id;
922
+ }
923
+ function semanticDiagnosticPath(contract, path) {
924
+ const sectionId = /^\$\.sections\.([^.[\]]+)/u.exec(path)?.[1];
925
+ if (sectionId !== undefined && contract.sections.some((section) => section.id === sectionId)) {
926
+ return `$.sections.${sectionId}`;
927
+ }
928
+ return path === "$" ? undefined : path.startsWith("$.artifact") ? undefined : path;
929
+ }
930
+ function roundTripParseMessage(code) {
931
+ switch (code) {
932
+ case "EXISTING_UNKNOWN_CHECKLIST_ITEM":
933
+ return "Rendered artifact contains an undeclared checklist item.";
934
+ case "EXISTING_EXTRA_CONTENT":
935
+ return "Rendered artifact contains content outside the compiled template structure.";
936
+ case "EXISTING_WRONG_TEMPLATE":
937
+ return "Rendered artifact does not match the compiled template structure.";
938
+ default:
939
+ return "Rendered artifact does not match the compiled field representation.";
940
+ }
941
+ }
300
942
  function stableValue(value) {
301
943
  if (value === undefined)
302
944
  return "undefined";
@@ -310,7 +952,18 @@ function stableValue(value) {
310
952
  .map((key) => `${JSON.stringify(key)}:${stableValue(record[key])}`)
311
953
  .join(",")}}`;
312
954
  }
313
- function renderIssueBody(contract, values) {
955
+ function issueReferenceFromUrl(url, number, repositoryId, repositoryHost) {
956
+ const match = /^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/issues\/[1-9][0-9]*$/u.exec(url);
957
+ if (match === null || repositoryId === undefined)
958
+ return undefined;
959
+ return {
960
+ repositoryHost: (repositoryHost ?? match[1]).toLocaleLowerCase("en-US"),
961
+ repositoryId,
962
+ repository: `${match[2]}/${match[3]}`.toLocaleLowerCase("en-US"),
963
+ number,
964
+ };
965
+ }
966
+ function renderIssueBody(contract, values, dependencies = undefined) {
314
967
  const blocks = [];
315
968
  for (let sectionIndex = 0; sectionIndex < contract.sections.length; sectionIndex += 1) {
316
969
  const section = contract.sections[sectionIndex];
@@ -329,7 +982,11 @@ function renderIssueBody(contract, values) {
329
982
  .join("\n\n");
330
983
  blocks.push([`### ${escapeHeading(title)}`, body].filter((part) => part.length > 0).join("\n\n"));
331
984
  }
332
- return `${blocks.join("\n\n")}\n`;
985
+ const marker = renderTemplateIdentityMarker(contract);
986
+ const dependencyMarker = dependencies !== undefined && (dependencies.blockedBy.length > 0 || dependencies.blocks.length > 0)
987
+ ? `\n${renderIssueDependencyMarker(dependencies)}`
988
+ : "";
989
+ return `${blocks.join("\n\n")}\n\n${marker}${dependencyMarker}\n`;
333
990
  }
334
991
  function renderPullRequestBody(contract, values) {
335
992
  const blocks = [];
@@ -350,7 +1007,7 @@ function renderPullRequestBody(contract, values) {
350
1007
  .filter(Boolean);
351
1008
  blocks.push([`${"#".repeat(level)} ${escapeHeading(title)}`, ...rendered].join("\n\n"));
352
1009
  }
353
- return `${blocks.join("\n\n")}\n`;
1010
+ return `${blocks.join("\n\n")}\n\n${renderTemplateIdentityMarker(contract)}\n`;
354
1011
  }
355
1012
  function renderDocumentation(section, content) {
356
1013
  const level = section.render.headingLevel ?? section.nativeMetadata.headingLevel;
@@ -360,10 +1017,16 @@ function renderDocumentation(section, content) {
360
1017
  }
361
1018
  function renderFieldValue(field, value, kind) {
362
1019
  if (field.type === "string" || field.type === "enum") {
1020
+ if (typeof value === "string" && value.length === 0) {
1021
+ return field.nativeMetadata.render === undefined
1022
+ ? EXPLICIT_EMPTY_STRING_MARKER
1023
+ : renderCodeBlock(EXPLICIT_EMPTY_STRING_MARKER, field.nativeMetadata.render);
1024
+ }
363
1025
  if (typeof value === "string" && (kind !== "issue" || value.trim().length > 0)) {
1026
+ const renderedValue = kind === "issue" ? issueNativeValue(field, value) : value;
364
1027
  return field.nativeMetadata.render === undefined
365
- ? escapeMarkdownValue(value)
366
- : renderCodeBlock(value, field.nativeMetadata.render);
1028
+ ? escapeMarkdownValue(renderedValue)
1029
+ : renderCodeBlock(renderedValue, field.nativeMetadata.render);
367
1030
  }
368
1031
  if (kind === "pull_request")
369
1032
  return field.nativeMetadata.placeholder ?? "";
@@ -376,20 +1039,53 @@ function renderFieldValue(field, value, kind) {
376
1039
  return kind === "issue" ? GITHUB_NO_RESPONSE : "";
377
1040
  if (value.length === 0)
378
1041
  return kind === "issue" ? GITHUB_NO_RESPONSE : "";
1042
+ const renderedValues = kind === "issue"
1043
+ ? value.map((entry) => (typeof entry === "string" ? issueNativeValue(field, entry) : String(entry)))
1044
+ : value.map((entry) => String(entry));
379
1045
  if (field.nativeMetadata.multiple === true)
380
- return value.map((entry) => escapeMarkdownValue(String(entry))).join(", ");
381
- return value.map((entry) => `- ${escapeMarkdownValue(String(entry))}`).join("\n");
1046
+ return renderedValues.map((entry) => escapeMarkdownValue(entry)).join(", ");
1047
+ return renderedValues.map((entry) => `- ${escapeMarkdownValue(entry)}`).join("\n");
382
1048
  }
383
1049
  const selected = new Set(Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : []);
384
1050
  const placeholder = kind === "pull_request" ? field.nativeMetadata.placeholder : undefined;
385
1051
  const lines = field.items.map((item) => `- [${selected.has(item.id) ? "x" : " "}] ${escapeMarkdownValue(item.label)}`);
386
1052
  return [placeholder === undefined ? "" : placeholder, lines.join("\n")].filter(Boolean).join("\n\n");
387
1053
  }
1054
+ /** Map canonical Issue semantic values to the labels shown by GitHub Issue Forms. */
1055
+ function issueNativeValue(field, value) {
1056
+ if (field.type === "enum")
1057
+ return field.options.find((option) => option.value === value)?.label ?? value;
1058
+ if (field.type === "array")
1059
+ return field.items.options?.find((option) => option.value === value)?.label ?? value;
1060
+ return value;
1061
+ }
1062
+ /** Map GitHub Issue Form labels back to canonical semantic values. */
1063
+ function issueSemanticValue(field, value) {
1064
+ if (value === undefined)
1065
+ return undefined;
1066
+ if (field.type === "enum")
1067
+ return field.options.find((option) => option.label === value)?.value ?? value;
1068
+ if (field.type === "array")
1069
+ return field.items.options?.find((option) => option.label === value)?.value ?? value;
1070
+ return value;
1071
+ }
388
1072
  function parseRenderedBody(contract, body, issueHeadingLevel, stripComments) {
389
- const source = normalizeSource(stripComments ? removeHtmlComments(body) : body);
1073
+ const dependencyMarker = contract.artifactKind === "issue" ? extractIssueDependencyMarker(body) : { status: "absent", body };
1074
+ const markerFreeBody = extractTemplateIdentityMarker(dependencyMarker.body).body;
1075
+ const source = normalizeSource(stripComments ? removeHtmlComments(markerFreeBody) : markerFreeBody);
390
1076
  const lines = source.split("\n");
391
1077
  const values = {};
392
1078
  const diagnostics = [];
1079
+ const dependencies = contract.artifactKind === "issue" ? (dependencyMarker.dependencies ?? EMPTY_ISSUE_DEPENDENCIES) : undefined;
1080
+ if (dependencyMarker.status === "malformed" || dependencyMarker.status === "unsupported-version") {
1081
+ diagnostics.push({
1082
+ code: "EXISTING_UNPARSEABLE",
1083
+ path: "$.dependencies",
1084
+ message: dependencyMarker.status === "unsupported-version"
1085
+ ? "Issue dependency marker uses an unsupported version."
1086
+ : "Issue dependency marker is malformed or semantically invalid.",
1087
+ });
1088
+ }
393
1089
  let cursor = 0;
394
1090
  for (let sectionIndex = 0; sectionIndex < contract.sections.length; sectionIndex += 1) {
395
1091
  const section = contract.sections[sectionIndex];
@@ -467,7 +1163,7 @@ function parseRenderedBody(contract, body, issueHeadingLevel, stripComments) {
467
1163
  }
468
1164
  }
469
1165
  const fieldLines = trimLineRange(lines.slice(contentStart, fieldEnd));
470
- const parsed = parseFieldLines(field, fieldLines, `$.${field.id}`, stripComments);
1166
+ const parsed = parseFieldLines(field, fieldLines, `$.${field.id}`, stripComments, issueHeadingLevel !== undefined);
471
1167
  diagnostics.push(...parsed.diagnostics);
472
1168
  if (parsed.value !== undefined)
473
1169
  values[field.id] = parsed.value;
@@ -485,9 +1181,39 @@ function parseRenderedBody(contract, body, issueHeadingLevel, stripComments) {
485
1181
  }
486
1182
  if (diagnostics.length > 0)
487
1183
  return { parsed: false, values: {}, diagnostics };
488
- return { parsed: true, values, diagnostics: [] };
1184
+ return {
1185
+ parsed: true,
1186
+ values,
1187
+ dependencies,
1188
+ ...(dependencyMarker.status === "valid" ? { dependencyInput: dependencyMarker.dependencies } : {}),
1189
+ diagnostics: [],
1190
+ };
1191
+ }
1192
+ function headingBlocks(source) {
1193
+ const lines = source.split("\n");
1194
+ const starts = [];
1195
+ let openFence;
1196
+ lines.forEach((line, index) => {
1197
+ const fenceMatch = /^(`{3,})/u.exec(line);
1198
+ if (openFence === undefined && fenceMatch !== null) {
1199
+ openFence = fenceMatch[1];
1200
+ return;
1201
+ }
1202
+ if (openFence !== undefined) {
1203
+ if (fenceMatch !== null && fenceMatch[1] === openFence)
1204
+ openFence = undefined;
1205
+ return;
1206
+ }
1207
+ const match = /^ {0,3}#{1,6}[ \t]+(.+?)[ \t]*$/u.exec(line);
1208
+ if (match !== null)
1209
+ starts.push({ index, title: match[1] });
1210
+ });
1211
+ return starts.map((start, position) => {
1212
+ const next = starts[position + 1]?.index ?? lines.length;
1213
+ return { title: start.title, body: trimLineRange(lines.slice(start.index + 1, next)) };
1214
+ });
489
1215
  }
490
- function parseFieldLines(field, lines, path, stripComments) {
1216
+ function parseFieldLines(field, lines, path, stripComments, issueBody) {
491
1217
  const diagnostics = [];
492
1218
  const filtered = stripComments ? lines.filter((line) => line.trim().length > 0) : lines;
493
1219
  if (filtered.length === 1 && filtered[0]?.trim() === GITHUB_NO_RESPONSE) {
@@ -496,15 +1222,17 @@ function parseFieldLines(field, lines, path, stripComments) {
496
1222
  return { value: field.type === "array" ? [] : undefined, diagnostics };
497
1223
  }
498
1224
  if (field.type === "string" || field.type === "enum") {
499
- const value = field.nativeMetadata.render === undefined
1225
+ const parsedValue = field.nativeMetadata.render === undefined
500
1226
  ? trimBlankLines(unescapeMarkdownValue(filtered.join("\n")))
501
1227
  : parseRenderedCodeBlock(filtered, field.nativeMetadata.render, path, diagnostics);
1228
+ if (parsedValue === EXPLICIT_EMPTY_STRING_MARKER)
1229
+ return { value: "", diagnostics };
502
1230
  const placeholder = stripComments
503
1231
  ? removeHtmlComments(field.nativeMetadata.placeholder ?? "")
504
1232
  : (field.nativeMetadata.placeholder ?? "");
505
- if (stripComments && value !== undefined && value === trimBlankLines(placeholder))
1233
+ if (stripComments && parsedValue !== undefined && parsedValue === trimBlankLines(placeholder))
506
1234
  return { value: undefined, diagnostics };
507
- return { value, diagnostics };
1235
+ return { value: issueBody ? issueSemanticValue(field, parsedValue) : parsedValue, diagnostics };
508
1236
  }
509
1237
  if (field.type === "array") {
510
1238
  if (field.nativeMetadata.multiple === true) {
@@ -517,7 +1245,7 @@ function parseFieldLines(field, lines, path, stripComments) {
517
1245
  .filter((entry) => entry.length > 0);
518
1246
  if (values.length === 0)
519
1247
  return { value: undefined, diagnostics };
520
- return { value: values, diagnostics };
1248
+ return { value: issueBody ? values.map((value) => issueSemanticValue(field, value)) : values, diagnostics };
521
1249
  }
522
1250
  const values = filtered
523
1251
  .map((line) => {
@@ -644,10 +1372,35 @@ function parseMetadata(input) {
644
1372
  }
645
1373
  function requiredMetadataString(value, key) {
646
1374
  const violation = validateRequiredMetadataString(value, key);
647
- if (violation !== undefined)
648
- throw new ArtifactInputError(violation.code, violation.message, violation.path);
1375
+ if (violation !== undefined) {
1376
+ throwMetadataInputError(value, key, violation.message, value === undefined ? "missing" : "invalid", value === undefined ? `Provide a value for ${key}.` : `Provide a non-empty string for ${key}.`);
1377
+ }
649
1378
  return value;
650
1379
  }
1380
+ function requiredCreateTitle(value, nativeTitle) {
1381
+ const title = requiredMetadataString(value, "title");
1382
+ const nativePrefix = nativeTitle?.trim();
1383
+ if (nativePrefix !== undefined && nativePrefix.length > 0 && title.trim() === nativePrefix) {
1384
+ throwMetadataInputError(title, "title", "title must contain content beyond the fixed native template prefix.", "invalid", "Provide a title containing caller content beyond the fixed native template prefix.");
1385
+ }
1386
+ return title;
1387
+ }
1388
+ function throwMetadataInputError(value, key, message, state, hint) {
1389
+ const path = `$.${key}`;
1390
+ const diagnostic = createArtifactDiagnostic({
1391
+ state,
1392
+ code: state === "missing" ? "FIELD_MISSING" : "FIELD_INVALID",
1393
+ detailCode: state === "missing" ? "FIELD_REQUIRED" : "FIELD_CONSTRAINT_VIOLATION",
1394
+ reason: state === "missing" ? "required" : "constraint",
1395
+ path,
1396
+ message,
1397
+ ...(state === "missing" || value === undefined ? {} : { actual: createFieldEvidence(path, value) }),
1398
+ recovery: [{ action: "provide", path, hint }],
1399
+ });
1400
+ throw new ArtifactInputError("INPUT_METADATA_INVALID", message, path, {
1401
+ diagnostics: createArtifactDiagnosticReport([diagnostic]),
1402
+ });
1403
+ }
651
1404
  function stringArray(value, key) {
652
1405
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
653
1406
  throw new ArtifactInputError("INPUT_METADATA_INVALID", `${key} must be an array of non-empty strings.`, `$.${key}`);
@@ -735,4 +1488,7 @@ function isRecord(value) {
735
1488
  function compareStrings(left, right) {
736
1489
  return left < right ? -1 : left > right ? 1 : 0;
737
1490
  }
1491
+ function semanticValidationFromLoad(loaded) {
1492
+ return { valid: loaded.valid, violations: loaded.violations, values: loaded.canonical };
1493
+ }
738
1494
  //# sourceMappingURL=artifact.js.map