@primmel/primmel 1.8.2 → 1.9.1

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.
Files changed (86) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +20 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/check-rules.d.ts.map +1 -1
  6. package/dist/src/check-rules.js +55 -0
  7. package/dist/src/check-rules.js.map +1 -1
  8. package/dist/src/check.d.ts +74 -2
  9. package/dist/src/check.d.ts.map +1 -1
  10. package/dist/src/check.js +514 -15
  11. package/dist/src/check.js.map +1 -1
  12. package/dist/src/export/impact.d.ts +45 -0
  13. package/dist/src/export/impact.d.ts.map +1 -0
  14. package/dist/src/export/impact.js +234 -0
  15. package/dist/src/export/impact.js.map +1 -0
  16. package/dist/src/export/rdf.d.ts.map +1 -1
  17. package/dist/src/export/rdf.js +4 -1
  18. package/dist/src/export/rdf.js.map +1 -1
  19. package/dist/src/export/retrieval.d.ts +408 -0
  20. package/dist/src/export/retrieval.d.ts.map +1 -0
  21. package/dist/src/export/retrieval.js +1156 -0
  22. package/dist/src/export/retrieval.js.map +1 -0
  23. package/dist/src/model-diff.d.ts +23 -0
  24. package/dist/src/model-diff.d.ts.map +1 -1
  25. package/dist/src/model-diff.js +132 -30
  26. package/dist/src/model-diff.js.map +1 -1
  27. package/dist/src/ser-des/config/calculation.d.ts.map +1 -1
  28. package/dist/src/ser-des/config/calculation.js +119 -4
  29. package/dist/src/ser-des/config/calculation.js.map +1 -1
  30. package/dist/src/ser-des/config/dimension.d.ts +12 -0
  31. package/dist/src/ser-des/config/dimension.d.ts.map +1 -0
  32. package/dist/src/ser-des/config/dimension.js +57 -0
  33. package/dist/src/ser-des/config/dimension.js.map +1 -0
  34. package/dist/src/ser-des/config/index.d.ts.map +1 -1
  35. package/dist/src/ser-des/config/index.js +5 -0
  36. package/dist/src/ser-des/config/index.js.map +1 -1
  37. package/dist/src/ser-des/config/packageManifest.d.ts.map +1 -1
  38. package/dist/src/ser-des/config/packageManifest.js +7 -1
  39. package/dist/src/ser-des/config/packageManifest.js.map +1 -1
  40. package/dist/src/ser-des/config/quantityRegister.d.ts.map +1 -1
  41. package/dist/src/ser-des/config/quantityRegister.js +38 -0
  42. package/dist/src/ser-des/config/quantityRegister.js.map +1 -1
  43. package/dist/src/ser-des/config/reference.d.ts.map +1 -1
  44. package/dist/src/ser-des/config/reference.js +18 -0
  45. package/dist/src/ser-des/config/reference.js.map +1 -1
  46. package/dist/src/ser-des/config/requirement.d.ts.map +1 -1
  47. package/dist/src/ser-des/config/requirement.js +47 -0
  48. package/dist/src/ser-des/config/requirement.js.map +1 -1
  49. package/dist/src/ser-des/config/subject.d.ts +10 -1
  50. package/dist/src/ser-des/config/subject.d.ts.map +1 -1
  51. package/dist/src/ser-des/config/subject.js +96 -63
  52. package/dist/src/ser-des/config/subject.js.map +1 -1
  53. package/dist/src/ser-des/config/term.d.ts.map +1 -1
  54. package/dist/src/ser-des/config/term.js +82 -20
  55. package/dist/src/ser-des/config/term.js.map +1 -1
  56. package/dist/src/ser-des/config/viewProfile.d.ts.map +1 -1
  57. package/dist/src/ser-des/config/viewProfile.js +22 -5
  58. package/dist/src/ser-des/config/viewProfile.js.map +1 -1
  59. package/dist/src/ser-des/parse-block.d.ts +1 -0
  60. package/dist/src/ser-des/parse-block.d.ts.map +1 -1
  61. package/dist/src/ser-des/parse-block.js +34 -0
  62. package/dist/src/ser-des/parse-block.js.map +1 -1
  63. package/dist/src/ser-des/parse.d.ts.map +1 -1
  64. package/dist/src/ser-des/parse.js +2 -0
  65. package/dist/src/ser-des/parse.js.map +1 -1
  66. package/dist/src/ser-des/types.d.ts +2 -1
  67. package/dist/src/ser-des/types.d.ts.map +1 -1
  68. package/dist/src/types/Calculation.d.ts +15 -0
  69. package/dist/src/types/Calculation.d.ts.map +1 -1
  70. package/dist/src/types/Package.d.ts +8 -0
  71. package/dist/src/types/Package.d.ts.map +1 -1
  72. package/dist/src/types/Quantity.d.ts +7 -0
  73. package/dist/src/types/Quantity.d.ts.map +1 -1
  74. package/dist/src/types/Reference.d.ts +11 -0
  75. package/dist/src/types/Reference.d.ts.map +1 -1
  76. package/dist/src/types/Requirement.d.ts +17 -0
  77. package/dist/src/types/Requirement.d.ts.map +1 -1
  78. package/dist/src/types/Standard.d.ts +2 -1
  79. package/dist/src/types/Standard.d.ts.map +1 -1
  80. package/dist/src/types/Subject.d.ts +10 -0
  81. package/dist/src/types/Subject.d.ts.map +1 -1
  82. package/dist/src/types/Term.d.ts +33 -1
  83. package/dist/src/types/Term.d.ts.map +1 -1
  84. package/dist-browser/index.mjs +1654 -1460
  85. package/dist-cli/primmel.mjs +2143 -188
  86. package/package.json +1 -1
