@intentset/core 0.5.0 → 0.6.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 +63 -1
- package/dist/artifact.js.map +1 -1
- package/dist/export.d.ts +1 -1
- package/dist/export.js +13 -1
- package/dist/export.js.map +1 -1
- package/dist/read-export.d.ts.map +1 -1
- package/dist/read-export.js +38 -3
- package/dist/read-export.js.map +1 -1
- package/dist/registries.d.ts +2 -2
- package/dist/registries.js +1 -1
- package/dist/registries.js.map +1 -1
- package/dist/types.d.ts +33 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -4
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +38 -5
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
- package/src/artifact.ts +84 -0
- package/src/export.ts +14 -1
- package/src/read-export.ts +38 -2
- package/src/registries.ts +3 -3
- package/src/types.ts +38 -4
- package/src/validate.ts +45 -7
package/src/read-export.ts
CHANGED
|
@@ -26,10 +26,14 @@ import {
|
|
|
26
26
|
ID_PATTERN,
|
|
27
27
|
LEVELS,
|
|
28
28
|
LINK_KINDS,
|
|
29
|
+
MEASURE_DIRECTIONS,
|
|
30
|
+
METRIC_PATTERN,
|
|
29
31
|
OWNERSHIP_REGIONS,
|
|
30
32
|
STATUSES,
|
|
31
33
|
SUPPORTED_EXPORT_CONTRACTS,
|
|
32
34
|
ARTIFACT_TYPES,
|
|
35
|
+
TIP_MAX_LENGTH,
|
|
36
|
+
TIP_PATTERN,
|
|
33
37
|
VISIBILITIES,
|
|
34
38
|
} from "./types.ts";
|
|
35
39
|
|
|
@@ -364,9 +368,18 @@ class Shape {
|
|
|
364
368
|
|
|
365
369
|
registries(value: unknown): void {
|
|
366
370
|
const at = "/registries";
|
|
367
|
-
const r = this.object(value, at, [
|
|
371
|
+
const r = this.object(value, at, [
|
|
372
|
+
"owners",
|
|
373
|
+
"audiences",
|
|
374
|
+
"releases",
|
|
375
|
+
"roles",
|
|
376
|
+
"editions",
|
|
377
|
+
"flags",
|
|
378
|
+
"evidenceSources",
|
|
379
|
+
"resources",
|
|
380
|
+
]);
|
|
368
381
|
if (r === null) return;
|
|
369
|
-
for (const key of ["owners", "audiences", "releases", "roles", "editions", "flags"])
|
|
382
|
+
for (const key of ["owners", "audiences", "releases", "roles", "editions", "flags", "evidenceSources"])
|
|
370
383
|
this.strings(r[key], `${at}/${key}`);
|
|
371
384
|
this.array(r.resources, `${at}/resources`, (resource, p) => {
|
|
372
385
|
const x = this.object(resource, p, ["id", "path", "owner", "consumers"]);
|
|
@@ -401,6 +414,8 @@ class Shape {
|
|
|
401
414
|
"availability",
|
|
402
415
|
"slice",
|
|
403
416
|
"verification",
|
|
417
|
+
"measure",
|
|
418
|
+
"tips",
|
|
404
419
|
"extensions",
|
|
405
420
|
],
|
|
406
421
|
["body"],
|
|
@@ -444,6 +459,27 @@ class Shape {
|
|
|
444
459
|
this.string(v.locator, `${p}/locator`);
|
|
445
460
|
this.string(v.selector, `${p}/selector`);
|
|
446
461
|
});
|
|
462
|
+
this.nullable(a.measure, `${at}/measure`, (x, p) => {
|
|
463
|
+
const m = this.object(x, p, ["metric", "baseline", "target", "window", "source", "direction"]);
|
|
464
|
+
if (m === null) return;
|
|
465
|
+
this.string(m.metric, `${p}/metric`, METRIC_PATTERN, "a metric identifier such as median_time_to_intervention");
|
|
466
|
+
for (const key of ["baseline", "target", "window", "source"]) this.string(m[key], `${p}/${key}`);
|
|
467
|
+
this.nullable(m.direction, `${p}/direction`, (d, q) => this.oneOf(d, q, MEASURE_DIRECTIONS));
|
|
468
|
+
});
|
|
469
|
+
this.nullable(a.tips, `${at}/tips`, (x, p) => {
|
|
470
|
+
if (!isRecord(x)) {
|
|
471
|
+
this.fail(p, "Expected an object of tips by explained ID.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const key of Object.keys(x).sort(compareStrings)) {
|
|
475
|
+
const q = `${p}/${key}`;
|
|
476
|
+
if (!ID_PATTERN.test(key)) this.fail(q, "Expected an artifact ID as the key.");
|
|
477
|
+
const tip = x[key];
|
|
478
|
+
if (typeof tip !== "string" || tip.length > TIP_MAX_LENGTH || !TIP_PATTERN.test(tip)) {
|
|
479
|
+
this.fail(q, `Expected one line of plain text, at most ${TIP_MAX_LENGTH} characters.`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
447
483
|
this.nullable(a.extensions, `${at}/extensions`, (x, p) => this.extensions(x, p));
|
|
448
484
|
if (Object.hasOwn(a, "body") && typeof a.body !== "string") this.fail(`${at}/body`, "Expected a string.");
|
|
449
485
|
}
|
package/src/registries.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The repository registries (Core §4, §7; VSA §3): owners, audiences, the
|
|
3
|
-
* four release dimensions, and the shared-resource inventory. The file is
|
|
2
|
+
* The repository registries (Core §4, §6, §7; VSA §3): owners, audiences, the
|
|
3
|
+
* four release dimensions, the evidence sources measures name, and the shared-resource inventory. The file is
|
|
4
4
|
* `registries.yaml` in the shape of examples/scheduling/registries.yaml.
|
|
5
5
|
* A missing or empty file is empty registries, which the validator reads as
|
|
6
6
|
* "not declared" rather than "nothing allowed".
|
|
@@ -10,7 +10,7 @@ import { compareStrings, isRecord, makeDiagnostic } from "./report.ts";
|
|
|
10
10
|
import { EMPTY_REGISTRIES, ID_PATTERN, type Registries, type Resource } from "./types.ts";
|
|
11
11
|
import { parseYamlDetailed } from "./yaml.ts";
|
|
12
12
|
|
|
13
|
-
const LIST_KEYS = ["owners", "audiences", "releases", "roles", "editions", "flags"] as const;
|
|
13
|
+
const LIST_KEYS = ["owners", "audiences", "releases", "roles", "editions", "flags", "evidenceSources"] as const;
|
|
14
14
|
const KNOWN_KEYS = new Set<string>([...LIST_KEYS, "resources"]);
|
|
15
15
|
|
|
16
16
|
/**
|
package/src/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ export const ARTIFACT_TYPES = [
|
|
|
10
10
|
"product",
|
|
11
11
|
"intent",
|
|
12
12
|
"outcome",
|
|
13
|
+
"measure",
|
|
13
14
|
"capability",
|
|
14
15
|
"behavior",
|
|
15
16
|
"rule",
|
|
@@ -75,6 +76,7 @@ export const LINK_ENDPOINTS: Record<
|
|
|
75
76
|
export const PARENT_TARGETS: Partial<Record<ArtifactType, readonly ArtifactType[]>> = {
|
|
76
77
|
intent: ["product"],
|
|
77
78
|
outcome: ["intent"],
|
|
79
|
+
measure: ["outcome"],
|
|
78
80
|
capability: ["outcome", "capability"],
|
|
79
81
|
behavior: ["capability"],
|
|
80
82
|
};
|
|
@@ -84,6 +86,7 @@ export const REQUIRED_SECTIONS: Record<ArtifactType, readonly string[]> = {
|
|
|
84
86
|
product: ["Scope"],
|
|
85
87
|
intent: ["Rationale"],
|
|
86
88
|
outcome: ["Measure"],
|
|
89
|
+
measure: ["Method"],
|
|
87
90
|
capability: ["Overview"],
|
|
88
91
|
behavior: ["Behavior", "Preconditions", "Outcomes"],
|
|
89
92
|
rule: ["Constraint"],
|
|
@@ -135,6 +138,28 @@ export interface VerificationMeta {
|
|
|
135
138
|
selector: string;
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
export const MEASURE_DIRECTIONS = ["increase", "decrease"] as const;
|
|
142
|
+
export type MeasureDirection = (typeof MEASURE_DIRECTIONS)[number];
|
|
143
|
+
|
|
144
|
+
/** Core §6: a metric identifier, lower case, words joined by `_` or `-`. */
|
|
145
|
+
export const METRIC_PATTERN = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*$/;
|
|
146
|
+
|
|
147
|
+
/** The literal a measure's baseline carries when none has been taken yet (Core §6). */
|
|
148
|
+
export const BASELINE_UNKNOWN = "unknown";
|
|
149
|
+
|
|
150
|
+
/** Core §6 measure metadata: how the parent outcome is judged. */
|
|
151
|
+
export interface MeasureMeta {
|
|
152
|
+
metric: string;
|
|
153
|
+
/** A value, or `unknown` (BASELINE_UNKNOWN). */
|
|
154
|
+
baseline: string;
|
|
155
|
+
target: string;
|
|
156
|
+
/** When the measure is read, relative to a release or a date; prose, not parsed. */
|
|
157
|
+
window: string;
|
|
158
|
+
/** An entry in the repository's `evidenceSources` registry. */
|
|
159
|
+
source: string;
|
|
160
|
+
direction?: MeasureDirection;
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
/** The `intentset` block of a document's frontmatter after validation, Core §4. */
|
|
139
164
|
export interface ArtifactMeta {
|
|
140
165
|
spec: typeof SPEC_VERSION;
|
|
@@ -158,6 +183,7 @@ export interface ArtifactMeta {
|
|
|
158
183
|
tips?: Record<string, string>;
|
|
159
184
|
slice?: SliceMeta;
|
|
160
185
|
verification?: VerificationMeta;
|
|
186
|
+
measure?: MeasureMeta;
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
export interface Heading {
|
|
@@ -234,6 +260,8 @@ export interface Registries {
|
|
|
234
260
|
roles: string[];
|
|
235
261
|
editions: string[];
|
|
236
262
|
flags: string[];
|
|
263
|
+
/** Where a measure's evidence is expected to come from (Core §6): an analytics product, a study, a survey. */
|
|
264
|
+
evidenceSources: string[];
|
|
237
265
|
resources: Resource[];
|
|
238
266
|
}
|
|
239
267
|
|
|
@@ -244,6 +272,7 @@ export const EMPTY_REGISTRIES: Registries = {
|
|
|
244
272
|
roles: [],
|
|
245
273
|
editions: [],
|
|
246
274
|
flags: [],
|
|
275
|
+
evidenceSources: [],
|
|
247
276
|
resources: [],
|
|
248
277
|
};
|
|
249
278
|
|
|
@@ -303,11 +332,12 @@ export interface ImpactReport {
|
|
|
303
332
|
}
|
|
304
333
|
|
|
305
334
|
/**
|
|
306
|
-
* The versioned export envelope (spec/export.md; spec/export.schema.json). 0.2
|
|
307
|
-
* the first version a consumer
|
|
308
|
-
* restricted artifacts by default (ADR 0008).
|
|
335
|
+
* The versioned export envelope (spec/export.md; spec/export.schema.json). 0.2 was
|
|
336
|
+
* the first version a consumer could pin: 0.1 carried untyped report slots and
|
|
337
|
+
* restricted artifacts by default (ADR 0008). 0.3 added the measure type, the
|
|
338
|
+
* `evidenceSources` registry, and `measure` and `tips` on every artifact (ADR 0012).
|
|
309
339
|
*/
|
|
310
|
-
export const EXPORT_CONTRACT = "intentset/export/0.
|
|
340
|
+
export const EXPORT_CONTRACT = "intentset/export/0.3";
|
|
311
341
|
|
|
312
342
|
/** The contracts this implementation reads. A consumer rejects every other value (spec/export.md §5). */
|
|
313
343
|
export const SUPPORTED_EXPORT_CONTRACTS: readonly string[] = [EXPORT_CONTRACT];
|
|
@@ -338,6 +368,10 @@ export interface ExportArtifact {
|
|
|
338
368
|
availability: Availability | null;
|
|
339
369
|
slice: SliceMeta | null;
|
|
340
370
|
verification: VerificationMeta | null;
|
|
371
|
+
/** Null unless the artifact is a measure; `direction` is null when the author set none. */
|
|
372
|
+
measure: (Omit<MeasureMeta, "direction"> & { direction: MeasureDirection | null }) | null;
|
|
373
|
+
/** Null unless the artifact is knowledge with tips (Core §9). */
|
|
374
|
+
tips: Record<string, string> | null;
|
|
341
375
|
extensions: Record<string, unknown> | null;
|
|
342
376
|
/** Present only when the export was asked to include bodies. */
|
|
343
377
|
body?: string;
|
package/src/validate.ts
CHANGED
|
@@ -414,6 +414,18 @@ function checkLifecycle(context: Context): void {
|
|
|
414
414
|
});
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
|
+
if (meta.measure !== undefined) {
|
|
418
|
+
if (registries.evidenceSources.length === 0) use("evidenceSources");
|
|
419
|
+
else if (!registries.evidenceSources.includes(meta.measure.source)) {
|
|
420
|
+
reportOn(context, artifact, {
|
|
421
|
+
code: "CORE005",
|
|
422
|
+
field: "/intentset/measure/source",
|
|
423
|
+
message: `measure source ${meta.measure.source} is not in the evidenceSources registry.`,
|
|
424
|
+
remediation:
|
|
425
|
+
"Add the analytics product, study or survey to registries.yaml evidenceSources, or name a registered source.",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
417
429
|
meta.slice?.usesResources.forEach((resource, i) => {
|
|
418
430
|
if (registries.resources.length === 0) use("resources");
|
|
419
431
|
else if (!resourceIds.has(resource)) {
|
|
@@ -465,12 +477,22 @@ interface Requirement {
|
|
|
465
477
|
remediation: string;
|
|
466
478
|
/** Core §5 names no active obligation for this type; only the draft warning applies. */
|
|
467
479
|
draftOnly?: boolean;
|
|
480
|
+
/** Only edges from these types count: an outcome is judged by its measures, not by the capabilities under it. */
|
|
481
|
+
fromTypes?: readonly ArtifactType[];
|
|
468
482
|
}
|
|
469
483
|
|
|
470
484
|
function requirement(artifact: Artifact): Requirement | null {
|
|
471
485
|
const { meta } = artifact;
|
|
472
486
|
const id = meta.id;
|
|
473
487
|
switch (meta.type) {
|
|
488
|
+
case "outcome":
|
|
489
|
+
return {
|
|
490
|
+
kinds: ["parent"],
|
|
491
|
+
direction: "in",
|
|
492
|
+
fromTypes: ["measure"],
|
|
493
|
+
message: "no measure names it as parent, so nothing says how it will be judged",
|
|
494
|
+
remediation: `Add a measure record with \`parent: ${id}\` carrying metric, baseline, target, window and source (Core §6).`,
|
|
495
|
+
};
|
|
474
496
|
case "rule":
|
|
475
497
|
return {
|
|
476
498
|
kinds: ["governedBy"],
|
|
@@ -529,18 +551,27 @@ function requirement(artifact: Artifact): Requirement | null {
|
|
|
529
551
|
}
|
|
530
552
|
|
|
531
553
|
/**
|
|
532
|
-
* Core §5 and VSA §3: active rules, scenarios, verifications, knowledge and
|
|
533
|
-
* product slices are attached
|
|
534
|
-
* and decisions nothing refers to, are
|
|
554
|
+
* Core §5, §6 and VSA §3: active rules, scenarios, verifications, knowledge and
|
|
555
|
+
* product slices are attached, and an active outcome has a measure (CORE003).
|
|
556
|
+
* Their drafts, and draft contracts and decisions nothing refers to, are
|
|
557
|
+
* unattached drafts (CORE009, warning).
|
|
535
558
|
*/
|
|
536
559
|
function checkAttachment(context: Context): void {
|
|
537
560
|
const { graph } = context;
|
|
538
561
|
for (const artifact of sortedArtifacts(graph)) {
|
|
539
562
|
const need = requirement(artifact);
|
|
540
563
|
if (need === null || artifact.meta.status === "retired") continue;
|
|
541
|
-
const edges = need.kinds
|
|
542
|
-
|
|
543
|
-
|
|
564
|
+
const edges = need.kinds
|
|
565
|
+
.flatMap((kind) =>
|
|
566
|
+
need.direction === "in"
|
|
567
|
+
? incomingEdges(graph, artifact.meta.id, kind)
|
|
568
|
+
: outgoing(graph, artifact.meta.id, kind),
|
|
569
|
+
)
|
|
570
|
+
.filter((edge) => {
|
|
571
|
+
if (need.fromTypes === undefined) return true;
|
|
572
|
+
const from = graph.artifacts.get(edge.from);
|
|
573
|
+
return from !== undefined && need.fromTypes.includes(from.meta.type);
|
|
574
|
+
});
|
|
544
575
|
if (edges.length > 0) continue;
|
|
545
576
|
const field = need.direction === "out" ? `/intentset/links/${need.kinds[0]}` : undefined;
|
|
546
577
|
if (artifact.meta.status === "draft") {
|
|
@@ -572,7 +603,7 @@ export function patternProblem(pattern: string): string | null {
|
|
|
572
603
|
return null;
|
|
573
604
|
}
|
|
574
605
|
|
|
575
|
-
/** VSA §3
|
|
606
|
+
/** VSA §3, Core §6 and §8: type-specific blocks on their own type only; technical slices give a rationale and implement nothing; patterns are plain. */
|
|
576
607
|
function checkSlices(context: Context): void {
|
|
577
608
|
const { graph } = context;
|
|
578
609
|
for (const artifact of sortedArtifacts(graph)) {
|
|
@@ -586,6 +617,13 @@ function checkSlices(context: Context): void {
|
|
|
586
617
|
"Remove the verification block, or change the type (Core §8).",
|
|
587
618
|
);
|
|
588
619
|
}
|
|
620
|
+
if (artifact.meta.measure !== undefined && artifact.meta.type !== "measure") {
|
|
621
|
+
profile(
|
|
622
|
+
"/intentset/measure",
|
|
623
|
+
`${id} is ${aType(artifact.meta.type)} and carries measure metadata, which only a measure has.`,
|
|
624
|
+
"Remove the measure block, or change the type (Core §6).",
|
|
625
|
+
);
|
|
626
|
+
}
|
|
589
627
|
const slice = artifact.meta.slice;
|
|
590
628
|
if (slice === undefined) continue;
|
|
591
629
|
if (artifact.meta.type !== "slice") {
|