@intentset/core 0.4.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/src/validate.ts CHANGED
@@ -112,6 +112,7 @@ export function validate(
112
112
  const graph = buildGraph(artifacts);
113
113
  const context: Context = { graph, registries, unreadable, locators, diagnostics, levelIndex: LEVELS.indexOf(level) };
114
114
  checkRelationships(context);
115
+ checkTips(context);
115
116
  checkCycles(context);
116
117
  checkLifecycle(context);
117
118
  checkAttachment(context);
@@ -262,6 +263,24 @@ function checkRelationships(context: Context): void {
262
263
  }
263
264
  }
264
265
 
266
+ /** Core §9: every tip on a knowledge record is keyed by an ID the record explains (CORE003). */
267
+ function checkTips(context: Context): void {
268
+ for (const artifact of sortedArtifacts(context.graph)) {
269
+ const { meta } = artifact;
270
+ if (meta.tips === undefined) continue;
271
+ const explained = new Set(meta.links.explains ?? []);
272
+ for (const key of Object.keys(meta.tips).sort(compareStrings)) {
273
+ if (explained.has(key)) continue;
274
+ reportOn(context, artifact, {
275
+ code: "CORE003",
276
+ field: `/intentset/tips/${key}`,
277
+ message: `${meta.id} tips ${key}, which it does not explain.`,
278
+ remediation: `Add ${key} to links.explains, or key the tip by an ID the knowledge explains (Core §9).`,
279
+ });
280
+ }
281
+ }
282
+ }
283
+
265
284
  /** Core §5: cycles in `parent`, `replacedBy` and `requires`, a self-reference being the shortest. */
266
285
  function checkCycles(context: Context): void {
267
286
  const { graph } = context;
@@ -395,6 +414,18 @@ function checkLifecycle(context: Context): void {
395
414
  });
396
415
  }
397
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
+ }
398
429
  meta.slice?.usesResources.forEach((resource, i) => {
399
430
  if (registries.resources.length === 0) use("resources");
400
431
  else if (!resourceIds.has(resource)) {
@@ -446,12 +477,22 @@ interface Requirement {
446
477
  remediation: string;
447
478
  /** Core §5 names no active obligation for this type; only the draft warning applies. */
448
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[];
449
482
  }
450
483
 
451
484
  function requirement(artifact: Artifact): Requirement | null {
452
485
  const { meta } = artifact;
453
486
  const id = meta.id;
454
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
+ };
455
496
  case "rule":
456
497
  return {
457
498
  kinds: ["governedBy"],
@@ -510,18 +551,27 @@ function requirement(artifact: Artifact): Requirement | null {
510
551
  }
511
552
 
512
553
  /**
513
- * Core §5 and VSA §3: active rules, scenarios, verifications, knowledge and
514
- * product slices are attached (CORE003). Their drafts, and draft contracts
515
- * and decisions nothing refers to, are unattached drafts (CORE009, warning).
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).
516
558
  */
517
559
  function checkAttachment(context: Context): void {
518
560
  const { graph } = context;
519
561
  for (const artifact of sortedArtifacts(graph)) {
520
562
  const need = requirement(artifact);
521
563
  if (need === null || artifact.meta.status === "retired") continue;
522
- const edges = need.kinds.flatMap((kind) =>
523
- need.direction === "in" ? incomingEdges(graph, artifact.meta.id, kind) : outgoing(graph, artifact.meta.id, kind),
524
- );
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
+ });
525
575
  if (edges.length > 0) continue;
526
576
  const field = need.direction === "out" ? `/intentset/links/${need.kinds[0]}` : undefined;
527
577
  if (artifact.meta.status === "draft") {
@@ -553,7 +603,7 @@ export function patternProblem(pattern: string): string | null {
553
603
  return null;
554
604
  }
555
605
 
556
- /** VSA §3 and Core §8: type-specific blocks on their own type only; technical slices give a rationale and implement nothing; patterns are plain. */
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. */
557
607
  function checkSlices(context: Context): void {
558
608
  const { graph } = context;
559
609
  for (const artifact of sortedArtifacts(graph)) {
@@ -567,6 +617,13 @@ function checkSlices(context: Context): void {
567
617
  "Remove the verification block, or change the type (Core §8).",
568
618
  );
569
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
+ }
570
627
  const slice = artifact.meta.slice;
571
628
  if (slice === undefined) continue;
572
629
  if (artifact.meta.type !== "slice") {