package/dist/src/check.js CHANGED
@@ -291,6 +291,18 @@
291
291
  // its initial state (the spec marks it required; the serializer
292
292
  // omits the line rather than emitting the dangling keyword — the
293
293
  // editor wave-03 finding, primmel/editor#19)
294
+ // C110 term-alias-shape (MN 114 v3.2 clause 13.10.1, TODO.primmel/11):
295
+ // the term alias family's shape — duplicates within one list
296
+ // (errors), the same string in two family fields of one term and
297
+ // label echoes (rollout warning legs; the cross-field leg tightens
298
+ // to the spec's error when 11d re-authors the estate's terms)
299
+ // C111 dimension-shape (MN 114 v3.2 clause 10.6, TODO.primmel/11):
300
+ // the applicability dimension namespace's declarations — unique
301
+ // value ids, implies resolving and acyclic, values vs values_of,
302
+ // one identifier once per namespace; and C3's resolution widens to
303
+ // the full namespace (clause 11.1.1: instrument dimensions +
304
+ // is_dimension attribute mirrors + the top-level declarations,
305
+ // values_of capabilities resolving from the capability register)
294
306
  //
295
307
  // Levels (TODO.roadmap/17): the DEFAULT level runs the normal-level
296
308
  // rules at their catalog severities. --audit additionally runs the
@@ -319,6 +331,10 @@ exports.checkPackage = checkPackage;
319
331
  exports.checkManifestResolution = checkManifestResolution;
320
332
  exports.checkSpellingCodes = checkSpellingCodes;
321
333
  exports.checkDeclaredPredicates = checkDeclaredPredicates;
334
+ exports.checkTermAliases = checkTermAliases;
335
+ exports.checkDimensions = checkDimensions;
336
+ exports.checkReferences = checkReferences;
337
+ exports.checkVerdictChains = checkVerdictChains;
322
338
  exports.checkEditionLifecycle = checkEditionLifecycle;
323
339
  const fs_1 = require("fs");
324
340
  const path_1 = require("path");
@@ -429,17 +445,27 @@ function checkPackage(dir, options = {}) {
429
445
  issues.push(...checkManifestResolution(dir));
430
446
  }
431
447
  }
432
- // ── C77–C80: edition lifecycle (TODO.roadmap/28, doctrine §13.4/§13.7) ──
448
+ // ── C77–C80 + C113: edition lifecycle (TODO.roadmap/28, doctrine §13.4/§13.7;
449
+ // v3.2 clause 9.3.1) ──
433
450
  // Versioning relations live on the package manifest, never in subject
434
451
  // models: status/register coherence (C77), validity windows (C78),
435
- // supersedes/replaces resolution + acyclicity across sibling manifests
436
- // (C79), and the INV-8 pin — every instance's definition_versions
452
+ // supersedes/replaces/superseded_by resolution + acyclicity across
453
+ // sibling manifests (C79) and the forward-edge coherence (C113), and the
454
+ // INV-8 pin — every instance's definition_versions
437
455
  // resolves against the package's edition register (C80).
438
456
  issues.push(...checkEditionLifecycle(dir, standard));
439
457
  // ── C89: ISO 24229 spelling codes (TODO.roadmap/25, doctrine ch. 10) ──
440
458
  issues.push(...checkSpellingCodes(standard));
441
459
  // ── C103: declared-predicate (docs/primmel/18 §18.6) ───────────────
442
460
  issues.push(...checkDeclaredPredicates(standard));
461
+ // ── C110: term-alias-shape (MN 114 v3.2, clause 13.10.1) ───────────
462
+ issues.push(...checkTermAliases(standard));
463
+ // ── C111: dimension-shape (MN 114 v3.2, clause 10.6) ───────────────
464
+ issues.push(...checkDimensions(standard));
465
+ // ── C112: reference-identity (MN 114 v3.2, clause 14.7) ────────────
466
+ issues.push(...checkReferences(standard));
467
+ // ── C116/C117: the verdict chain (MN 114 v3.2, clause 11.3) ────────
468
+ issues.push(...checkVerdictChains(standard));
443
469
  const reqIds = new Set((standard.requirements ?? []).map((r) => r.id));
444
470
  const testIds = new Set((standard.conformanceTests ?? []).map((t) => t.id));
445
471
  const attrIds = new Set((standard.attributeDefinitions ?? []).map((a) => a.id));
