@intentset/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/export.d.ts +11 -2
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +156 -15
- package/dist/export.js.map +1 -1
- package/dist/impact.d.ts +9 -1
- package/dist/impact.d.ts.map +1 -1
- package/dist/impact.js +29 -0
- package/dist/impact.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/read-export.d.ts +32 -0
- package/dist/read-export.d.ts.map +1 -0
- package/dist/read-export.js +775 -0
- package/dist/read-export.js.map +1 -0
- package/dist/types.d.ts +179 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +35 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/export.ts +182 -14
- package/src/impact.ts +42 -1
- package/src/index.ts +1 -0
- package/src/read-export.ts +892 -0
- package/src/types.ts +198 -8
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an export as a consumer must (spec/export.md §5): the checks a
|
|
3
|
+
* connector runs before it lets an envelope replace the snapshot it holds.
|
|
4
|
+
*
|
|
5
|
+
* not-json the bytes are not UTF-8 JSON, or are over the size limit
|
|
6
|
+
* unsupported-contract `contract` is missing or names a version this reader does not know
|
|
7
|
+
* malformed the value does not have the envelope's shape (spec/export.schema.json)
|
|
8
|
+
* identity-mismatch the repository or product is not the one the connection was made for
|
|
9
|
+
* mixed-snapshot a report was computed at another commit or graph hash than the envelope's
|
|
10
|
+
* forbidden-content the envelope holds what its own withholding says it left out
|
|
11
|
+
* inconsistent-report counts, statuses or IDs contradict each other or the artifacts
|
|
12
|
+
*
|
|
13
|
+
* The first three stop the read; the rest are all collected, and the result's
|
|
14
|
+
* category is the first in that order. The shape checks are typed code that
|
|
15
|
+
* mirrors the schema (ADR 0002), and a test holds the two in agreement over
|
|
16
|
+
* every consumer fixture. Nothing here trusts the producer to have been this
|
|
17
|
+
* implementation: a consumer in another language makes the same checks.
|
|
18
|
+
*/
|
|
19
|
+
import { compareStrings, isRecord } from "./report.js";
|
|
20
|
+
import { CLAIM_KINDS, EXPORT_EVIDENCE_STATUSES, EXPORT_REPORTS, ID_PATTERN, LEVELS, LINK_KINDS, OWNERSHIP_REGIONS, STATUSES, SUPPORTED_EXPORT_CONTRACTS, ARTIFACT_TYPES, VISIBILITIES, } from "./types.js";
|
|
21
|
+
export const EXPORT_PROBLEM_CATEGORIES = [
|
|
22
|
+
"not-json",
|
|
23
|
+
"unsupported-contract",
|
|
24
|
+
"malformed",
|
|
25
|
+
"identity-mismatch",
|
|
26
|
+
"mixed-snapshot",
|
|
27
|
+
"forbidden-content",
|
|
28
|
+
"inconsistent-report",
|
|
29
|
+
];
|
|
30
|
+
export const EXPORT_MAX_BYTES = 64 * 1024 * 1024;
|
|
31
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
32
|
+
const COMMIT = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
33
|
+
const GENERATED_AT = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})$/;
|
|
34
|
+
const TIMESTAMP = /^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,9})?Z$/;
|
|
35
|
+
const CODE = /^[A-Z]+[0-9]{3}$/;
|
|
36
|
+
const POINTER = /^(?:\/.*)?$/;
|
|
37
|
+
const PROFILE = /^intentset\/[a-z]+\/0\.1$/;
|
|
38
|
+
const EXTENSION_KEY = /^[^/]+\/.+$/;
|
|
39
|
+
const TOKEN = /^\S+$/;
|
|
40
|
+
const TEXT = /\S/;
|
|
41
|
+
const URI = /^[A-Za-z][A-Za-z0-9+.-]*:\S+$/;
|
|
42
|
+
/** spec/export.md §5: read an envelope as a consumer must, from text, bytes or an already parsed value. */
|
|
43
|
+
export function readExport(input, options = {}) {
|
|
44
|
+
const maxBytes = options.maxBytes ?? EXPORT_MAX_BYTES;
|
|
45
|
+
let value = input;
|
|
46
|
+
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
47
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input).length : input.length;
|
|
48
|
+
if (bytes > maxBytes) {
|
|
49
|
+
return failed("not-json", "", `The export is ${bytes} bytes, over the ${maxBytes}-byte limit.`);
|
|
50
|
+
}
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = typeof input === "string" ? input : new TextDecoder("utf-8", { fatal: true }).decode(input);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return failed("not-json", "", "The export is not valid UTF-8.");
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return failed("not-json", "", `The export is not JSON: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(value) || typeof value.contract !== "string") {
|
|
66
|
+
return failed("unsupported-contract", "/contract", "The export names no contract, so its version is unknown.");
|
|
67
|
+
}
|
|
68
|
+
if (!SUPPORTED_EXPORT_CONTRACTS.includes(value.contract)) {
|
|
69
|
+
return failed("unsupported-contract", "/contract", `The export's contract is ${JSON.stringify(value.contract).slice(0, 80)}; this reader supports ${SUPPORTED_EXPORT_CONTRACTS.join(", ")}.`);
|
|
70
|
+
}
|
|
71
|
+
const shape = new Shape();
|
|
72
|
+
shape.envelope(value);
|
|
73
|
+
if (shape.problems.length > 0) {
|
|
74
|
+
return { ok: false, category: "malformed", problems: shape.problems };
|
|
75
|
+
}
|
|
76
|
+
const envelope = value;
|
|
77
|
+
const problems = [
|
|
78
|
+
...identity(envelope, options),
|
|
79
|
+
...snapshots(envelope),
|
|
80
|
+
...forbidden(envelope),
|
|
81
|
+
...consistency(envelope),
|
|
82
|
+
];
|
|
83
|
+
if (problems.length > 0) {
|
|
84
|
+
const rank = (p) => EXPORT_PROBLEM_CATEGORIES.indexOf(p.category);
|
|
85
|
+
problems.sort((a, b) => rank(a) - rank(b) || compareStrings(a.pointer, b.pointer));
|
|
86
|
+
return { ok: false, category: problems[0].category, problems };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, envelope, supplied: EXPORT_REPORTS.filter((name) => envelope.reports[name] !== undefined) };
|
|
89
|
+
}
|
|
90
|
+
function failed(category, pointer, message) {
|
|
91
|
+
return { ok: false, category, problems: [{ category, pointer, message }] };
|
|
92
|
+
}
|
|
93
|
+
/** The shape checks: spec/export.schema.json in typed code. Every problem is `malformed`. */
|
|
94
|
+
class Shape {
|
|
95
|
+
problems = [];
|
|
96
|
+
fail(pointer, message) {
|
|
97
|
+
// A broken input can repeat one mistake thousands of times; the first hundred say enough.
|
|
98
|
+
if (this.problems.length < 100)
|
|
99
|
+
this.problems.push({ category: "malformed", pointer, message });
|
|
100
|
+
}
|
|
101
|
+
/** An object with exactly these members (optional ones may be absent); null when it is not an object. */
|
|
102
|
+
object(value, at, required, optional = []) {
|
|
103
|
+
if (!isRecord(value)) {
|
|
104
|
+
this.fail(at, "Expected an object.");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
for (const key of required)
|
|
108
|
+
if (!Object.hasOwn(value, key))
|
|
109
|
+
this.fail(`${at}/${key}`, `Missing required member ${key}.`);
|
|
110
|
+
for (const key of Object.keys(value)) {
|
|
111
|
+
if (!required.includes(key) && !optional.includes(key))
|
|
112
|
+
this.fail(`${at}/${pointerToken(key)}`, `Unknown member ${key}.`);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
string(value, at, pattern, what = "a string") {
|
|
117
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
118
|
+
this.fail(at, `Expected ${what}.`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (pattern !== undefined && !pattern.test(value)) {
|
|
122
|
+
this.fail(at, `Expected ${what}.`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
nullable(value, at, check) {
|
|
128
|
+
if (value !== null)
|
|
129
|
+
check(value, at);
|
|
130
|
+
}
|
|
131
|
+
id(value, at) {
|
|
132
|
+
this.string(value, at, ID_PATTERN, "an artifact ID");
|
|
133
|
+
}
|
|
134
|
+
integer(value, at, minimum = 0) {
|
|
135
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum) {
|
|
136
|
+
this.fail(at, `Expected an integer of at least ${minimum}.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
boolean(value, at) {
|
|
140
|
+
if (typeof value !== "boolean")
|
|
141
|
+
this.fail(at, "Expected true or false.");
|
|
142
|
+
}
|
|
143
|
+
oneOf(value, at, options) {
|
|
144
|
+
if (!options.includes(value))
|
|
145
|
+
this.fail(at, `Expected one of ${options.join(", ")}.`);
|
|
146
|
+
}
|
|
147
|
+
/** An array whose items pass `item`; `unique` compares items as JSON, `sorted` as strings or by `key`. */
|
|
148
|
+
array(value, at, item, rules = {}) {
|
|
149
|
+
if (!Array.isArray(value)) {
|
|
150
|
+
this.fail(at, "Expected an array.");
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
for (const [i, entry] of value.entries())
|
|
154
|
+
item(entry, `${at}/${i}`);
|
|
155
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
156
|
+
this.fail(at, `Expected at least ${rules.minItems} item${rules.minItems === 1 ? "" : "s"}.`);
|
|
157
|
+
}
|
|
158
|
+
if (rules.unique === true && new Set(value.map((v) => JSON.stringify(v))).size !== value.length) {
|
|
159
|
+
this.fail(at, "Expected no repeated items.");
|
|
160
|
+
}
|
|
161
|
+
if (rules.sortedBy !== undefined) {
|
|
162
|
+
const keys = value.map(rules.sortedBy);
|
|
163
|
+
for (let i = 1; i < keys.length; i++) {
|
|
164
|
+
if (compareStrings(keys[i - 1], keys[i]) >= 0) {
|
|
165
|
+
this.fail(`${at}/${i}`, "Expected items sorted and unique.");
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
strings(value, at, rules = {}) {
|
|
173
|
+
this.array(value, at, (v, p) => this.string(v, p), { unique: true, ...rules });
|
|
174
|
+
}
|
|
175
|
+
ids(value, at) {
|
|
176
|
+
this.array(value, at, (v, p) => this.id(v, p), { unique: true });
|
|
177
|
+
}
|
|
178
|
+
extensions(value, at) {
|
|
179
|
+
if (!isRecord(value)) {
|
|
180
|
+
this.fail(at, "Expected an object.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const key of Object.keys(value)) {
|
|
184
|
+
if (!EXTENSION_KEY.test(key))
|
|
185
|
+
this.fail(`${at}/${pointerToken(key)}`, "Expected a namespaced key such as example.org/name.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
envelope(value) {
|
|
189
|
+
const v = this.object(value, "", [
|
|
190
|
+
"contract",
|
|
191
|
+
"spec",
|
|
192
|
+
"generatedAt",
|
|
193
|
+
"repository",
|
|
194
|
+
"products",
|
|
195
|
+
"source",
|
|
196
|
+
"graphHash",
|
|
197
|
+
"release",
|
|
198
|
+
"validation",
|
|
199
|
+
"withholding",
|
|
200
|
+
"registries",
|
|
201
|
+
"artifacts",
|
|
202
|
+
"reports",
|
|
203
|
+
]);
|
|
204
|
+
if (v === null)
|
|
205
|
+
return;
|
|
206
|
+
if (v.spec !== "0.1")
|
|
207
|
+
this.fail("/spec", "Expected 0.1.");
|
|
208
|
+
this.string(v.generatedAt, "/generatedAt", GENERATED_AT, "an ISO 8601 time");
|
|
209
|
+
this.string(v.repository, "/repository");
|
|
210
|
+
this.array(v.products, "/products", (p, at) => this.id(p, at), { sortedBy: String });
|
|
211
|
+
this.source(v.source);
|
|
212
|
+
this.string(v.graphHash, "/graphHash", SHA256, "a SHA-256 in lowercase hex");
|
|
213
|
+
this.nullable(v.release, "/release", (release, at) => {
|
|
214
|
+
const r = this.object(release, at, ["product", "label"]);
|
|
215
|
+
if (r === null)
|
|
216
|
+
return;
|
|
217
|
+
this.id(r.product, `${at}/product`);
|
|
218
|
+
this.string(r.label, `${at}/label`);
|
|
219
|
+
});
|
|
220
|
+
this.validation(v.validation);
|
|
221
|
+
const w = this.object(v.withholding, "/withholding", ["visibilities", "artifacts", "diagnostics"]);
|
|
222
|
+
if (w !== null) {
|
|
223
|
+
this.array(w.visibilities, "/withholding/visibilities", (x, at) => this.oneOf(x, at, VISIBILITIES), {
|
|
224
|
+
unique: true,
|
|
225
|
+
});
|
|
226
|
+
this.integer(w.artifacts, "/withholding/artifacts");
|
|
227
|
+
this.integer(w.diagnostics, "/withholding/diagnostics");
|
|
228
|
+
}
|
|
229
|
+
this.registries(v.registries);
|
|
230
|
+
this.array(v.artifacts, "/artifacts", (a, at) => this.artifact(a, at), {
|
|
231
|
+
sortedBy: (a) => (isRecord(a) && typeof a.id === "string" ? a.id : ""),
|
|
232
|
+
});
|
|
233
|
+
this.reports(v.reports);
|
|
234
|
+
}
|
|
235
|
+
source(value) {
|
|
236
|
+
const s = this.object(value, "/source", ["commit", "uncommitted"], ["commitUnavailable"]);
|
|
237
|
+
if (s === null)
|
|
238
|
+
return;
|
|
239
|
+
this.boolean(s.uncommitted, "/source/uncommitted");
|
|
240
|
+
if (s.commit === null) {
|
|
241
|
+
if (!Object.hasOwn(s, "commitUnavailable"))
|
|
242
|
+
this.fail("/source/commitUnavailable", "Expected a reason when commit is null.");
|
|
243
|
+
else
|
|
244
|
+
this.string(s.commitUnavailable, "/source/commitUnavailable");
|
|
245
|
+
if (s.uncommitted === true)
|
|
246
|
+
this.fail("/source/uncommitted", "Expected false when commit is null.");
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.string(s.commit, "/source/commit", COMMIT, "a full commit ID");
|
|
250
|
+
if (Object.hasOwn(s, "commitUnavailable"))
|
|
251
|
+
this.fail("/source/commitUnavailable", "Expected no reason when there is a commit.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
validation(value) {
|
|
255
|
+
const v = this.object(value, "/validation", ["level", "scope", "status", "errors", "warnings", "diagnostics"]);
|
|
256
|
+
if (v === null)
|
|
257
|
+
return;
|
|
258
|
+
this.oneOf(v.level, "/validation/level", LEVELS);
|
|
259
|
+
this.array(v.scope, "/validation/scope", (s, at) => this.string(s, at));
|
|
260
|
+
this.oneOf(v.status, "/validation/status", ["pass", "fail"]);
|
|
261
|
+
this.integer(v.errors, "/validation/errors");
|
|
262
|
+
this.integer(v.warnings, "/validation/warnings");
|
|
263
|
+
this.array(v.diagnostics, "/validation/diagnostics", (d, at) => this.diagnostic(d, at));
|
|
264
|
+
}
|
|
265
|
+
diagnostic(value, at) {
|
|
266
|
+
const d = this.object(value, at, ["code", "severity", "origin", "artifact", "path", "message", "remediation"], ["location", "field"]);
|
|
267
|
+
if (d === null)
|
|
268
|
+
return;
|
|
269
|
+
this.string(d.code, `${at}/code`, CODE, "a diagnostic code such as CORE003");
|
|
270
|
+
this.oneOf(d.severity, `${at}/severity`, ["error", "warning"]);
|
|
271
|
+
this.oneOf(d.origin, `${at}/origin`, [
|
|
272
|
+
"syntax",
|
|
273
|
+
"profile",
|
|
274
|
+
"graph",
|
|
275
|
+
"architecture",
|
|
276
|
+
"evidence",
|
|
277
|
+
"publication",
|
|
278
|
+
"render",
|
|
279
|
+
]);
|
|
280
|
+
if (d.artifact !== null && typeof d.artifact !== "string")
|
|
281
|
+
this.fail(`${at}/artifact`, "Expected a string or null.");
|
|
282
|
+
if (d.path !== null && typeof d.path !== "string")
|
|
283
|
+
this.fail(`${at}/path`, "Expected a string or null.");
|
|
284
|
+
if (Object.hasOwn(d, "location")) {
|
|
285
|
+
const l = this.object(d.location, `${at}/location`, ["line"], ["column"]);
|
|
286
|
+
if (l !== null) {
|
|
287
|
+
this.integer(l.line, `${at}/location/line`, 1);
|
|
288
|
+
if (Object.hasOwn(l, "column"))
|
|
289
|
+
this.integer(l.column, `${at}/location/column`, 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (Object.hasOwn(d, "field") && (typeof d.field !== "string" || !POINTER.test(d.field))) {
|
|
293
|
+
this.fail(`${at}/field`, "Expected a JSON pointer.");
|
|
294
|
+
}
|
|
295
|
+
this.string(d.message, `${at}/message`);
|
|
296
|
+
this.string(d.remediation, `${at}/remediation`);
|
|
297
|
+
}
|
|
298
|
+
registries(value) {
|
|
299
|
+
const at = "/registries";
|
|
300
|
+
const r = this.object(value, at, ["owners", "audiences", "releases", "roles", "editions", "flags", "resources"]);
|
|
301
|
+
if (r === null)
|
|
302
|
+
return;
|
|
303
|
+
for (const key of ["owners", "audiences", "releases", "roles", "editions", "flags"])
|
|
304
|
+
this.strings(r[key], `${at}/${key}`);
|
|
305
|
+
this.array(r.resources, `${at}/resources`, (resource, p) => {
|
|
306
|
+
const x = this.object(resource, p, ["id", "path", "owner", "consumers"]);
|
|
307
|
+
if (x === null)
|
|
308
|
+
return;
|
|
309
|
+
this.id(x.id, `${p}/id`);
|
|
310
|
+
this.string(x.path, `${p}/path`);
|
|
311
|
+
this.string(x.owner, `${p}/owner`);
|
|
312
|
+
this.strings(x.consumers, `${p}/consumers`);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
artifact(value, at) {
|
|
316
|
+
const a = this.object(value, at, [
|
|
317
|
+
"id",
|
|
318
|
+
"type",
|
|
319
|
+
"title",
|
|
320
|
+
"status",
|
|
321
|
+
"owner",
|
|
322
|
+
"visibility",
|
|
323
|
+
"audiences",
|
|
324
|
+
"profile",
|
|
325
|
+
"revision",
|
|
326
|
+
"path",
|
|
327
|
+
"sourceHash",
|
|
328
|
+
"parent",
|
|
329
|
+
"links",
|
|
330
|
+
"derived",
|
|
331
|
+
"withheldLinks",
|
|
332
|
+
"availability",
|
|
333
|
+
"slice",
|
|
334
|
+
"verification",
|
|
335
|
+
"extensions",
|
|
336
|
+
], ["body"]);
|
|
337
|
+
if (a === null)
|
|
338
|
+
return;
|
|
339
|
+
this.id(a.id, `${at}/id`);
|
|
340
|
+
this.oneOf(a.type, `${at}/type`, ARTIFACT_TYPES);
|
|
341
|
+
this.string(a.title, `${at}/title`);
|
|
342
|
+
this.oneOf(a.status, `${at}/status`, STATUSES);
|
|
343
|
+
this.string(a.owner, `${at}/owner`);
|
|
344
|
+
this.oneOf(a.visibility, `${at}/visibility`, VISIBILITIES);
|
|
345
|
+
this.strings(a.audiences, `${at}/audiences`, { minItems: 1 });
|
|
346
|
+
this.string(a.profile, `${at}/profile`, PROFILE, "a profile such as intentset/behavior/0.1");
|
|
347
|
+
this.nullable(a.revision, `${at}/revision`, (r, p) => this.integer(r, p, 1));
|
|
348
|
+
this.string(a.path, `${at}/path`);
|
|
349
|
+
this.string(a.sourceHash, `${at}/sourceHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
350
|
+
this.nullable(a.parent, `${at}/parent`, (p, q) => this.id(p, q));
|
|
351
|
+
const kinds = LINK_KINDS.filter((kind) => kind !== "parent");
|
|
352
|
+
const links = this.object(a.links, `${at}/links`, [], kinds);
|
|
353
|
+
if (links !== null)
|
|
354
|
+
for (const key of Object.keys(links))
|
|
355
|
+
if (kinds.includes(key))
|
|
356
|
+
this.ids(links[key], `${at}/links/${key}`);
|
|
357
|
+
const derived = this.object(a.derived, `${at}/derived`, [], LINK_KINDS);
|
|
358
|
+
if (derived !== null) {
|
|
359
|
+
for (const key of Object.keys(derived))
|
|
360
|
+
if (LINK_KINDS.includes(key))
|
|
361
|
+
this.ids(derived[key], `${at}/derived/${key}`);
|
|
362
|
+
}
|
|
363
|
+
this.integer(a.withheldLinks, `${at}/withheldLinks`);
|
|
364
|
+
this.nullable(a.availability, `${at}/availability`, (x, p) => {
|
|
365
|
+
const av = this.object(x, p, ["products", "releases", "roles", "editions", "flags"]);
|
|
366
|
+
if (av === null)
|
|
367
|
+
return;
|
|
368
|
+
for (const key of ["products", "releases", "roles", "editions"])
|
|
369
|
+
this.strings(av[key], `${p}/${key}`, { minItems: 1 });
|
|
370
|
+
this.strings(av.flags, `${p}/flags`);
|
|
371
|
+
});
|
|
372
|
+
this.nullable(a.slice, `${at}/slice`, (x, p) => this.slice(x, p));
|
|
373
|
+
this.nullable(a.verification, `${at}/verification`, (x, p) => {
|
|
374
|
+
const v = this.object(x, p, ["method", "locator", "selector"]);
|
|
375
|
+
if (v === null)
|
|
376
|
+
return;
|
|
377
|
+
this.oneOf(v.method, `${p}/method`, ["automated", "manual"]);
|
|
378
|
+
this.string(v.locator, `${p}/locator`);
|
|
379
|
+
this.string(v.selector, `${p}/selector`);
|
|
380
|
+
});
|
|
381
|
+
this.nullable(a.extensions, `${at}/extensions`, (x, p) => this.extensions(x, p));
|
|
382
|
+
if (Object.hasOwn(a, "body") && typeof a.body !== "string")
|
|
383
|
+
this.fail(`${at}/body`, "Expected a string.");
|
|
384
|
+
}
|
|
385
|
+
slice(value, at) {
|
|
386
|
+
const s = this.object(value, at, ["kind", "domain", "entrypoints", "layers", "claims", "usesResources"], ["rationale"]);
|
|
387
|
+
if (s === null)
|
|
388
|
+
return;
|
|
389
|
+
this.oneOf(s.kind, `${at}/kind`, ["product", "technical"]);
|
|
390
|
+
if (Object.hasOwn(s, "rationale") && typeof s.rationale !== "string")
|
|
391
|
+
this.fail(`${at}/rationale`, "Expected a string.");
|
|
392
|
+
if (typeof s.domain !== "string")
|
|
393
|
+
this.fail(`${at}/domain`, "Expected a string.");
|
|
394
|
+
this.strings(s.entrypoints, `${at}/entrypoints`, { minItems: 1 });
|
|
395
|
+
if (isRecord(s.layers)) {
|
|
396
|
+
for (const [layer, files] of Object.entries(s.layers))
|
|
397
|
+
this.strings(files, `${at}/layers/${pointerToken(layer)}`);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.fail(`${at}/layers`, "Expected an object.");
|
|
401
|
+
}
|
|
402
|
+
this.array(s.claims, `${at}/claims`, (claim, p) => {
|
|
403
|
+
const c = this.object(claim, p, ["kind", "path"]);
|
|
404
|
+
if (c === null)
|
|
405
|
+
return;
|
|
406
|
+
this.oneOf(c.kind, `${p}/kind`, CLAIM_KINDS);
|
|
407
|
+
this.string(c.path, `${p}/path`);
|
|
408
|
+
});
|
|
409
|
+
this.ids(s.usesResources, `${at}/usesResources`);
|
|
410
|
+
}
|
|
411
|
+
reports(value) {
|
|
412
|
+
const r = this.object(value, "/reports", [], EXPORT_REPORTS);
|
|
413
|
+
if (r === null)
|
|
414
|
+
return;
|
|
415
|
+
if (Object.hasOwn(r, "evidence"))
|
|
416
|
+
this.evidence(r.evidence, "/reports/evidence");
|
|
417
|
+
if (Object.hasOwn(r, "knowledge"))
|
|
418
|
+
this.knowledge(r.knowledge, "/reports/knowledge");
|
|
419
|
+
if (Object.hasOwn(r, "impact"))
|
|
420
|
+
this.impact(r.impact, "/reports/impact");
|
|
421
|
+
if (Object.hasOwn(r, "ownership"))
|
|
422
|
+
this.ownership(r.ownership, "/reports/ownership");
|
|
423
|
+
}
|
|
424
|
+
snapshotCommit(value, at) {
|
|
425
|
+
this.nullable(value, at, (c, p) => this.string(c, p, COMMIT, "a full commit ID"));
|
|
426
|
+
}
|
|
427
|
+
evidence(value, at) {
|
|
428
|
+
const e = this.object(value, at, ["commit", "graphHash", "scope", "records", "verifications", "claims"]);
|
|
429
|
+
if (e === null)
|
|
430
|
+
return;
|
|
431
|
+
this.snapshotCommit(e.commit, `${at}/commit`);
|
|
432
|
+
this.string(e.graphHash, `${at}/graphHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
433
|
+
this.nullable(e.scope, `${at}/scope`, (x, p) => this.runScope(x, p));
|
|
434
|
+
this.integer(e.records, `${at}/records`);
|
|
435
|
+
const byId = (x) => (isRecord(x) && typeof x.id === "string" ? x.id : "");
|
|
436
|
+
this.array(e.verifications, `${at}/verifications`, (x, p) => {
|
|
437
|
+
const v = this.object(x, p, ["id", "method", "status", "note", "latest", "atSnapshot", "runs", "outOfScope"]);
|
|
438
|
+
if (v === null)
|
|
439
|
+
return;
|
|
440
|
+
this.id(v.id, `${p}/id`);
|
|
441
|
+
this.oneOf(v.method, `${p}/method`, ["automated", "manual"]);
|
|
442
|
+
this.oneOf(v.status, `${p}/status`, EXPORT_EVIDENCE_STATUSES);
|
|
443
|
+
this.nullable(v.note, `${p}/note`, (n, q) => this.string(n, q));
|
|
444
|
+
this.nullable(v.latest, `${p}/latest`, (r, q) => this.runRecord(r, q));
|
|
445
|
+
const counts = this.object(v.atSnapshot, `${p}/atSnapshot`, ["pass", "fail", "skip", "error"]);
|
|
446
|
+
if (counts !== null)
|
|
447
|
+
for (const key of ["pass", "fail", "skip", "error"])
|
|
448
|
+
this.integer(counts[key], `${p}/atSnapshot/${key}`);
|
|
449
|
+
this.integer(v.runs, `${p}/runs`);
|
|
450
|
+
this.integer(v.outOfScope, `${p}/outOfScope`);
|
|
451
|
+
}, { sortedBy: byId });
|
|
452
|
+
this.array(e.claims, `${at}/claims`, (x, p) => {
|
|
453
|
+
const c = this.object(x, p, [
|
|
454
|
+
"id",
|
|
455
|
+
"type",
|
|
456
|
+
"lifecycle",
|
|
457
|
+
"required",
|
|
458
|
+
"linked",
|
|
459
|
+
"verified",
|
|
460
|
+
"verifications",
|
|
461
|
+
"withheld",
|
|
462
|
+
]);
|
|
463
|
+
if (c === null)
|
|
464
|
+
return;
|
|
465
|
+
this.id(c.id, `${p}/id`);
|
|
466
|
+
this.oneOf(c.type, `${p}/type`, ["behavior", "rule", "scenario"]);
|
|
467
|
+
this.oneOf(c.lifecycle, `${p}/lifecycle`, STATUSES);
|
|
468
|
+
this.boolean(c.required, `${p}/required`);
|
|
469
|
+
this.boolean(c.linked, `${p}/linked`);
|
|
470
|
+
this.boolean(c.verified, `${p}/verified`);
|
|
471
|
+
this.array(c.verifications, `${p}/verifications`, (id, q) => this.id(id, q), { sortedBy: String });
|
|
472
|
+
this.integer(c.withheld, `${p}/withheld`);
|
|
473
|
+
}, { sortedBy: byId });
|
|
474
|
+
}
|
|
475
|
+
runScope(value, at) {
|
|
476
|
+
const s = this.object(value, at, ["product", "release"]);
|
|
477
|
+
if (s === null)
|
|
478
|
+
return;
|
|
479
|
+
this.id(s.product, `${at}/product`);
|
|
480
|
+
this.string(s.release, `${at}/release`, TEXT);
|
|
481
|
+
}
|
|
482
|
+
runRecord(value, at) {
|
|
483
|
+
const r = this.object(value, at, [
|
|
484
|
+
"evidenceId",
|
|
485
|
+
"verificationId",
|
|
486
|
+
"commit",
|
|
487
|
+
"graphHash",
|
|
488
|
+
"environment",
|
|
489
|
+
"scope",
|
|
490
|
+
"tool",
|
|
491
|
+
"reviewer",
|
|
492
|
+
"startedAt",
|
|
493
|
+
"finishedAt",
|
|
494
|
+
"result",
|
|
495
|
+
"uri",
|
|
496
|
+
], ["rationale", "extensions"]);
|
|
497
|
+
if (r === null)
|
|
498
|
+
return;
|
|
499
|
+
this.string(r.evidenceId, `${at}/evidenceId`, TOKEN);
|
|
500
|
+
this.id(r.verificationId, `${at}/verificationId`);
|
|
501
|
+
this.string(r.commit, `${at}/commit`, TOKEN);
|
|
502
|
+
if (r.graphHash !== "@current")
|
|
503
|
+
this.string(r.graphHash, `${at}/graphHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
504
|
+
this.string(r.environment, `${at}/environment`, TEXT);
|
|
505
|
+
this.runScope(r.scope, `${at}/scope`);
|
|
506
|
+
this.nullable(r.tool, `${at}/tool`, (x, p) => {
|
|
507
|
+
const t = this.object(x, p, ["name", "version"]);
|
|
508
|
+
if (t === null)
|
|
509
|
+
return;
|
|
510
|
+
this.string(t.name, `${p}/name`, TEXT);
|
|
511
|
+
this.string(t.version, `${p}/version`, TEXT);
|
|
512
|
+
});
|
|
513
|
+
this.nullable(r.reviewer, `${at}/reviewer`, (x, p) => this.string(x, p, TEXT));
|
|
514
|
+
this.string(r.startedAt, `${at}/startedAt`, TIMESTAMP, "a UTC timestamp");
|
|
515
|
+
this.string(r.finishedAt, `${at}/finishedAt`, TIMESTAMP, "a UTC timestamp");
|
|
516
|
+
this.oneOf(r.result, `${at}/result`, ["pass", "fail", "skip", "error"]);
|
|
517
|
+
this.string(r.uri, `${at}/uri`, URI, "a URI");
|
|
518
|
+
if (Object.hasOwn(r, "rationale"))
|
|
519
|
+
this.string(r.rationale, `${at}/rationale`, TEXT);
|
|
520
|
+
if (Object.hasOwn(r, "extensions"))
|
|
521
|
+
this.extensions(r.extensions, `${at}/extensions`);
|
|
522
|
+
if (typeof r.reviewer === "string" && !Object.hasOwn(r, "rationale")) {
|
|
523
|
+
this.fail(`${at}/rationale`, "Expected a rationale for a manual review.");
|
|
524
|
+
}
|
|
525
|
+
if (r.reviewer === null && !isRecord(r.tool))
|
|
526
|
+
this.fail(`${at}/tool`, "Expected the tool of an automated run.");
|
|
527
|
+
}
|
|
528
|
+
knowledge(value, at) {
|
|
529
|
+
const k = this.object(value, at, ["graphHash", "artifacts"]);
|
|
530
|
+
if (k === null)
|
|
531
|
+
return;
|
|
532
|
+
this.string(k.graphHash, `${at}/graphHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
533
|
+
this.array(k.artifacts, `${at}/artifacts`, (x, p) => {
|
|
534
|
+
const e = this.object(x, p, [
|
|
535
|
+
"id",
|
|
536
|
+
"lifecycle",
|
|
537
|
+
"status",
|
|
538
|
+
"reviewer",
|
|
539
|
+
"reviewedAt",
|
|
540
|
+
"sources",
|
|
541
|
+
"changed",
|
|
542
|
+
"missing",
|
|
543
|
+
"withheld",
|
|
544
|
+
]);
|
|
545
|
+
if (e === null)
|
|
546
|
+
return;
|
|
547
|
+
this.id(e.id, `${p}/id`);
|
|
548
|
+
this.oneOf(e.lifecycle, `${p}/lifecycle`, STATUSES);
|
|
549
|
+
this.oneOf(e.status, `${p}/status`, ["current", "needs-review"]);
|
|
550
|
+
this.nullable(e.reviewer, `${p}/reviewer`, (r, q) => this.string(r, q));
|
|
551
|
+
this.nullable(e.reviewedAt, `${p}/reviewedAt`, (r, q) => this.string(r, q));
|
|
552
|
+
for (const key of ["sources", "changed", "missing"]) {
|
|
553
|
+
this.array(e[key], `${p}/${key}`, (id, q) => this.id(id, q), { sortedBy: String });
|
|
554
|
+
}
|
|
555
|
+
this.integer(e.withheld, `${p}/withheld`);
|
|
556
|
+
}, { sortedBy: (x) => (isRecord(x) && typeof x.id === "string" ? x.id : "") });
|
|
557
|
+
}
|
|
558
|
+
impact(value, at) {
|
|
559
|
+
const i = this.object(value, at, ["graphHash", "note", "starts"]);
|
|
560
|
+
if (i === null)
|
|
561
|
+
return;
|
|
562
|
+
this.string(i.graphHash, `${at}/graphHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
563
|
+
this.string(i.note, `${at}/note`);
|
|
564
|
+
const hits = (list, p) => this.array(list, p, (x, q) => {
|
|
565
|
+
const h = this.object(x, q, ["id", "path"]);
|
|
566
|
+
if (h === null)
|
|
567
|
+
return;
|
|
568
|
+
this.id(h.id, `${q}/id`);
|
|
569
|
+
this.array(h.path, `${q}/path`, (step, r) => {
|
|
570
|
+
const s = this.object(step, r, ["id", "via", "reason"]);
|
|
571
|
+
if (s === null)
|
|
572
|
+
return;
|
|
573
|
+
this.id(s.id, `${r}/id`);
|
|
574
|
+
this.string(s.via, `${r}/via`);
|
|
575
|
+
this.string(s.reason, `${r}/reason`);
|
|
576
|
+
}, { minItems: 1 });
|
|
577
|
+
});
|
|
578
|
+
this.array(i.starts, `${at}/starts`, (x, p) => {
|
|
579
|
+
const e = this.object(x, p, ["start", "direct", "candidates", "context", "ancestors", "withheld"]);
|
|
580
|
+
if (e === null)
|
|
581
|
+
return;
|
|
582
|
+
this.id(e.start, `${p}/start`);
|
|
583
|
+
hits(e.direct, `${p}/direct`);
|
|
584
|
+
hits(e.candidates, `${p}/candidates`);
|
|
585
|
+
hits(e.context, `${p}/context`);
|
|
586
|
+
this.ids(e.ancestors, `${p}/ancestors`);
|
|
587
|
+
this.integer(e.withheld, `${p}/withheld`);
|
|
588
|
+
}, { sortedBy: (x) => (isRecord(x) && typeof x.start === "string" ? x.start : "") });
|
|
589
|
+
}
|
|
590
|
+
ownership(value, at) {
|
|
591
|
+
const o = this.object(value, at, ["commit", "graphHash", "files", "withheld"]);
|
|
592
|
+
if (o === null)
|
|
593
|
+
return;
|
|
594
|
+
this.snapshotCommit(o.commit, `${at}/commit`);
|
|
595
|
+
this.string(o.graphHash, `${at}/graphHash`, SHA256, "a SHA-256 in lowercase hex");
|
|
596
|
+
this.array(o.files, `${at}/files`, (x, p) => {
|
|
597
|
+
const f = this.object(x, p, ["path", "region", "owner"]);
|
|
598
|
+
if (f === null)
|
|
599
|
+
return;
|
|
600
|
+
this.string(f.path, `${p}/path`);
|
|
601
|
+
this.oneOf(f.region, `${p}/region`, OWNERSHIP_REGIONS);
|
|
602
|
+
this.nullable(f.owner, `${p}/owner`, (id, q) => this.id(id, q));
|
|
603
|
+
}, { sortedBy: (x) => (isRecord(x) && typeof x.path === "string" ? x.path : "") });
|
|
604
|
+
this.integer(o.withheld, `${at}/withheld`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function pointerToken(key) {
|
|
608
|
+
return key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
609
|
+
}
|
|
610
|
+
function identity(envelope, options) {
|
|
611
|
+
const problems = [];
|
|
612
|
+
if (options.repository !== undefined && envelope.repository !== options.repository) {
|
|
613
|
+
problems.push({
|
|
614
|
+
category: "identity-mismatch",
|
|
615
|
+
pointer: "/repository",
|
|
616
|
+
message: `The export is from repository ${envelope.repository}, and the connection is for ${options.repository}.`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (options.product !== undefined && !envelope.products.includes(options.product)) {
|
|
620
|
+
problems.push({
|
|
621
|
+
category: "identity-mismatch",
|
|
622
|
+
pointer: "/products",
|
|
623
|
+
message: `The export does not contain product ${options.product}, which the connection is for.`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return problems;
|
|
627
|
+
}
|
|
628
|
+
/** Every report names the snapshot it was computed at, and it must be the envelope's: no partial snapshots mixed in. */
|
|
629
|
+
function snapshots(envelope) {
|
|
630
|
+
const problems = [];
|
|
631
|
+
const commit = envelope.source.commit;
|
|
632
|
+
for (const name of EXPORT_REPORTS) {
|
|
633
|
+
const report = envelope.reports[name];
|
|
634
|
+
if (report === undefined)
|
|
635
|
+
continue;
|
|
636
|
+
if (report.graphHash !== envelope.graphHash) {
|
|
637
|
+
problems.push({
|
|
638
|
+
category: "mixed-snapshot",
|
|
639
|
+
pointer: `/reports/${name}/graphHash`,
|
|
640
|
+
message: `The ${name} report was computed at graph ${report.graphHash.slice(0, 12)}, not the export's ${envelope.graphHash.slice(0, 12)}.`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if ("commit" in report && report.commit !== commit) {
|
|
644
|
+
problems.push({
|
|
645
|
+
category: "mixed-snapshot",
|
|
646
|
+
pointer: `/reports/${name}/commit`,
|
|
647
|
+
message: `The ${name} report was computed at commit ${report.commit ?? "null"}, not the export's ${commit ?? "null"}.`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return problems;
|
|
652
|
+
}
|
|
653
|
+
/** What the envelope says it withheld must not be in it. */
|
|
654
|
+
function forbidden(envelope) {
|
|
655
|
+
const withheld = new Set(envelope.withholding.visibilities);
|
|
656
|
+
const problems = [];
|
|
657
|
+
envelope.artifacts.forEach((artifact, i) => {
|
|
658
|
+
if (withheld.has(artifact.visibility)) {
|
|
659
|
+
problems.push({
|
|
660
|
+
category: "forbidden-content",
|
|
661
|
+
pointer: `/artifacts/${i}/visibility`,
|
|
662
|
+
message: `Artifact ${artifact.id} is ${artifact.visibility}, a visibility the export says it withholds.`,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
return problems;
|
|
667
|
+
}
|
|
668
|
+
function consistency(envelope) {
|
|
669
|
+
const problems = [];
|
|
670
|
+
const fail = (pointer, message) => problems.push({ category: "inconsistent-report", pointer, message });
|
|
671
|
+
const types = new Map(envelope.artifacts.map((a) => [a.id, a.type]));
|
|
672
|
+
const { validation, withholding } = envelope;
|
|
673
|
+
if (validation.status !== (validation.errors > 0 ? "fail" : "pass")) {
|
|
674
|
+
fail("/validation/status", `The status is ${validation.status} with ${validation.errors} errors.`);
|
|
675
|
+
}
|
|
676
|
+
const listedErrors = validation.diagnostics.filter((d) => d.severity === "error").length;
|
|
677
|
+
if (validation.diagnostics.length + withholding.diagnostics !== validation.errors + validation.warnings ||
|
|
678
|
+
listedErrors > validation.errors ||
|
|
679
|
+
validation.diagnostics.length - listedErrors > validation.warnings) {
|
|
680
|
+
fail("/validation/diagnostics", "The diagnostics listed and withheld do not add up to the error and warning counts.");
|
|
681
|
+
}
|
|
682
|
+
envelope.products.forEach((id, i) => {
|
|
683
|
+
if (types.get(id) !== "product")
|
|
684
|
+
fail(`/products/${i}`, `${id} is not a product artifact in the export.`);
|
|
685
|
+
});
|
|
686
|
+
const evidence = envelope.reports.evidence;
|
|
687
|
+
if (evidence !== undefined) {
|
|
688
|
+
const at = (r) => r.graphHash === envelope.graphHash && (envelope.source.commit === null || r.commit === envelope.source.commit);
|
|
689
|
+
const statusOf = new Map(evidence.verifications.map((v) => [v.id, v.status]));
|
|
690
|
+
for (const artifact of envelope.artifacts) {
|
|
691
|
+
if (artifact.type === "verification" && !statusOf.has(artifact.id)) {
|
|
692
|
+
fail("/reports/evidence/verifications", `Verification ${artifact.id} is exported but has no evidence entry.`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
evidence.verifications.forEach((v, i) => {
|
|
696
|
+
const p = `/reports/evidence/verifications/${i}`;
|
|
697
|
+
if (types.get(v.id) !== "verification")
|
|
698
|
+
fail(`${p}/id`, `${v.id} is not a verification artifact in the export.`);
|
|
699
|
+
if (v.latest !== null && v.latest.verificationId !== v.id) {
|
|
700
|
+
fail(`${p}/latest/verificationId`, `The latest record of ${v.id} is a run of ${v.latest.verificationId}.`);
|
|
701
|
+
}
|
|
702
|
+
if (v.status.startsWith("current-") || v.status === "skip" || v.status === "error") {
|
|
703
|
+
const result = v.status.replace("current-", "");
|
|
704
|
+
if (v.latest === null || !at(v.latest) || v.latest.result !== result) {
|
|
705
|
+
fail(`${p}/status`, `${v.id} is ${v.status}, but its latest record is not a ${result} at the export's snapshot.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else if (v.status === "stale") {
|
|
709
|
+
if (v.latest === null || at(v.latest))
|
|
710
|
+
fail(`${p}/status`, `${v.id} is stale, but has no record from another snapshot.`);
|
|
711
|
+
}
|
|
712
|
+
else if (v.status === "missing" && (v.latest !== null || v.runs !== 0)) {
|
|
713
|
+
fail(`${p}/status`, `${v.id} is missing, but has run records.`);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
evidence.claims.forEach((claim, i) => {
|
|
717
|
+
const p = `/reports/evidence/claims/${i}`;
|
|
718
|
+
if (types.get(claim.id) !== claim.type)
|
|
719
|
+
fail(`${p}/id`, `${claim.id} is not a ${claim.type} artifact in the export.`);
|
|
720
|
+
if (claim.linked !== claim.verifications.length + claim.withheld > 0) {
|
|
721
|
+
fail(`${p}/linked`, `${claim.id} says linked is ${claim.linked} with ${claim.verifications.length + claim.withheld} verifications.`);
|
|
722
|
+
}
|
|
723
|
+
if (claim.verified && (!claim.linked || claim.verifications.some((id) => statusOf.get(id) !== "current-pass"))) {
|
|
724
|
+
fail(`${p}/verified`, `${claim.id} is marked verified, but not every verification naming it is a current pass.`);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
const knowledge = envelope.reports.knowledge;
|
|
729
|
+
if (knowledge !== undefined) {
|
|
730
|
+
knowledge.artifacts.forEach((entry, i) => {
|
|
731
|
+
const p = `/reports/knowledge/artifacts/${i}`;
|
|
732
|
+
if (types.get(entry.id) !== "knowledge")
|
|
733
|
+
fail(`${p}/id`, `${entry.id} is not a knowledge artifact in the export.`);
|
|
734
|
+
if (entry.status === "current" &&
|
|
735
|
+
(entry.changed.length > 0 || entry.missing.length > 0 || entry.reviewer === null || entry.reviewedAt === null)) {
|
|
736
|
+
fail(`${p}/status`, `${entry.id} is current, but a source changed or is unpinned, or no reviewer or time is named.`);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const impact = envelope.reports.impact;
|
|
741
|
+
if (impact !== undefined) {
|
|
742
|
+
impact.starts.forEach((entry, i) => {
|
|
743
|
+
const p = `/reports/impact/starts/${i}`;
|
|
744
|
+
if (!types.has(entry.start))
|
|
745
|
+
fail(`${p}/start`, `${entry.start} is not an artifact in the export.`);
|
|
746
|
+
for (const group of ["direct", "candidates", "context"]) {
|
|
747
|
+
entry[group].forEach((hit, j) => {
|
|
748
|
+
const last = hit.path[hit.path.length - 1];
|
|
749
|
+
if (!types.has(hit.id) || last?.id !== hit.id || hit.path.some((step) => !types.has(step.id))) {
|
|
750
|
+
fail(`${p}/${group}/${j}`, `The ${group} hit ${hit.id} names an artifact the export lacks, or its path does not end at it.`);
|
|
751
|
+
}
|
|
752
|
+
else if ((group === "candidates") !== hit.path.length > 1) {
|
|
753
|
+
fail(`${p}/${group}/${j}`, `A ${group} hit has a path of ${hit.path.length} step${hit.path.length === 1 ? "" : "s"}.`);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
const ownership = envelope.reports.ownership;
|
|
760
|
+
if (ownership !== undefined) {
|
|
761
|
+
ownership.files.forEach((file, i) => {
|
|
762
|
+
const p = `/reports/ownership/files/${i}`;
|
|
763
|
+
const slice = file.region === "slice" || file.region === "test";
|
|
764
|
+
if ((file.region === "slice" && file.owner === null) ||
|
|
765
|
+
(slice && file.owner !== null && types.get(file.owner) !== "slice")) {
|
|
766
|
+
fail(`${p}/owner`, `${file.path} is in a slice region, but its owner is not a slice in the export.`);
|
|
767
|
+
}
|
|
768
|
+
if (!slice && file.region !== "resource" && file.owner !== null) {
|
|
769
|
+
fail(`${p}/owner`, `${file.path} is in the ${file.region} region, which has no owner.`);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return problems;
|
|
774
|
+
}
|
|
775
|
+
//# sourceMappingURL=read-export.js.map
|