@intentset/core 0.10.0 → 0.13.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/artifact.d.ts.map +1 -1
- package/dist/artifact.js +27 -3
- package/dist/artifact.js.map +1 -1
- package/dist/content-hash.d.ts +9 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +57 -0
- package/dist/content-hash.js.map +1 -0
- package/dist/delivery.d.ts +11 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +12 -0
- package/dist/delivery.js.map +1 -0
- package/dist/export.d.ts +9 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +15 -6
- package/dist/export.js.map +1 -1
- package/dist/glob.d.ts +18 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +62 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/read-export.d.ts.map +1 -1
- package/dist/read-export.js +8 -1
- package/dist/read-export.js.map +1 -1
- package/dist/select.d.ts +168 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +586 -0
- package/dist/select.js.map +1 -0
- package/dist/slices.d.ts +50 -0
- package/dist/slices.d.ts.map +1 -0
- package/dist/slices.js +111 -0
- package/dist/slices.js.map +1 -0
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +40 -1
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
- package/src/artifact.ts +37 -2
- package/src/content-hash.ts +56 -0
- package/src/delivery.ts +11 -0
- package/src/export.ts +20 -11
- package/src/glob.ts +60 -0
- package/src/index.ts +5 -0
- package/src/read-export.ts +9 -0
- package/src/select.ts +761 -0
- package/src/slices.ts +149 -0
- package/src/types.ts +18 -1
- package/src/validate.ts +39 -1
package/src/artifact.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { closingFenceIndex } from "./carrier.ts";
|
|
19
19
|
import type { Diagnostic, Location } from "./diagnostics.ts";
|
|
20
20
|
import { sha256Hex } from "./hash.ts";
|
|
21
|
+
import { contentHashOf } from "./content-hash.ts";
|
|
21
22
|
import { type Locate, NO_LOCATION, aType, capitalize, compareStrings, isRecord, makeDiagnostic } from "./report.ts";
|
|
22
23
|
import {
|
|
23
24
|
ARTIFACT_TYPES,
|
|
@@ -28,6 +29,9 @@ import {
|
|
|
28
29
|
BASELINE_UNKNOWN,
|
|
29
30
|
CLAIM_KINDS,
|
|
30
31
|
type Claim,
|
|
32
|
+
DELIVERIES,
|
|
33
|
+
DELIVERY_TYPES,
|
|
34
|
+
type Delivery,
|
|
31
35
|
type DocumentInput,
|
|
32
36
|
ID_PATTERN,
|
|
33
37
|
LINK_KINDS,
|
|
@@ -79,6 +83,7 @@ const OPTIONAL_COMMON = [
|
|
|
79
83
|
"revision",
|
|
80
84
|
"reviewedAt",
|
|
81
85
|
"reviewedBy",
|
|
86
|
+
"delivery",
|
|
82
87
|
"extensions",
|
|
83
88
|
"tips",
|
|
84
89
|
"slice",
|
|
@@ -206,6 +211,7 @@ export function readDocument(input: DocumentInput): ReadDocument {
|
|
|
206
211
|
meta,
|
|
207
212
|
path: input.path,
|
|
208
213
|
sourceHash: sha256Hex(input.source),
|
|
214
|
+
contentHash: contentHashOf(root, bodyOf(input.source)),
|
|
209
215
|
body: bodyOf(input.source),
|
|
210
216
|
headings: input.headings,
|
|
211
217
|
foreign,
|
|
@@ -383,6 +389,24 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
383
389
|
if (typeof s.reviewedBy === "string") reviewedBy = s.reviewedBy;
|
|
384
390
|
else problem(`${base}/reviewedBy`, "`reviewedBy` must be a string.", "Name the reviewer or review record.");
|
|
385
391
|
}
|
|
392
|
+
let delivery: Delivery | undefined;
|
|
393
|
+
if (Object.hasOwn(s, "delivery") && s.delivery !== null) {
|
|
394
|
+
if (type !== null && !(DELIVERY_TYPES as readonly string[]).includes(type)) {
|
|
395
|
+
problem(
|
|
396
|
+
`${base}/delivery`,
|
|
397
|
+
`\`delivery\` is written on ${aType(type)}; only ${DELIVERY_TYPES.slice(0, -1).join(", ")} and ${DELIVERY_TYPES[DELIVERY_TYPES.length - 1]} records say whether their work is built.`,
|
|
398
|
+
"Remove `delivery`; it states whether the code a record describes exists (Core §7).",
|
|
399
|
+
);
|
|
400
|
+
} else if (typeof s.delivery === "string" && (DELIVERIES as readonly string[]).includes(s.delivery)) {
|
|
401
|
+
delivery = s.delivery as Delivery;
|
|
402
|
+
} else {
|
|
403
|
+
problem(
|
|
404
|
+
`${base}/delivery`,
|
|
405
|
+
"`delivery` must be planned or built.",
|
|
406
|
+
"Write `delivery: built` when the code the record describes exists, `delivery: planned` when it does not yet, or leave it out (Core §7).",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
386
410
|
let extensions: Record<string, unknown> | undefined;
|
|
387
411
|
if (Object.hasOwn(s, "extensions") && s.extensions !== null) {
|
|
388
412
|
if (!isRecord(s.extensions)) {
|
|
@@ -462,6 +486,7 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
462
486
|
if (revision !== undefined) meta.revision = revision;
|
|
463
487
|
if (reviewedAt !== undefined) meta.reviewedAt = reviewedAt;
|
|
464
488
|
if (reviewedBy !== undefined) meta.reviewedBy = reviewedBy;
|
|
489
|
+
if (delivery !== undefined) meta.delivery = delivery;
|
|
465
490
|
if (extensions !== undefined) meta.extensions = extensions;
|
|
466
491
|
if (tips !== undefined) meta.tips = tips;
|
|
467
492
|
if (slice !== undefined) meta.slice = slice;
|
|
@@ -472,6 +497,8 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
472
497
|
|
|
473
498
|
interface ListOptions {
|
|
474
499
|
minItems?: number;
|
|
500
|
+
/** What to do about a list shorter than `minItems`, when "add a value" is not enough of an answer. */
|
|
501
|
+
minItemsRemediation?: string;
|
|
475
502
|
pattern?: RegExp;
|
|
476
503
|
/** Code for a repeated entry; CORE001 unless the list is a relationship. */
|
|
477
504
|
duplicateCode?: string;
|
|
@@ -529,7 +556,7 @@ function stringList(
|
|
|
529
556
|
problem(
|
|
530
557
|
field,
|
|
531
558
|
`\`${name}\` must list at least ${options.minItems} entr${options.minItems === 1 ? "y" : "ies"}.`,
|
|
532
|
-
`Add at least one value to ${name}.`,
|
|
559
|
+
options.minItemsRemediation ?? `Add at least one value to ${name}.`,
|
|
533
560
|
);
|
|
534
561
|
}
|
|
535
562
|
return null;
|
|
@@ -601,7 +628,15 @@ function readAvailability(value: unknown, field: string, problem: Problem): Avai
|
|
|
601
628
|
ok = false;
|
|
602
629
|
continue;
|
|
603
630
|
}
|
|
604
|
-
|
|
631
|
+
// Availability intersects on every dimension (Core §7), so an empty one would apply the artifact nowhere; a
|
|
632
|
+
// product with no roles or editions of its own still names one of each, and registers it.
|
|
633
|
+
const list = stringList(value[key], `${field}/${key}`, key, problem, {
|
|
634
|
+
minItems: key === "flags" ? 0 : 1,
|
|
635
|
+
minItemsRemediation:
|
|
636
|
+
key === "products"
|
|
637
|
+
? "Name the product the artifact belongs to."
|
|
638
|
+
: `List at least one of the ${key} the artifact applies to, from the registries; a product with no ${key} of its own still names one (such as ${key === "roles" ? "user" : key === "editions" ? "standard" : "v1"}) and registers it.`,
|
|
639
|
+
});
|
|
605
640
|
if (list === null) ok = false;
|
|
606
641
|
else out[key] = list;
|
|
607
642
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record's content hash (publication §3, ADR 0018): what a review pins
|
|
3
|
+
* when it pins `content-v1`. It covers what the record promises, and leaves
|
|
4
|
+
* out where the record stands in its lifecycle and who reviewed it, so that
|
|
5
|
+
* approving, implementing or marking delivery does not send every guide that
|
|
6
|
+
* explains the record back to review.
|
|
7
|
+
*
|
|
8
|
+
* The hash is SHA-256 over the canonical JSON (ADR 0005) of
|
|
9
|
+
* `{ body, frontmatter, ending }`:
|
|
10
|
+
*
|
|
11
|
+
* - `frontmatter` is the whole parsed frontmatter, `markset` and unknown
|
|
12
|
+
* top-level keys included, without `status`, `delivery`, `reviewedBy` and
|
|
13
|
+
* `reviewedAt` under `intentset`, and without the `intentset.org/review`
|
|
14
|
+
* extension (and `extensions` itself when nothing else is in it);
|
|
15
|
+
* - `body` is everything after the frontmatter, as written;
|
|
16
|
+
* - `ending` is the status when it is `deprecated` or `retired`, and absent
|
|
17
|
+
* otherwise, so a record that is going away still moves the hash: what a
|
|
18
|
+
* guide explains is ending, and its reader needs to know.
|
|
19
|
+
*
|
|
20
|
+
* Because it is taken over parsed values, a change of key order, quoting or
|
|
21
|
+
* indentation in the frontmatter does not move it.
|
|
22
|
+
*/
|
|
23
|
+
import { canonicalJson, sha256Hex } from "./hash.ts";
|
|
24
|
+
|
|
25
|
+
/** The `intentset` fields a content hash leaves out: lifecycle and review, not promise. */
|
|
26
|
+
export const CONTENT_HASH_OMITS = ["status", "delivery", "reviewedBy", "reviewedAt"] as const;
|
|
27
|
+
/** The extension a content hash leaves out: the review's own pins. */
|
|
28
|
+
export const CONTENT_HASH_OMITS_EXTENSION = "intentset.org/review";
|
|
29
|
+
/** The statuses a content hash names, because a record in them is going away. */
|
|
30
|
+
export const ENDING_STATUSES = ["deprecated", "retired"] as const;
|
|
31
|
+
|
|
32
|
+
/** The content hash of a record from its parsed frontmatter and its body (publication §3, ADR 0018). */
|
|
33
|
+
export function contentHashOf(frontmatter: Record<string, unknown>, body: string): string {
|
|
34
|
+
const copy: Record<string, unknown> = { ...frontmatter };
|
|
35
|
+
const section = frontmatter.intentset;
|
|
36
|
+
let ending: string | undefined;
|
|
37
|
+
if (isRecord(section)) {
|
|
38
|
+
const intentset: Record<string, unknown> = { ...section };
|
|
39
|
+
if (typeof section.status === "string" && (ENDING_STATUSES as readonly string[]).includes(section.status)) {
|
|
40
|
+
ending = section.status;
|
|
41
|
+
}
|
|
42
|
+
for (const key of CONTENT_HASH_OMITS) delete intentset[key];
|
|
43
|
+
if (isRecord(section.extensions)) {
|
|
44
|
+
const extensions: Record<string, unknown> = { ...section.extensions };
|
|
45
|
+
delete extensions[CONTENT_HASH_OMITS_EXTENSION];
|
|
46
|
+
if (Object.keys(extensions).length === 0) delete intentset.extensions;
|
|
47
|
+
else intentset.extensions = extensions;
|
|
48
|
+
}
|
|
49
|
+
copy.intentset = intentset;
|
|
50
|
+
}
|
|
51
|
+
return sha256Hex(canonicalJson({ body, frontmatter: copy, ending }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
package/src/delivery.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core §7, ADR 0017: a record's effective delivery. Its `delivery` when it states one; otherwise `built` when its
|
|
3
|
+
* status claims an implementation (`implemented`, `released`, `deprecated`); otherwise null, not stated. `approved`
|
|
4
|
+
* claims nothing about the code, so an approved record without the field is not stated.
|
|
5
|
+
*/
|
|
6
|
+
import { BUILT_STATUSES, type Delivery, type Status } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export function deliveryOf(meta: { delivery?: Delivery | null; status: Status }): Delivery | null {
|
|
9
|
+
if (meta.delivery !== undefined && meta.delivery !== null) return meta.delivery;
|
|
10
|
+
return (BUILT_STATUSES as readonly string[]).includes(meta.status) ? "built" : null;
|
|
11
|
+
}
|
package/src/export.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type ExportEnvelope,
|
|
21
21
|
type ExportImpactHit,
|
|
22
22
|
type ExportReports,
|
|
23
|
+
type Graph,
|
|
23
24
|
LINK_KINDS,
|
|
24
25
|
type Level,
|
|
25
26
|
type LinkKind,
|
|
@@ -60,7 +61,7 @@ export interface ExportMeta {
|
|
|
60
61
|
/** spec/export.md: the `intentset/export/0.3` envelope over a validation result. */
|
|
61
62
|
export function exportGraph(result: ValidationResult, registries: Registries, meta: ExportMeta): ExportEnvelope {
|
|
62
63
|
const { graph } = result;
|
|
63
|
-
const withheld = withheldIds(
|
|
64
|
+
const withheld = withheldIds(graph, meta.includeRestricted === true);
|
|
64
65
|
const ids = [...graph.artifacts.keys()].filter((id) => !withheld.ids.has(id)).sort(compareStrings);
|
|
65
66
|
const errors = result.diagnostics.filter((d) => d.severity === "error").length;
|
|
66
67
|
const warnings = result.diagnostics.length - errors;
|
|
@@ -95,11 +96,24 @@ export function exportGraph(result: ValidationResult, registries: Registries, me
|
|
|
95
96
|
diagnostics: result.diagnostics.length - diagnostics.length,
|
|
96
97
|
},
|
|
97
98
|
registries: exportRegistries(registries, withheld.ids),
|
|
98
|
-
artifacts: ids.map((id) => exportArtifact(
|
|
99
|
+
artifacts: ids.map((id) => exportArtifact(graph, id, meta.includeBodies === true, withheld.ids)),
|
|
99
100
|
reports: withholdReports(meta.reports ?? {}, withheld),
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The artifacts an export of `graph` would carry, without bodies, and how many it would withhold: the projection
|
|
106
|
+
* `select` reads a graph through (spec/list.md §2), so a list from the tree and a list from its export agree.
|
|
107
|
+
*/
|
|
108
|
+
export function projectArtifacts(
|
|
109
|
+
graph: Graph,
|
|
110
|
+
includeRestricted: boolean,
|
|
111
|
+
): { artifacts: ExportArtifact[]; withheld: number } {
|
|
112
|
+
const withheld = withheldIds(graph, includeRestricted);
|
|
113
|
+
const ids = [...graph.artifacts.keys()].filter((id) => !withheld.ids.has(id)).sort(compareStrings);
|
|
114
|
+
return { artifacts: ids.map((id) => exportArtifact(graph, id, false, withheld.ids)), withheld: withheld.ids.size };
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
interface Withheld {
|
|
104
118
|
ids: Set<string>;
|
|
105
119
|
/** Source paths of the withheld artifacts, for diagnostics that name a file but no artifact. */
|
|
@@ -110,11 +124,11 @@ interface Withheld {
|
|
|
110
124
|
|
|
111
125
|
const WITHHELD_VISIBILITIES: readonly Visibility[] = ["restricted"];
|
|
112
126
|
|
|
113
|
-
function withheldIds(
|
|
127
|
+
function withheldIds(graph: Graph, includeRestricted: boolean): Withheld {
|
|
114
128
|
const ids = new Set<string>();
|
|
115
129
|
const paths = new Set<string>();
|
|
116
130
|
if (!includeRestricted) {
|
|
117
|
-
for (const [id, artifact] of
|
|
131
|
+
for (const [id, artifact] of graph.artifacts) {
|
|
118
132
|
if (WITHHELD_VISIBILITIES.includes(artifact.meta.visibility)) {
|
|
119
133
|
ids.add(id);
|
|
120
134
|
paths.add(artifact.path);
|
|
@@ -158,13 +172,7 @@ function withholdDiagnostics(diagnostics: readonly Diagnostic[], withheld: Withh
|
|
|
158
172
|
return out;
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
function exportArtifact(
|
|
162
|
-
result: ValidationResult,
|
|
163
|
-
id: string,
|
|
164
|
-
includeBody: boolean,
|
|
165
|
-
withheld: ReadonlySet<string>,
|
|
166
|
-
): ExportArtifact {
|
|
167
|
-
const { graph } = result;
|
|
175
|
+
function exportArtifact(graph: Graph, id: string, includeBody: boolean, withheld: ReadonlySet<string>): ExportArtifact {
|
|
168
176
|
const artifact = graph.artifacts.get(id);
|
|
169
177
|
if (artifact === undefined) throw new Error(`graph has no artifact ${id}`);
|
|
170
178
|
const { meta } = artifact;
|
|
@@ -211,6 +219,7 @@ function exportArtifact(
|
|
|
211
219
|
type: meta.type,
|
|
212
220
|
title: meta.title,
|
|
213
221
|
status: meta.status,
|
|
222
|
+
delivery: meta.delivery ?? null,
|
|
214
223
|
owner: meta.owner,
|
|
215
224
|
visibility: meta.visibility,
|
|
216
225
|
audiences: [...meta.audiences].sort(compareStrings),
|
package/src/glob.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pattern language of `.intentset/config.yaml` scope and ignore lists,
|
|
3
|
+
* which is VSA §3's: repository-relative POSIX paths made of literal segments,
|
|
4
|
+
* `*` within one segment, and `**` across zero or more segments. Nothing else
|
|
5
|
+
* is special: no `?`, no classes, no braces, no negation, so there is no
|
|
6
|
+
* precedence to explain. A pattern and a path match segment by segment, and
|
|
7
|
+
* `*` never crosses a `/`.
|
|
8
|
+
*
|
|
9
|
+
* Core holds it because what a slice claims is part of the model's own
|
|
10
|
+
* definitions (Core §10, ADR 0016): `claimingSlices` and `select` match claims
|
|
11
|
+
* with it, and the CLI re-exports it. The architecture package keeps its own
|
|
12
|
+
* copy for claims, as it did before the move.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** VSA §3: true when the repository-relative POSIX `path` matches `pattern`. */
|
|
16
|
+
export function matchGlob(pattern: string, path: string): boolean {
|
|
17
|
+
const p = normalize(pattern).split("/");
|
|
18
|
+
const s = path.split("/");
|
|
19
|
+
// Memo of (pattern index, path index) pairs already known not to match, so
|
|
20
|
+
// several `**` in one pattern stay polynomial rather than exponential.
|
|
21
|
+
const failed = new Set<number>();
|
|
22
|
+
const width = s.length + 1;
|
|
23
|
+
const match = (i: number, j: number): boolean => {
|
|
24
|
+
if (failed.has(i * width + j)) return false;
|
|
25
|
+
let ok: boolean;
|
|
26
|
+
if (i === p.length) ok = j === s.length;
|
|
27
|
+
else if (p[i] === "**") ok = match(i + 1, j) || (j < s.length && match(i, j + 1));
|
|
28
|
+
else ok = j < s.length && matchSegment(p[i], s[j]) && match(i + 1, j + 1);
|
|
29
|
+
if (!ok) failed.add(i * width + j);
|
|
30
|
+
return ok;
|
|
31
|
+
};
|
|
32
|
+
return match(0, 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when any of `patterns` matches `path`. */
|
|
36
|
+
export function matchAny(patterns: readonly string[], path: string): boolean {
|
|
37
|
+
return patterns.some((pattern) => matchGlob(pattern, path));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A leading `./` names the same path; repeated `/` separate nothing. */
|
|
41
|
+
function normalize(pattern: string): string {
|
|
42
|
+
return pattern.replace(/^(?:\.\/)+/, "").replace(/\/{2,}/g, "/");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SEGMENTS = new Map<string, RegExp>();
|
|
46
|
+
|
|
47
|
+
/** One segment: literal text, with each `*` standing for any run of characters other than `/`. */
|
|
48
|
+
function matchSegment(pattern: string, name: string): boolean {
|
|
49
|
+
if (!pattern.includes("*")) return pattern === name;
|
|
50
|
+
let regex = SEGMENTS.get(pattern);
|
|
51
|
+
if (regex === undefined) {
|
|
52
|
+
const body = pattern
|
|
53
|
+
.split("*")
|
|
54
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
55
|
+
.join("[^/]*");
|
|
56
|
+
regex = new RegExp(`^${body}$`);
|
|
57
|
+
SEGMENTS.set(pattern, regex);
|
|
58
|
+
}
|
|
59
|
+
return regex.test(name);
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,18 @@ export * from "./artifact.ts";
|
|
|
2
2
|
export * from "./carrier.ts";
|
|
3
3
|
export * from "./config.ts";
|
|
4
4
|
export * from "./context.ts";
|
|
5
|
+
export * from "./delivery.ts";
|
|
5
6
|
export * from "./diagnostics.ts";
|
|
6
7
|
export * from "./export.ts";
|
|
8
|
+
export * from "./glob.ts";
|
|
7
9
|
export * from "./graph.ts";
|
|
10
|
+
export * from "./content-hash.ts";
|
|
8
11
|
export * from "./hash.ts";
|
|
9
12
|
export * from "./impact.ts";
|
|
10
13
|
export * from "./read-export.ts";
|
|
11
14
|
export * from "./registries.ts";
|
|
15
|
+
export * from "./select.ts";
|
|
16
|
+
export * from "./slices.ts";
|
|
12
17
|
export * from "./types.ts";
|
|
13
18
|
export * from "./validate.ts";
|
|
14
19
|
export * from "./yaml.ts";
|
package/src/read-export.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
EVIDENCE_BINDINGS,
|
|
24
24
|
EXPORT_EVIDENCE_STATUSES,
|
|
25
25
|
EXPORT_REPORTS,
|
|
26
|
+
DELIVERIES,
|
|
27
|
+
DELIVERY_TYPES,
|
|
26
28
|
type ExportEnvelope,
|
|
27
29
|
type ExportReportName,
|
|
28
30
|
ID_PATTERN,
|
|
@@ -414,6 +416,7 @@ class Shape {
|
|
|
414
416
|
"type",
|
|
415
417
|
"title",
|
|
416
418
|
"status",
|
|
419
|
+
"delivery",
|
|
417
420
|
"owner",
|
|
418
421
|
"visibility",
|
|
419
422
|
"audiences",
|
|
@@ -439,6 +442,12 @@ class Shape {
|
|
|
439
442
|
this.oneOf(a.type, `${at}/type`, ARTIFACT_TYPES);
|
|
440
443
|
this.string(a.title, `${at}/title`);
|
|
441
444
|
this.oneOf(a.status, `${at}/status`, STATUSES);
|
|
445
|
+
this.nullable(a.delivery, `${at}/delivery`, (d, p) => {
|
|
446
|
+
this.oneOf(d, p, DELIVERIES);
|
|
447
|
+
if (typeof a.type === "string" && !(DELIVERY_TYPES as readonly string[]).includes(a.type)) {
|
|
448
|
+
this.fail(p, `Expected null: ${a.type} records do not carry delivery.`);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
442
451
|
this.string(a.owner, `${at}/owner`);
|
|
443
452
|
this.oneOf(a.visibility, `${at}/visibility`, VISIBILITIES);
|
|
444
453
|
this.strings(a.audiences, `${at}/audiences`, { minItems: 1 });
|