@@ -449,8 +475,19 @@ function checkPackage(dir, options = {}) {
449
475
  // binds_to / limit.uses may reference them (e.g. sample.test_context
450
476
  // quantities that are measured test outputs).
451
477
  const symbolIds = new Set((standard.symbols ?? []).map((s) => s.id));
478
+ // The applicability dimension namespace (MN 114 v3.2, clause 11.1.1):
479
+ // the union of the instruments' classification dimensions, the
480
+ // attribute definitions declared is_dimension true (the bound enum or
481
+ // the inline enum_values supply the domain), and the top-level
482
+ // `dimension` declarations (clause 10.6; `values_of capabilities`
483
+ // resolves its domain from the merged capability register). A null
484
+ // domain means the axis is declared but its value set is open here
485
+ // (an undocumented values_of register — C111 warns there).
452
486
  const dimIds = new Map();
453
487
  const dimCardinality = new Map();
488
+ // Attribute-mirror dimensions carry no cardinality facet — the
489
+ // match-mode warning (C3) only fires on a KNOWN single cardinality.
490
+ const dimCardinalityOpen = new Set();
454
491
  for (const inst of standard.instruments ?? []) {
455
492
  for (const d of inst.dimensions ?? []) {
456
493
  dimIds.set(d.id, new Set(d.values.map(v => v.id)));
@@ -459,6 +496,35 @@ function checkPackage(dir, options = {}) {
459
496
  }
460
497
  }
461
498
  }
499
+ for (const a of standard.attributeDefinitions ?? []) {
500
+ if (a.isDimension !== true || dimIds.has(a.id)) {
501
+ continue;
502
+ }
503
+ const enumDef = (standard.enums ?? []).find(e => e.id === a.enumRef);
504
+ const domain = enumDef
505
+ ? new Set(enumDef.values.map(v => v.id))
506
+ : a.enumValues && a.enumValues.length > 0
507
+ ? new Set(a.enumValues)
508
+ : null;
509
+ dimIds.set(a.id, domain);
510
+ dimCardinalityOpen.add(a.id);
511
+ }
512
+ for (const d of standard.dimensions ?? []) {
513
+ if (dimIds.has(d.id)) {
514
+ continue; // the duplicate declaration is C111's finding
515
+ }
516
+ if (d.valuesOf) {
517
+ dimIds.set(d.id, d.valuesOf === 'capabilities'
518
+ ? new Set((standard.capabilities ?? []).map(c => c.id))
519
+ : null);
520
+ }
521
+ else {
522
+ dimIds.set(d.id, new Set(d.values.map(v => v.id)));
523
+ }
524
+ if (d.cardinality) {
525
+ dimCardinality.set(d.id, d.cardinality);
526
+ }
527
+ }
462
528
  // C1 — bind path scope vs attribute scope
463
529
  for (const form of standard.forms ?? []) {
464
530
  const checkField = (f) => {
@@ -577,7 +643,8 @@ function checkPackage(dir, options = {}) {
577
643
  if (!dimIds.has(dim)) {
578
644
  err('C3', `conformance test ${t.id}: test_subject dimension "${dim}" not declared`);
579
645
  }
580
- else if (!dimIds.get(dim).has(String(value))) {
646
+ else if (dimIds.get(dim) !== null &&
647
+ !dimIds.get(dim).has(String(value))) {
581
648
  err('C3', `conformance test ${t.id}: test_subject ${dim}="${value}" not in the dimension's values`);
582
649
  }
583
650
  }
@@ -591,19 +658,30 @@ function checkPackage(dir, options = {}) {
591
658
  err('C3', `requirement ${r.id}: applicability dimension "${a.dimension}" not declared`);
592
659
  }
593
660
  else {
661
+ const domain = dimIds.get(a.dimension) ?? null;
594
662
  for (const v of a.values ?? []) {
595
- if (!dimIds.get(a.dimension).has(v)) {
663
+ if (domain !== null && !domain.has(v)) {
596
664
  err('C3', `requirement ${r.id}: applicability ${a.dimension}="${v}" not in the dimension's values`);
597
665
  }
598
666
  }
667
+ // The parameter-mapping form ({ A: 5, C: 3 }): the map's keys
668
+ // are listed values of the dimension (clause 11.1.1).
669
+ for (const k of Object.keys(a.mapping ?? {})) {
670
+ if (domain !== null && !domain.has(k)) {
671
+ err('C3', `requirement ${r.id}: applicability ${a.dimension} mapping key "${k}" not in the dimension's values`);
672
+ }
673
+ }
599
674
  // Universal/exact matching is the set-dimension semantics — on a
600
675
  // single-cardinality dimension the selection holds at most one
601
676
  // value, so `match all` and `match exact` both reduce to plain
602
677
  // membership (equivalent to the default any). Flag the redundant
603
678
  // mode so the data author either sets the dimension's cardinality
604
- // or drops the mode.
679
+ // or drops the mode. An attribute-mirror dimension carries no
680
+ // cardinality facet: the warning only fires on a KNOWN single
681
+ // cardinality (clause 11.1.1).
605
682
  if ((a.match === 'all' || a.match === 'exact') &&
606
- dimCardinality.get(a.dimension) !== 'set') {
683
+ dimCardinality.get(a.dimension) !== 'set' &&
684
+ !dimCardinalityOpen.has(a.dimension)) {
607
685
  warn('C3', `requirement ${r.id}: applicability ${a.dimension} match ${a.match} on a single-cardinality dimension — only meaningful on set dimensions ('all' reduces to 'any'; a multi-value 'exact' is unsatisfiable on one value)`);
608
686
  }
609
687
  }
@@ -2761,6 +2839,130 @@ function checkPackage(dir, options = {}) {
2761
2839
  err('C33', `${u.kind} ${u.id}: declared quantity_kind "${u.qk}" but unit "${u.unit}" measures kind ${uKind} — quantity kinds differ (quantity-coherence)`);
2762
2840
  }
2763
2841
  }
2842
+ // ── C114/C115: the v3.2 units-typed quantities (MN 114 v3.2, clauses
2843
+ // 11.1.2 and 13.7.1; TODO.primmel/11) ──
2844
+ // C114 — limit-quantity-coherence: a limit that declares its derived
2845
+ // quantity's typing AND binds a verdict must agree with the verdict's
2846
+ // declared quantity, so a limit and its acceptance chain can never
2847
+ // drift apart in units. Silent when either side leaves the facet
2848
+ // undeclared (the block is the migration path); an unresolvable
2849
+ // accepts.verdict is the resolution rule's leg, not this one's.
2850
+ for (const r of standard.requirements ?? []) {
2851
+ const q = r.limit?.quantity;
2852
+ const accepts = r.limit?.accepts;
2853
+ if (!q || !accepts) {
2854
+ continue;
2855
+ }
2856
+ const verdict = (standard.verdicts ?? []).find(v => v.id === accepts.verdict);
2857
+ if (!verdict) {
2858
+ continue;
2859
+ }
2860
+ if (q.kind && verdict.quantityKind && q.kind !== verdict.quantityKind) {
2861
+ err('C114', `requirement ${r.id}: limit declares quantity kind "${q.kind}" but accepts verdict ${verdict.id} derives kind "${verdict.quantityKind}" — a limit and its acceptance chain never drift apart in units (limit-quantity-coherence)`);
2862
+ }
2863
+ if (q.unit && verdict.unit && q.unit !== verdict.unit) {
2864
+ err('C114', `requirement ${r.id}: limit declares quantity unit "${q.unit}" but accepts verdict ${verdict.id} derives unit "${verdict.unit}" (limit-quantity-coherence)`);
2865
+ }
2866
+ }
2867
+ for (const c of standard.calculations ?? []) {
2868
+ const entries = [
2869
+ ...(c.inputs ?? []).map(inp => ({
2870
+ owner: `calculation ${c.id}: input ${inp.name}`,
2871
+ type: inp.type,
2872
+ unit: inp.unit,
2873
+ isInput: true,
2874
+ quantityKind: inp.quantityKind,
2875
+ rangeMin: inp.rangeMin,
2876
+ rangeMax: inp.rangeMax,
2877
+ hasRange: inp.hasRange,
2878
+ enumValues: inp.enumValues,
2879
+ })),
2880
+ {
2881
+ owner: `calculation ${c.id}: output`,
2882
+ type: c.output.type,
2883
+ unit: c.output.unit,
2884
+ isInput: false,
2885
+ quantityKind: c.output.quantityKind,
2886
+ rangeMin: c.output.rangeMin,
2887
+ rangeMax: c.output.rangeMax,
2888
+ hasRange: c.output.hasRange,
2889
+ },
2890
+ ];
2891
+ for (const e of entries) {
2892
+ const declaredUnit = e.unit && e.unit !== '1' ? e.unit : '';
2893
+ const unitKind = declaredUnit ? kindOfUnit(declaredUnit) : undefined;
2894
+ if (declaredUnit && registersExist && unitKind === undefined) {
2895
+ // Rollout warning leg (see the rule's catalog entry): the spec's
2896
+ // error tightens when the estate's registers catch up.
2897
+ warn('C115', `${e.owner}: unit "${declaredUnit}" resolves to no registered unit of the merged quantity register (calculation-signature-shape)`);
2898
+ }
2899
+ if (e.quantityKind) {
2900
+ if (!kindDecl.has(e.quantityKind)) {
2901
+ err('C115', `${e.owner}: quantity_kind "${e.quantityKind}" resolves to no registered kind of the merged quantity register (calculation-signature-shape)`);
2902
+ }
2903
+ else if (unitKind !== undefined && unitKind !== e.quantityKind) {
2904
+ err('C115', `${e.owner}: declared quantity_kind "${e.quantityKind}" but unit "${declaredUnit}" measures kind ${unitKind} — the typed-unit pair must agree in kind (calculation-signature-shape)`);
2905
+ }
2906
+ }
2907
+ if (e.hasRange && e.rangeMin && e.rangeMax) {
2908
+ const lo = Number(e.rangeMin);
2909
+ const hi = Number(e.rangeMax);
2910
+ if (!Number.isNaN(lo) && !Number.isNaN(hi) && lo > hi) {
2911
+ err('C115', `${e.owner}: range min ${e.rangeMin} exceeds max ${e.rangeMax} (calculation-signature-shape)`);
2912
+ }
2913
+ }
2914
+ if (e.isInput &&
2915
+ e.type === 'enum' &&
2916
+ (e.enumValues === undefined || e.enumValues.length === 0)) {
2917
+ err('C115', `${e.owner}: an enum-typed input declares its values — enum_values is absent or empty (calculation-signature-shape)`);
2918
+ }
2919
+ }
2920
+ }
2921
+ // ── C118: requirement-parameter-shape (MN 114 v3.2, clause 11.1.3) ──
2922
+ // The instance-parameter schema is the binding contract an execution
2923
+ // environment reads: a parameter's bind path resolves against the
2924
+ // subject's aspect catalog (the binds_to path discipline of C2), its
2925
+ // unit resolves against the merged quantity register (the rollout
2926
+ // WARNING leg — the C33 §6.8 precedent, shared with C115), and a
2927
+ // range's min never exceeds its max.
2928
+ for (const r of standard.requirements ?? []) {
2929
+ for (const p of r.parameters ?? []) {
2930
+ const where = `requirement ${r.id}: param ${p.name}`;
2931
+ if (p.bind) {
2932
+ const parts = String(p.bind).split('.');
2933
+ const id = parts[2];
2934
+ if (id) {
2935
+ if (parts[1] === 'classification') {
2936
+ const dim = DIM_ALIASES[id] ?? id;
2937
+ if (!dimIds.has(dim) && !attrIds.has(id) && !attrIds.has(dim)) {
2938
+ err('C118', `${where}: bind "${p.bind}" — dimension "${id}" not declared (requirement-parameter-shape)`);
2939
+ }
2940
+ }
2941
+ else if (parts[1] === 'behaviors') {
2942
+ if (!behaviorIds.has(id)) {
2943
+ err('C118', `${where}: bind "${p.bind}" — behavior "${id}" not declared (requirement-parameter-shape)`);
2944
+ }
2945
+ }
2946
+ else if (!isIdentityPath(String(p.bind)) &&
2947
+ !attrIds.has(id) &&
2948
+ !symbolIds.has(id)) {
2949
+ err('C118', `${where}: bind "${p.bind}" — attribute "${id}" not defined (requirement-parameter-shape)`);
2950
+ }
2951
+ }
2952
+ }
2953
+ if (p.unit && registersExist && kindOfUnit(p.unit) === undefined) {
2954
+ // Rollout warning leg (see the rule's catalog entry).
2955
+ warn('C118', `${where}: unit "${p.unit}" resolves to no registered unit of the merged quantity register (requirement-parameter-shape)`);
2956
+ }
2957
+ if (p.hasRange && p.rangeMin && p.rangeMax) {
2958
+ const lo = Number(p.rangeMin);
2959
+ const hi = Number(p.rangeMax);
2960
+ if (!Number.isNaN(lo) && !Number.isNaN(hi) && lo > hi) {
2961
+ err('C118', `${where}: range min ${p.rangeMin} exceeds max ${p.rangeMax} (requirement-parameter-shape)`);
2962
+ }
2963
+ }
2964
+ }
2965
+ }
2764
2966
  // C34 — duality-coherence: one value structure, two roles.
2765
2967
  for (const d of standard.duals ?? []) {
2766
2968
  if (d.attribute && !attrIds.has(d.attribute)) {
@@ -4272,7 +4474,266 @@ function checkDeclaredPredicates(standard) {
4272
4474
  return issues;
4273
4475
  }
4274
4476
  /**
4275
- * C77–C80 + C85 — edition lifecycle (TODO.roadmap/28; doctrine ch. 13
4477
+ * C110 — term-alias-shape (MN 114 v3.2, clause 13.10.1;
4478
+ * TODO.primmel/11): the term alias family is the everyday-words →
4479
+ * defined-term bridge, so its shape is checked. Duplicate entries within
4480
+ * one list of one term are errors; an entry that merely echoes the
4481
+ * term's label is a warning (it adds no bridge). The same string in two
4482
+ * different family fields of one term — the spec's "one entry has one
4483
+ * home" error — ships as a WARNING leg during the v3.2 rollout: v2
4484
+ * content authored `alt` under the old admitted-forms semantics, where
4485
+ * an entry doubling as the term's abbreviation was legal, and the
4486
+ * shipped corpus carries exactly that debt (`alt { LC }` +
4487
+ * `abbreviations { LC }` on R 60's load-cell and mpe terms); the leg
4488
+ * tightens to the spec's error once the consumer wave re-authors the
4489
+ * family (TODO.primmel/11d). Each spelling-tagged variant list is its
4490
+ * own scope: the same string in two spellings is the variant mechanism
4491
+ * working, never a duplicate.
4492
+ */
4493
+ function checkTermAliases(standard) {
4494
+ const issues = [];
4495
+ const err = (message) => issues.push({ check: 'C110', severity: 'error', message });
4496
+ const warn = (message) => issues.push({ check: 'C110', severity: 'warning', message });
4497
+ const FACETS = ['aliases', 'colloquial', 'abbreviations', 'deprecated'];
4498
+ for (const t of standard.terms ?? []) {
4499
+ // The family scopes of the term: the default-spelling lists plus one
4500
+ // scope per spelling-tagged variant.
4501
+ const scopes = [
4502
+ [
4503
+ '',
4504
+ {
4505
+ aliases: t.aliases ?? t.alt,
4506
+ colloquial: t.colloquial,
4507
+ abbreviations: t.abbreviations,
4508
+ deprecated: t.deprecated,
4509
+ },
4510
+ ],
4511
+ ...Object.entries(t.aliasSpellings ?? {}).map(([code, v]) => [
4512
+ ` ${code}`,
4513
+ { ...v },
4514
+ ]),
4515
+ ];
4516
+ for (const [tag, scope] of scopes) {
4517
+ const where = (facet) => `term ${t.id}: ${facet}${tag ? ` ${tag}` : ''}`;
4518
+ for (const facet of FACETS) {
4519
+ const seen = new Set();
4520
+ for (const entry of scope[facet] ?? []) {
4521
+ if (seen.has(entry)) {
4522
+ err(`${where(facet)}: duplicate entry "${entry}" — one entry has one home (term-alias-shape)`);
4523
+ }
4524
+ seen.add(entry);
4525
+ }
4526
+ }
4527
+ const home = new Map();
4528
+ for (const facet of FACETS) {
4529
+ for (const entry of scope[facet] ?? []) {
4530
+ const prior = home.get(entry);
4531
+ if (prior !== undefined && prior !== facet) {
4532
+ // Rollout warning leg (see the rule's doc comment): the
4533
+ // spec's error tightens when the estate re-authors.
4534
+ warn(`${where(facet)}: "${entry}" already appears in ${prior} — one entry has one home (term-alias-shape)`);
4535
+ }
4536
+ else {
4537
+ home.set(entry, facet);
4538
+ }
4539
+ }
4540
+ }
4541
+ }
4542
+ // The label echo: any entry that restates the term's own label
4543
+ // bridges nothing (checked across every scope).
4544
+ if (t.label) {
4545
+ for (const [tag, scope] of scopes) {
4546
+ for (const facet of FACETS) {
4547
+ if ((scope[facet] ?? []).includes(t.label)) {
4548
+ warn(`term ${t.id}: ${facet}${tag ? ` ${tag}` : ''} entry "${t.label}" merely echoes the term's label — an alias adds a bridge or it does not belong (term-alias-shape)`);
4549
+ }
4550
+ }
4551
+ }
4552
+ }
4553
+ }
4554
+ return issues;
4555
+ }
4556
+ /**
4557
+ * C111 — dimension-shape (MN 114 v3.2, clause 10.6; TODO.primmel/11):
4558
+ * the applicability dimension namespace is typed and shared, so its
4559
+ * declarations are checked. Per dimension (the instruments' inline
4560
+ * blocks AND the top-level declarations — one grammar, two placements):
4561
+ * value identifiers are unique, every `implies` target resolves inside
4562
+ * its own dimension, and the implication graph is acyclic (the
4563
+ * applicability engine walks the closure). A top-level dimension carries
4564
+ * `values` or `values_of`, never both; a `values_of` naming a register
4565
+ * the kernel does not know (the documented register is `capabilities`)
4566
+ * warns — the domain stays open, C3's value resolution skips it.
4567
+ * Namespace uniqueness: a top-level declaration colliding with an
4568
+ * instrument's inline dimension or an is_dimension attribute, or two
4569
+ * instruments declaring the same dimension id in the merged package, is
4570
+ * an error. The one tolerated overlap is the estate's mirror pattern: an
4571
+ * attribute definition declared `is_dimension true` and an instrument's
4572
+ * inline dimension carrying the SAME id are one axis declared at two
4573
+ * layers (the attribute carries the typed aspect and the enum domain,
4574
+ * the inline block the presentation) — R 144's power_supply axis is the
4575
+ * shipped precedent.
4576
+ */
4577
+ function checkDimensions(standard) {
4578
+ const issues = [];
4579
+ const err = (message) => issues.push({ check: 'C111', severity: 'error', message });
4580
+ const warn = (message) => issues.push({ check: 'C111', severity: 'warning', message });
4581
+ const checkShape = (owner, d) => {
4582
+ const seen = new Set();
4583
+ for (const v of d.values) {
4584
+ if (seen.has(v.id)) {
4585
+ err(`${owner} dimension ${d.id}: duplicate value id "${v.id}" (dimension-shape)`);
4586
+ }
4587
+ seen.add(v.id);
4588
+ }
4589
+ const valueIds = new Set(d.values.map(v => v.id));
4590
+ const adj = new Map();
4591
+ for (const v of d.values) {
4592
+ for (const target of v.implies ?? []) {
4593
+ if (!valueIds.has(target)) {
4594
+ err(`${owner} dimension ${d.id}: value ${v.id} implies "${target}", which is not a value of this dimension — implies targets resolve inside their own dimension (dimension-shape)`);
4595
+ }
4596
+ }
4597
+ adj.set(v.id, (v.implies ?? []).filter(t => valueIds.has(t)));
4598
+ }
4599
+ const cycle = findCycle(adj);
4600
+ if (cycle) {
4601
+ err(`${owner} dimension ${d.id}: implies cycle ${cycle.join(' → ')} — the applicability engine walks the implication closure, so the graph must be acyclic (dimension-shape)`);
4602
+ }
4603
+ if (d.valuesOf && d.values.length > 0) {
4604
+ err(`${owner} dimension ${d.id}: declares both values and values_of — a dimension carries one value domain (dimension-shape)`);
4605
+ }
4606
+ if (d.valuesOf && d.valuesOf !== 'capabilities') {
4607
+ warn(`${owner} dimension ${d.id}: values_of "${d.valuesOf}" names a register the kernel does not document (the documented register is capabilities) — the value domain stays open (dimension-shape)`);
4608
+ }
4609
+ };
4610
+ for (const inst of standard.instruments ?? []) {
4611
+ for (const d of inst.dimensions ?? []) {
4612
+ checkShape(`instrument ${inst.id}:`, d);
4613
+ }
4614
+ }
4615
+ for (const d of standard.dimensions ?? []) {
4616
+ checkShape('top-level', d);
4617
+ }
4618
+ // Namespace uniqueness. The inline-vs-attribute mirror is tolerated
4619
+ // (see the rule's doc comment); every other double declaration is an
4620
+ // error.
4621
+ const inlineHome = new Map(); // dim id → instrument id
4622
+ for (const inst of standard.instruments ?? []) {
4623
+ for (const d of inst.dimensions ?? []) {
4624
+ const prior = inlineHome.get(d.id);
4625
+ if (prior !== undefined && prior !== inst.id) {
4626
+ err(`dimension ${d.id}: declared inline on both instrument ${prior} and instrument ${inst.id} — one dimension identifier once per applicability namespace (dimension-shape)`);
4627
+ }
4628
+ else {
4629
+ inlineHome.set(d.id, inst.id);
4630
+ }
4631
+ }
4632
+ }
4633
+ const attrDims = new Set((standard.attributeDefinitions ?? [])
4634
+ .filter(a => a.isDimension === true)
4635
+ .map(a => a.id));
4636
+ for (const d of standard.dimensions ?? []) {
4637
+ if (inlineHome.has(d.id)) {
4638
+ err(`dimension ${d.id}: declared both top-level and inline on instrument ${inlineHome.get(d.id)} — one dimension identifier once per applicability namespace (dimension-shape)`);
4639
+ }
4640
+ if (attrDims.has(d.id)) {
4641
+ err(`dimension ${d.id}: declared both top-level and as the is_dimension attribute definition of the same id — one dimension identifier once per applicability namespace (dimension-shape)`);
4642
+ }
4643
+ }
4644
+ return issues;
4645
+ }
4646
+ /**
4647
+ * C112 — reference-identity (MN 114 v3.2, clause 14.7; TODO.primmel/11):
4648
+ * the reference construct is the registry of named citation targets, and
4649
+ * v3.2 grows its facet set from display strings to a resolvable identity.
4650
+ * A malformed `urn` is an error — the same well-formedness class as C85's
4651
+ * baseUrn check (a scheme, then no whitespace or IRI delimiters). When
4652
+ * `urn` is present it IS the document's identity; when it is absent the
4653
+ * org/document/edition/clause quadruple is what a consumer's locator
4654
+ * resolves, so a reference carrying neither `urn` nor the
4655
+ * org-and-document pair is a warning — a display-string citation is
4656
+ * visible debt, never silently citable.
4657
+ */
4658
+ function checkReferences(standard) {
4659
+ const issues = [];
4660
+ for (const r of standard.references ?? []) {
4661
+ if (r.urn !== undefined) {
4662
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"{}|^`\\]*$/.test(r.urn)) {
4663
+ issues.push({
4664
+ check: 'C112',
4665
+ severity: 'error',
4666
+ message: `reference ${r.id}: urn "${r.urn}" is not a well-formed IRI (a scheme followed by no whitespace or IRI delimiters) (reference-identity)`,
4667
+ });
4668
+ }
4669
+ }
4670
+ else if (!(r.org && r.document)) {
4671
+ issues.push({
4672
+ check: 'C112',
4673
+ severity: 'warning',
4674
+ message: `reference ${r.id}: carries neither urn nor the org-and-document pair — a display-string citation is not machine-traversable (reference-identity)`,
4675
+ });
4676
+ }
4677
+ }
4678
+ return issues;
4679
+ }
4680
+ /**
4681
+ * C116/C117 — the verdict chain (MN 114 v3.2, clause 11.3;
4682
+ * TODO.primmel/11): a verdict's `inputs` name the free identifiers of
4683
+ * its derivation, and an input may name another VERDICT — the
4684
+ * acceptance chain is then an explicit graph: requirement →
4685
+ * limit.accepts.verdict → the verdict's inputs (symbols and
4686
+ * sub-verdicts) → each verdict's acceptance.
4687
+ * C116 verdict-inputs-resolve: every input resolves to a declared
4688
+ * symbol, a test variable or observable, or another verdict.
4689
+ * C117 verdict-chain-acyclic: the verdict→verdict graph never cycles
4690
+ * (a consumption system traverses the chain; a cycle never
4691
+ * terminates).
4692
+ */
4693
+ function checkVerdictChains(standard) {
4694
+ const issues = [];
4695
+ const symbolIds = new Set((standard.symbols ?? []).map(s => s.id));
4696
+ const testNames = new Set();
4697
+ for (const t of standard.conformanceTests ?? []) {
4698
+ for (const v of t.variables ?? []) {
4699
+ testNames.add(v.name);
4700
+ }
4701
+ for (const o of t.observables ?? []) {
4702
+ testNames.add(o.name);
4703
+ }
4704
+ }
4705
+ const verdictIds = new Set((standard.verdicts ?? []).map(v => v.id));
4706
+ // C116 — every input resolves.
4707
+ for (const v of standard.verdicts ?? []) {
4708
+ for (const inp of v.inputs ?? []) {
4709
+ if (!symbolIds.has(inp) && !testNames.has(inp) && !verdictIds.has(inp)) {
4710
+ issues.push({
4711
+ check: 'C116',
4712
+ severity: 'error',
4713
+ message: `verdict ${v.id}: input "${inp}" resolves to no declared symbol, test variable or observable, or verdict (verdict-inputs-resolve)`,
4714
+ });
4715
+ }
4716
+ }
4717
+ }
4718
+ // C117 — the verdict→verdict subgraph never cycles. Self-derivation
4719
+ // stays in the adjacency: a verdict deriving from itself is the
4720
+ // length-1 cycle this rule exists for.
4721
+ const adj = new Map();
4722
+ for (const v of standard.verdicts ?? []) {
4723
+ adj.set(v.id, (v.inputs ?? []).filter(i => verdictIds.has(i)));
4724
+ }
4725
+ const cycle = findCycle(adj);
4726
+ if (cycle) {
4727
+ issues.push({
4728
+ check: 'C117',
4729
+ severity: 'error',
4730
+ message: `verdict chain cycle: ${cycle.join(' → ')} — the acceptance chain is a graph a consumer traverses, so it must be acyclic (verdict-chain-acyclic)`,
4731
+ });
4732
+ }
4733
+ return issues;
4734
+ }
4735
+ /**
4736
+ * C77–C80 + C85 + C113 — edition lifecycle (TODO.roadmap/28; doctrine ch. 13
4276
4737
  * §13.4/§13.7) and the manifest base URN (TODO.roadmap/27). Editions are
4277
4738
  * packagings: the relations live on the manifest, and the checks run
4278
4739
  * WITHOUT composing content (the checkManifestResolution sibling
@@ -4284,7 +4745,8 @@ function checkDeclaredPredicates(standard) {
4284
4745
  * parser, like `kind` — an unknown token is a parse error.)
4285
4746
  * C78 edition-validity-window: the window is well-formed ISO 8601 and
4286
4747
  * `to` is not before `from`.
4287
- * C79 edition-supersedes-resolves: supersedes/replaces targets are
4748
+ * C79 edition-supersedes-resolves: supersedes/replaces/superseded_by
4749
+ * targets are
4288
4750
  * well-formed URNs, never the package itself; a target naming an
4289
4751
  * earlier edition OF THE SAME document resolves against the edition
4290
4752
  * register (warning when the register omits it); the supersedes
@@ -4298,6 +4760,10 @@ function checkDeclaredPredicates(standard) {
4298
4760
  * grounds every downstream IRI (the RDF projection, provenance
4299
4761
  * comparisons), and the free-string field was the one manifest
4300
4762
  * IRI surface nothing validated.
4763
+ * C113 edition-lineage-coherent (v3.2, clause 9.3.1): the forward
4764
+ * `superseded_by` edge, where declared, must agree with the
4765
+ * successor's backward edges across the composed closure; a missing
4766
+ * forward edge is never an error.
4301
4767
  */
4302
4768
  function checkEditionLifecycle(dir, standard) {
4303
4769
  const issues = [];
@@ -4353,12 +4819,15 @@ function checkEditionLifecycle(dir, standard) {
4353
4819
  !/^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"{}|^`\\]*$/.test(m.baseUrn)) {
4354
4820
  err('C85', `package "${m.id}": baseUrn "${m.baseUrn}" is not a well-formed IRI (a scheme followed by no whitespace or IRI delimiters) (baseurn-wellformed)`);
4355
4821
  }
4356
- // C79 — supersedes/replaces: well-formed URNs, never self, resolving
4357
- // against the register (same-document targets) and acyclic across the
4358
- // repo's manifests (§13.7).
4822
+ // C79 — supersedes/replaces/superseded_by: well-formed URNs, never self,
4823
+ // resolving against the register (same-document targets) and acyclic
4824
+ // across the repo's manifests (§13.7). The v3.2 forward edge
4825
+ // (superseded_by, clause 9.3.1) takes the same shape checks; coherence of
4826
+ // the two directions is C113 below.
4359
4827
  const relations = [
4360
4828
  ...(m.supersedes ?? []).map(t => ['supersedes', t]),
4361
4829
  ...(m.replaces ?? []).map(t => ['replaces', t]),
4830
+ ...(m.supersededBy ?? []).map(t => ['superseded_by', t]),
4362
4831
  ];
4363
4832
  const ownBasis = (0, model_diff_1.normalizeSourceRef)(m.baseUrn ?? '', '').basis;
4364
4833
  for (const [rel, target] of relations) {
@@ -4371,7 +4840,7 @@ function checkEditionLifecycle(dir, standard) {
4371
4840
  continue;
4372
4841
  }
4373
4842
  const tNorm = (0, model_diff_1.normalizeSourceRef)(target, '');
4374
- if (tNorm.basis === ownBasis && tNorm.edition) {
4843
+ if (rel !== 'superseded_by' && tNorm.basis === ownBasis && tNorm.edition) {
4375
4844
  if (!register.has(tNorm.edition)) {
4376
4845
  warn('C79', `package "${m.id}": ${rel} ${target}, but the edition register { ${[...register].join(' ')} } does not list ${tNorm.edition} (edition-supersedes-resolves)`);
4377
4846
  }
@@ -4381,8 +4850,12 @@ function checkEditionLifecycle(dir, standard) {
4381
4850
  // The supersedes graph over the repo's manifests (sibling dirs keyed
4382
4851
  // by baseUrn) must be acyclic — no superseding oneself through a
4383
4852
  // chain. Edges to URNs no sibling declares are external and cannot
4384
- // close a cycle here.
4385
- const nodes = new Map(); // baseUrn → relations
4853
+ // close a cycle here. The walk also captures each node's forward edge
4854
+ // (superseded_by) for C113; the cycle detector deliberately reads the
4855
+ // backward edges ONLY — a matched supersedes/superseded_by pair is
4856
+ // one lineage fact declared at both ends, never a 2-cycle.
4857
+ const nodes = new Map(); // baseUrn → backward relations
4858
+ const forwardByNode = new Map(); // baseUrn → superseded_by targets
4386
4859
  const parent = (0, path_1.dirname)((0, path_1.resolve)(dir));
4387
4860
  for (const entry of (0, fs_1.readdirSync)(parent).sort()) {
4388
4861
  const full = (0, path_1.join)(parent, entry);
@@ -4396,6 +4869,7 @@ function checkEditionLifecycle(dir, standard) {
4396
4869
  ...(sib.supersedes ?? []).map(t => ['supersedes', t]),
4397
4870
  ...(sib.replaces ?? []).map(t => ['replaces', t]),
4398
4871
  ]);
4872
+ forwardByNode.set(key, new Set(sib.supersededBy ?? []));
4399
4873
  }
4400
4874
  catch {
4401
4875
  // Not a package dir — not a graph node.
@@ -4413,6 +4887,31 @@ function checkEditionLifecycle(dir, standard) {
4413
4887
  if (cycle) {
4414
4888
  err('C79', `supersedes cycle: ${cycle.join(' → ')} — the supersedes graph must be acyclic; a package cannot supersede itself through a chain (edition-supersedes-resolves)`);
4415
4889
  }
4890
+ // C113 — edition-lineage-coherent (MN 114 v3.2, clause 9.3.1): the
4891
+ // lineage graph is the union of the declared edges of both directions;
4892
+ // when two composed packages declare opposite edges between the same
4893
+ // pair, the declarations must agree. A MISSING forward edge is never
4894
+ // an error — the backward edges remain the authored minimum — so each
4895
+ // leg fires only when the opposite edge set is declared and omits this
4896
+ // package. Edges to URNs no sibling declares are external: nothing to
4897
+ // cohere with here.
4898
+ const ownKey = m.baseUrn || m.id;
4899
+ for (const target of [...(m.supersedes ?? []), ...(m.replaces ?? [])]) {
4900
+ const fwd = forwardByNode.get(target);
4901
+ if (fwd !== undefined && fwd.size > 0 && !fwd.has(ownKey)) {
4902
+ err('C113', `package "${m.id}": supersedes/replaces ${target}, but that package declares superseded_by { ${[...fwd].join(' ')} } without ${ownKey} — opposite lineage edges between the same pair must agree (edition-lineage-coherent)`);
4903
+ }
4904
+ }
4905
+ for (const target of m.supersededBy ?? []) {
4906
+ const bwdRels = nodes.get(target);
4907
+ if (bwdRels === undefined) {
4908
+ continue;
4909
+ }
4910
+ const bwd = new Set(bwdRels.map(([, t]) => t));
4911
+ if (bwd.size > 0 && !bwd.has(ownKey)) {
4912
+ err('C113', `package "${m.id}": superseded_by ${target}, but that package declares supersedes/replaces { ${[...bwd].join(' ')} } without ${ownKey} — opposite lineage edges between the same pair must agree (edition-lineage-coherent)`);
4913
+ }
4914
+ }
4416
4915
  }
4417
4916
  // C80 — INV-8: definition version pins resolve against the edition
4418
4917
  // register. Silent on a register-less package (nothing to resolve