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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +35 -10
- package/dist/artifact.d.ts +158 -15
- package/dist/artifact.js +834 -78
- package/dist/artifact.js.map +1 -1
- package/dist/cli.js +484 -65
- package/dist/cli.js.map +1 -1
- package/dist/contract/index.d.ts +2 -0
- package/dist/contract/index.js +2 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/ir.d.ts +15 -0
- package/dist/contract/ir.js +40 -2
- package/dist/contract/ir.js.map +1 -1
- package/dist/contract/issue-form.js +16 -3
- package/dist/contract/issue-form.js.map +1 -1
- package/dist/contract/issue-reference.d.ts +58 -0
- package/dist/contract/issue-reference.js +157 -0
- package/dist/contract/issue-reference.js.map +1 -0
- package/dist/contract/normalization.d.ts +31 -0
- package/dist/contract/normalization.js +73 -0
- package/dist/contract/normalization.js.map +1 -0
- package/dist/contract/schema.d.ts +4 -0
- package/dist/contract/schema.js +42 -1
- package/dist/contract/schema.js.map +1 -1
- package/dist/contract/validation.d.ts +120 -2
- package/dist/contract/validation.js +476 -3
- package/dist/contract/validation.js.map +1 -1
- package/dist/diagnostics.d.ts +133 -0
- package/dist/diagnostics.js +421 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/github/adapter.d.ts +2 -0
- package/dist/github/adapter.js +80 -32
- package/dist/github/adapter.js.map +1 -1
- package/dist/github/capability.js +1 -0
- package/dist/github/capability.js.map +1 -1
- package/dist/github/types.d.ts +7 -0
- package/dist/governance.d.ts +8 -2
- package/dist/governance.js +56 -4
- package/dist/governance.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pr-policy.d.ts +9 -0
- package/dist/pr-policy.js +36 -16
- package/dist/pr-policy.js.map +1 -1
- package/dist/pr-sync-input.d.ts +74 -0
- package/dist/pr-sync-input.js +231 -0
- package/dist/pr-sync-input.js.map +1 -0
- package/dist/reconciliation.d.ts +21 -14
- package/dist/reconciliation.js +515 -42
- package/dist/reconciliation.js.map +1 -1
- package/dist/semantic-template.js +32 -5
- package/dist/semantic-template.js.map +1 -1
- package/dist/skill.js +55 -21
- package/dist/skill.js.map +1 -1
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
55
|
-
if (!
|
|
56
|
-
throw new SemanticValidationError(
|
|
57
|
-
return renderIssueBody(contractInput,
|
|
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
|
|
65
|
-
if (!
|
|
66
|
-
throw new SemanticValidationError(
|
|
67
|
-
return renderPullRequestBody(contractInput,
|
|
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
|
|
76
|
-
if (!
|
|
77
|
-
throw new SemanticValidationError(
|
|
78
|
-
const 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
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
if (!
|
|
100
|
-
throw new SemanticValidationError(
|
|
101
|
-
const 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,
|
|
105
|
-
verifyRenderedRoundTrip(contractInput,
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 =
|
|
878
|
+
const path = fieldPath(key);
|
|
278
879
|
if (!expectedPresent || !actualPresent) {
|
|
279
|
-
diagnostics.push({
|
|
280
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
366
|
-
: renderCodeBlock(
|
|
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
|
|
381
|
-
return
|
|
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
|
|
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 {
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|