@intentset/core 0.9.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/slices.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slices, the code they claim and the records that describe them (Core §10),
|
|
3
|
+
* the definitions `review`'s drift list, `context <path>` and `select`'s
|
|
4
|
+
* `describing` and `claims` fields share. They moved here from the CLI in
|
|
5
|
+
* 0.11.0 (ADR 0016), so a consumer calls Intentset's definition rather than
|
|
6
|
+
* copying it.
|
|
7
|
+
*
|
|
8
|
+
* A slice's code is what its `source`, `backend` and `contract` claims match;
|
|
9
|
+
* its `verification` and `documentation` claims are checks and prose, not
|
|
10
|
+
* implementation. The records that describe a slice are the slice itself, the
|
|
11
|
+
* behaviors it implements, the rules that govern them, the scenarios that
|
|
12
|
+
* illustrate them, the verifications of any of those, the contracts it
|
|
13
|
+
* exposes, and the decisions that inform the slice, its behaviors or its
|
|
14
|
+
* contracts. Knowledge is left out: it explains the model to an audience, and
|
|
15
|
+
* review pins already mark it for review when the model changes.
|
|
16
|
+
*/
|
|
17
|
+
import { matchGlob } from "./glob.ts";
|
|
18
|
+
import { compareStrings } from "./report.ts";
|
|
19
|
+
import {
|
|
20
|
+
type ArtifactType,
|
|
21
|
+
type Artifact,
|
|
22
|
+
type Claim,
|
|
23
|
+
type Graph,
|
|
24
|
+
LINK_ENDPOINTS,
|
|
25
|
+
type LinkKind,
|
|
26
|
+
PARENT_TARGETS,
|
|
27
|
+
} from "./types.ts";
|
|
28
|
+
|
|
29
|
+
const CODE_CLAIMS = new Set(["source", "backend", "contract"]);
|
|
30
|
+
|
|
31
|
+
/** Every slice that is not retired and has slice metadata, sorted by ID. */
|
|
32
|
+
export function activeSlices(graph: Graph): Artifact[] {
|
|
33
|
+
return [...graph.artifacts.values()]
|
|
34
|
+
.filter((a) => a.meta.type === "slice" && a.meta.status !== "retired" && a.meta.slice !== undefined)
|
|
35
|
+
.sort((a, b) => compareStrings(a.meta.id, b.meta.id));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The slices with any claim matching `path`, sorted by ID. */
|
|
39
|
+
export function claimingSlices(graph: Graph, path: string): Artifact[] {
|
|
40
|
+
return activeSlices(graph).filter((slice) => claimsPath(slice.meta.slice?.claims ?? [], path));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True when any of `claims` matches the repository-relative `path` (VSA §3). */
|
|
44
|
+
export function claimsPath(claims: readonly Claim[], path: string): boolean {
|
|
45
|
+
return claims.some((claim) => matchGlob(claim.path, path));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when one of the slice's source, backend or contract claims matches `path`. */
|
|
49
|
+
export function isSliceCode(slice: Artifact, path: string): boolean {
|
|
50
|
+
return slice.meta.slice?.claims.some((claim) => CODE_CLAIMS.has(claim.kind) && matchGlob(claim.path, path)) ?? false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The IDs of the records that describe a slice, the slice included, sorted. Only IDs present in the graph. */
|
|
54
|
+
export function describingRecords(graph: Graph, slice: Artifact): string[] {
|
|
55
|
+
return [...describing(graphIndex(graph), slice.meta.id, false).ids].sort(compareStrings);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What the describing traversal reads: authored links in each direction among the records it can see, and which of
|
|
60
|
+
* them have link ends it cannot see. A graph hides nothing; an export hides what it withheld (spec/list.md §3).
|
|
61
|
+
*/
|
|
62
|
+
export interface LinkIndex {
|
|
63
|
+
has(id: string): boolean;
|
|
64
|
+
type(id: string): ArtifactType | undefined;
|
|
65
|
+
/** Visible targets of the authored `kind` links leaving `id`, as authored (an unresolved ID included). */
|
|
66
|
+
targets(id: string, kind: LinkKind): readonly string[];
|
|
67
|
+
/** Visible sources of the authored `kind` links arriving at `id`. */
|
|
68
|
+
sources(id: string, kind: LinkKind): readonly string[];
|
|
69
|
+
/** True when `id` has a link end at a withheld artifact. */
|
|
70
|
+
hidden(id: string): boolean;
|
|
71
|
+
/** Visible records of `type` with a link end at a withheld artifact, sorted. */
|
|
72
|
+
hiddenOfType(type: ArtifactType): readonly string[];
|
|
73
|
+
/** True when the source withheld any artifact. */
|
|
74
|
+
withholds: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A graph as a LinkIndex: everything visible, nothing hidden. */
|
|
78
|
+
export function graphIndex(graph: Graph): LinkIndex {
|
|
79
|
+
return {
|
|
80
|
+
has: (id) => graph.artifacts.has(id),
|
|
81
|
+
type: (id) => graph.artifacts.get(id)?.meta.type,
|
|
82
|
+
targets: (id, kind) => (graph.out.get(id) ?? []).filter((edge) => edge.kind === kind).map((edge) => edge.to),
|
|
83
|
+
sources: (id, kind) => (graph.in.get(id) ?? []).filter((edge) => edge.kind === kind).map((edge) => edge.from),
|
|
84
|
+
hidden: () => false,
|
|
85
|
+
hiddenOfType: () => [],
|
|
86
|
+
withholds: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True when a record of `type` could have a `kind` link in `direction` at all (Core §5's endpoints), so a withheld
|
|
92
|
+
* end of that record could be one. A rule authors no `governedBy`, so its withheld ends never change that answer.
|
|
93
|
+
*/
|
|
94
|
+
export function linkPossible(type: ArtifactType, direction: "out" | "in", kind: LinkKind): boolean {
|
|
95
|
+
if (kind === "replacedBy") return true;
|
|
96
|
+
if (kind === "parent") {
|
|
97
|
+
return direction === "out"
|
|
98
|
+
? PARENT_TARGETS[type] !== undefined
|
|
99
|
+
: Object.values(PARENT_TARGETS).some((targets) => targets?.includes(type));
|
|
100
|
+
}
|
|
101
|
+
const ends = LINK_ENDPOINTS[kind];
|
|
102
|
+
return (direction === "out" ? ends.from : ends.to).includes(type);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A set of reached records, and whether a withheld record may also have been reached. */
|
|
106
|
+
interface Reach {
|
|
107
|
+
ids: Set<string>;
|
|
108
|
+
wild: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The describing traversal from `sliceId` (Core §10). Exact (`loose` false), it reaches what the visible links reach.
|
|
113
|
+
* Loose, it also reaches every record a withheld artifact could have connected: where a reached record has a withheld
|
|
114
|
+
* end of the kind a step follows, the step may reach a withheld record, and from a withheld record every visible
|
|
115
|
+
* record of the step's type that has a withheld end of its own. Loose minus exact is what is undetermined.
|
|
116
|
+
*/
|
|
117
|
+
export function describing(index: LinkIndex, sliceId: string, loose: boolean): Reach {
|
|
118
|
+
const start: Reach = index.has(sliceId)
|
|
119
|
+
? { ids: new Set([sliceId]), wild: false }
|
|
120
|
+
: { ids: new Set(), wild: loose && index.withholds };
|
|
121
|
+
const step = (from: Reach, kind: LinkKind, direction: "out" | "in", type: ArtifactType): Reach => {
|
|
122
|
+
const ids = new Set<string>();
|
|
123
|
+
let wild = false;
|
|
124
|
+
for (const id of from.ids) {
|
|
125
|
+
for (const next of direction === "out" ? index.targets(id, kind) : index.sources(id, kind)) ids.add(next);
|
|
126
|
+
const own = index.type(id);
|
|
127
|
+
if (loose && own !== undefined && index.hidden(id) && linkPossible(own, direction, kind)) wild = true;
|
|
128
|
+
}
|
|
129
|
+
if (loose && from.wild) {
|
|
130
|
+
wild = true;
|
|
131
|
+
for (const id of index.hiddenOfType(type)) ids.add(id);
|
|
132
|
+
}
|
|
133
|
+
return { ids, wild };
|
|
134
|
+
};
|
|
135
|
+
const union = (...reaches: Reach[]): Reach => ({
|
|
136
|
+
ids: new Set(reaches.flatMap((reach) => [...reach.ids])),
|
|
137
|
+
wild: reaches.some((reach) => reach.wild),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const behaviors = step(start, "implements", "out", "behavior");
|
|
141
|
+
const rules = step(behaviors, "governedBy", "out", "rule");
|
|
142
|
+
const scenarios = step(behaviors, "illustrates", "in", "scenario");
|
|
143
|
+
const claims = union(behaviors, rules, scenarios);
|
|
144
|
+
const verifications = step(claims, "verifies", "in", "verification");
|
|
145
|
+
const contracts = step(start, "exposes", "out", "contract");
|
|
146
|
+
const decisions = step(union(start, behaviors, contracts), "informedBy", "out", "decision");
|
|
147
|
+
const all = union(start, claims, verifications, contracts, decisions);
|
|
148
|
+
return { ids: new Set([...all.ids].filter((id) => index.has(id))), wild: all.wild };
|
|
149
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,17 @@ export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
|
|
|
29
29
|
export const STATUSES = ["draft", "approved", "implemented", "released", "deprecated", "retired"] as const;
|
|
30
30
|
export type Status = (typeof STATUSES)[number];
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Core §7, ADR 0017: whether the work a record describes exists, apart from its approval. `planned`: it does not
|
|
34
|
+
* yet; `built`: the code it describes exists. Written only on the types in `DELIVERY_TYPES`; absent, it is not stated.
|
|
35
|
+
*/
|
|
36
|
+
export const DELIVERIES = ["planned", "built"] as const;
|
|
37
|
+
export type Delivery = (typeof DELIVERIES)[number];
|
|
38
|
+
/** The types that describe work code delivers, and so may carry `delivery`. */
|
|
39
|
+
export const DELIVERY_TYPES = ["capability", "behavior", "rule", "scenario", "slice", "contract"] as const;
|
|
40
|
+
/** The statuses that claim an implementation, and so make delivery `built` when the field is absent (Core §7). */
|
|
41
|
+
export const BUILT_STATUSES = ["implemented", "released", "deprecated"] as const;
|
|
42
|
+
|
|
32
43
|
export const VISIBILITIES = ["public", "customer", "internal", "restricted"] as const;
|
|
33
44
|
export type Visibility = (typeof VISIBILITIES)[number];
|
|
34
45
|
|
|
@@ -182,6 +193,8 @@ export interface ArtifactMeta {
|
|
|
182
193
|
revision?: number;
|
|
183
194
|
reviewedAt?: string;
|
|
184
195
|
reviewedBy?: string;
|
|
196
|
+
/** Core §7, ADR 0017: whether the work exists, on the types in `DELIVERY_TYPES` only. */
|
|
197
|
+
delivery?: Delivery;
|
|
185
198
|
/** Namespaced extensions, preserved and never interpreted. */
|
|
186
199
|
extensions?: Record<string, unknown>;
|
|
187
200
|
/** Knowledge only (Core §9): one sentence per explained ID, keys sorted. */
|
|
@@ -383,7 +396,7 @@ export interface ImpactReport {
|
|
|
383
396
|
* subject it was judged at, each record's binding and subject hash, and a
|
|
384
397
|
* verification's `exercises` (ADR 0014).
|
|
385
398
|
*/
|
|
386
|
-
export const EXPORT_CONTRACT = "intentset/export/0.
|
|
399
|
+
export const EXPORT_CONTRACT = "intentset/export/0.5";
|
|
387
400
|
|
|
388
401
|
/** The contracts this implementation reads. A consumer rejects every other value (spec/export.md §5). */
|
|
389
402
|
export const SUPPORTED_EXPORT_CONTRACTS: readonly string[] = [EXPORT_CONTRACT];
|
|
@@ -397,6 +410,8 @@ export interface ExportArtifact {
|
|
|
397
410
|
type: ArtifactType;
|
|
398
411
|
title: string;
|
|
399
412
|
status: Status;
|
|
413
|
+
/** The record's `delivery` as authored, null when it states none (Core §7, ADR 0017); `deliveryOf` gives the effective value. */
|
|
414
|
+
delivery: Delivery | null;
|
|
400
415
|
owner: string;
|
|
401
416
|
visibility: Visibility;
|
|
402
417
|
audiences: string[];
|
package/src/validate.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* are reported as CORE002; neither is merged with the other.
|
|
12
12
|
*/
|
|
13
13
|
import { type ReadDocument, readDocument } from "./artifact.ts";
|
|
14
|
+
import { deliveryOf } from "./delivery.ts";
|
|
14
15
|
import { type Diagnostic, hasErrors, sortDiagnostics } from "./diagnostics.ts";
|
|
15
16
|
import { buildGraph, incomingEdges, outgoing } from "./graph.ts";
|
|
16
17
|
import { type Locate, aType, capitalize, compareStrings, describeTypes, makeDiagnostic } from "./report.ts";
|
|
@@ -449,6 +450,27 @@ function checkLifecycle(context: Context): void {
|
|
|
449
450
|
});
|
|
450
451
|
}
|
|
451
452
|
});
|
|
453
|
+
if (meta.delivery === "planned" && deliveryOf({ status: meta.status }) === "built") {
|
|
454
|
+
reportOn(context, artifact, {
|
|
455
|
+
code: "CORE005",
|
|
456
|
+
field: "/intentset/delivery",
|
|
457
|
+
message: `${meta.id} says its work is planned while its status, ${meta.status}, claims an implementation.`,
|
|
458
|
+
remediation:
|
|
459
|
+
"Write `delivery: built`, or remove `delivery`, when the code exists; return the record to an earlier status, with a review note, when it does not (Core §7).",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (meta.type === "behavior" && deliveryOf(meta) === "built") {
|
|
463
|
+
for (const edge of incomingEdges(graph, meta.id, "implements")) {
|
|
464
|
+
const slice = graph.artifacts.get(edge.from);
|
|
465
|
+
if (slice?.meta.delivery !== "planned") continue;
|
|
466
|
+
reportOn(context, artifact, {
|
|
467
|
+
code: "CORE005",
|
|
468
|
+
field: meta.delivery === undefined ? "/intentset/status" : "/intentset/delivery",
|
|
469
|
+
message: `${meta.id}'s work is built, but ${slice.meta.id}, which implements it, says its work is planned.`,
|
|
470
|
+
remediation: `Write \`delivery: built\` on ${slice.meta.id} once the code it claims exists, or correct ${meta.id}'s delivery (Core §7).`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
452
474
|
if (meta.status === "retired" && (meta.links.replacedBy ?? []).length === 0) {
|
|
453
475
|
reportOn(context, artifact, {
|
|
454
476
|
code: "CORE005",
|
|
@@ -701,13 +723,29 @@ function checkOwnership(context: Context): void {
|
|
|
701
723
|
const { graph } = context;
|
|
702
724
|
for (const artifact of sortedArtifacts(graph)) {
|
|
703
725
|
const { meta } = artifact;
|
|
704
|
-
if (meta.type !== "behavior"
|
|
726
|
+
if (meta.type !== "behavior") continue;
|
|
727
|
+
// ADR 0017: a behavior that is not yet owned by status but says its work is built is held to having an owner,
|
|
728
|
+
// as a warning, so built code nobody owns shows before anyone approves it.
|
|
729
|
+
const builtDraft = !OWNED_STATUSES.has(meta.status) && meta.status !== "retired" && deliveryOf(meta) === "built";
|
|
730
|
+
if (!OWNED_STATUSES.has(meta.status) && !builtDraft) continue;
|
|
705
731
|
const owners = incomingEdges(graph, meta.id, "implements")
|
|
706
732
|
.map((edge) => graph.artifacts.get(edge.from))
|
|
707
733
|
.filter((slice): slice is Artifact => slice?.meta.slice?.kind === "product")
|
|
708
734
|
.map((slice) => slice.meta.id)
|
|
709
735
|
.sort(compareStrings);
|
|
710
736
|
if (owners.length === 1) continue;
|
|
737
|
+
if (builtDraft) {
|
|
738
|
+
if (owners.length > 0) continue;
|
|
739
|
+
reportOn(context, artifact, {
|
|
740
|
+
code: "CORE006",
|
|
741
|
+
severity: "warning",
|
|
742
|
+
field: "/intentset/delivery",
|
|
743
|
+
message: `${capitalize(meta.status)} behavior ${meta.id} says its work is built, and no product slice implements it.`,
|
|
744
|
+
remediation:
|
|
745
|
+
"Add the behavior to the implements of the product slice whose code delivers it, so the built code has an owner before the behavior is approved (Core §5, §7).",
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
711
749
|
reportOn(context, artifact, {
|
|
712
750
|
code: "CORE006",
|
|
713
751
|
message:
|