@intentset/core 0.10.0 → 0.12.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 +25 -3
- package/dist/artifact.js.map +1 -1
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +166 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +579 -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 +15 -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 +35 -2
- package/src/delivery.ts +11 -0
- package/src/export.ts +20 -11
- package/src/glob.ts +60 -0
- package/src/index.ts +4 -0
- package/src/read-export.ts +9 -0
- package/src/select.ts +753 -0
- package/src/slices.ts +149 -0
- package/src/types.ts +16 -1
- package/src/validate.ts +39 -1
package/src/artifact.ts
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
BASELINE_UNKNOWN,
|
|
29
29
|
CLAIM_KINDS,
|
|
30
30
|
type Claim,
|
|
31
|
+
DELIVERIES,
|
|
32
|
+
DELIVERY_TYPES,
|
|
33
|
+
type Delivery,
|
|
31
34
|
type DocumentInput,
|
|
32
35
|
ID_PATTERN,
|
|
33
36
|
LINK_KINDS,
|
|
@@ -79,6 +82,7 @@ const OPTIONAL_COMMON = [
|
|
|
79
82
|
"revision",
|
|
80
83
|
"reviewedAt",
|
|
81
84
|
"reviewedBy",
|
|
85
|
+
"delivery",
|
|
82
86
|
"extensions",
|
|
83
87
|
"tips",
|
|
84
88
|
"slice",
|
|
@@ -383,6 +387,24 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
383
387
|
if (typeof s.reviewedBy === "string") reviewedBy = s.reviewedBy;
|
|
384
388
|
else problem(`${base}/reviewedBy`, "`reviewedBy` must be a string.", "Name the reviewer or review record.");
|
|
385
389
|
}
|
|
390
|
+
let delivery: Delivery | undefined;
|
|
391
|
+
if (Object.hasOwn(s, "delivery") && s.delivery !== null) {
|
|
392
|
+
if (type !== null && !(DELIVERY_TYPES as readonly string[]).includes(type)) {
|
|
393
|
+
problem(
|
|
394
|
+
`${base}/delivery`,
|
|
395
|
+
`\`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.`,
|
|
396
|
+
"Remove `delivery`; it states whether the code a record describes exists (Core §7).",
|
|
397
|
+
);
|
|
398
|
+
} else if (typeof s.delivery === "string" && (DELIVERIES as readonly string[]).includes(s.delivery)) {
|
|
399
|
+
delivery = s.delivery as Delivery;
|
|
400
|
+
} else {
|
|
401
|
+
problem(
|
|
402
|
+
`${base}/delivery`,
|
|
403
|
+
"`delivery` must be planned or built.",
|
|
404
|
+
"Write `delivery: built` when the code the record describes exists, `delivery: planned` when it does not yet, or leave it out (Core §7).",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
386
408
|
let extensions: Record<string, unknown> | undefined;
|
|
387
409
|
if (Object.hasOwn(s, "extensions") && s.extensions !== null) {
|
|
388
410
|
if (!isRecord(s.extensions)) {
|
|
@@ -462,6 +484,7 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
462
484
|
if (revision !== undefined) meta.revision = revision;
|
|
463
485
|
if (reviewedAt !== undefined) meta.reviewedAt = reviewedAt;
|
|
464
486
|
if (reviewedBy !== undefined) meta.reviewedBy = reviewedBy;
|
|
487
|
+
if (delivery !== undefined) meta.delivery = delivery;
|
|
465
488
|
if (extensions !== undefined) meta.extensions = extensions;
|
|
466
489
|
if (tips !== undefined) meta.tips = tips;
|
|
467
490
|
if (slice !== undefined) meta.slice = slice;
|
|
@@ -472,6 +495,8 @@ function readMeta(s: Record<string, unknown>, problem: Problem): ArtifactMeta |
|
|
|
472
495
|
|
|
473
496
|
interface ListOptions {
|
|
474
497
|
minItems?: number;
|
|
498
|
+
/** What to do about a list shorter than `minItems`, when "add a value" is not enough of an answer. */
|
|
499
|
+
minItemsRemediation?: string;
|
|
475
500
|
pattern?: RegExp;
|
|
476
501
|
/** Code for a repeated entry; CORE001 unless the list is a relationship. */
|
|
477
502
|
duplicateCode?: string;
|
|
@@ -529,7 +554,7 @@ function stringList(
|
|
|
529
554
|
problem(
|
|
530
555
|
field,
|
|
531
556
|
`\`${name}\` must list at least ${options.minItems} entr${options.minItems === 1 ? "y" : "ies"}.`,
|
|
532
|
-
`Add at least one value to ${name}.`,
|
|
557
|
+
options.minItemsRemediation ?? `Add at least one value to ${name}.`,
|
|
533
558
|
);
|
|
534
559
|
}
|
|
535
560
|
return null;
|
|
@@ -601,7 +626,15 @@ function readAvailability(value: unknown, field: string, problem: Problem): Avai
|
|
|
601
626
|
ok = false;
|
|
602
627
|
continue;
|
|
603
628
|
}
|
|
604
|
-
|
|
629
|
+
// Availability intersects on every dimension (Core §7), so an empty one would apply the artifact nowhere; a
|
|
630
|
+
// product with no roles or editions of its own still names one of each, and registers it.
|
|
631
|
+
const list = stringList(value[key], `${field}/${key}`, key, problem, {
|
|
632
|
+
minItems: key === "flags" ? 0 : 1,
|
|
633
|
+
minItemsRemediation:
|
|
634
|
+
key === "products"
|
|
635
|
+
? "Name the product the artifact belongs to."
|
|
636
|
+
: `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.`,
|
|
637
|
+
});
|
|
605
638
|
if (list === null) ok = false;
|
|
606
639
|
else out[key] = list;
|
|
607
640
|
}
|
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,17 @@ 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";
|
|
8
10
|
export * from "./hash.ts";
|
|
9
11
|
export * from "./impact.ts";
|
|
10
12
|
export * from "./read-export.ts";
|
|
11
13
|
export * from "./registries.ts";
|
|
14
|
+
export * from "./select.ts";
|
|
15
|
+
export * from "./slices.ts";
|
|
12
16
|
export * from "./types.ts";
|
|
13
17
|
export * from "./validate.ts";
|
|
14
18
|
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 });
|