@primmel/primmel 1.8.2 → 1.9.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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/src/check-rules.d.ts.map +1 -1
- package/dist/src/check-rules.js +55 -0
- package/dist/src/check-rules.js.map +1 -1
- package/dist/src/check.d.ts +74 -2
- package/dist/src/check.d.ts.map +1 -1
- package/dist/src/check.js +514 -15
- package/dist/src/check.js.map +1 -1
- package/dist/src/export/impact.d.ts +45 -0
- package/dist/src/export/impact.d.ts.map +1 -0
- package/dist/src/export/impact.js +234 -0
- package/dist/src/export/impact.js.map +1 -0
- package/dist/src/export/rdf.d.ts.map +1 -1
- package/dist/src/export/rdf.js +4 -1
- package/dist/src/export/rdf.js.map +1 -1
- package/dist/src/export/retrieval.d.ts +408 -0
- package/dist/src/export/retrieval.d.ts.map +1 -0
- package/dist/src/export/retrieval.js +1156 -0
- package/dist/src/export/retrieval.js.map +1 -0
- package/dist/src/model-diff.d.ts +23 -0
- package/dist/src/model-diff.d.ts.map +1 -1
- package/dist/src/model-diff.js +132 -30
- package/dist/src/model-diff.js.map +1 -1
- package/dist/src/ser-des/config/calculation.d.ts.map +1 -1
- package/dist/src/ser-des/config/calculation.js +119 -4
- package/dist/src/ser-des/config/calculation.js.map +1 -1
- package/dist/src/ser-des/config/dimension.d.ts +12 -0
- package/dist/src/ser-des/config/dimension.d.ts.map +1 -0
- package/dist/src/ser-des/config/dimension.js +57 -0
- package/dist/src/ser-des/config/dimension.js.map +1 -0
- package/dist/src/ser-des/config/index.d.ts.map +1 -1
- package/dist/src/ser-des/config/index.js +5 -0
- package/dist/src/ser-des/config/index.js.map +1 -1
- package/dist/src/ser-des/config/packageManifest.d.ts.map +1 -1
- package/dist/src/ser-des/config/packageManifest.js +7 -1
- package/dist/src/ser-des/config/packageManifest.js.map +1 -1
- package/dist/src/ser-des/config/quantityRegister.d.ts.map +1 -1
- package/dist/src/ser-des/config/quantityRegister.js +38 -0
- package/dist/src/ser-des/config/quantityRegister.js.map +1 -1
- package/dist/src/ser-des/config/reference.d.ts.map +1 -1
- package/dist/src/ser-des/config/reference.js +18 -0
- package/dist/src/ser-des/config/reference.js.map +1 -1
- package/dist/src/ser-des/config/requirement.d.ts.map +1 -1
- package/dist/src/ser-des/config/requirement.js +43 -0
- package/dist/src/ser-des/config/requirement.js.map +1 -1
- package/dist/src/ser-des/config/subject.d.ts +10 -1
- package/dist/src/ser-des/config/subject.d.ts.map +1 -1
- package/dist/src/ser-des/config/subject.js +96 -63
- package/dist/src/ser-des/config/subject.js.map +1 -1
- package/dist/src/ser-des/config/term.d.ts.map +1 -1
- package/dist/src/ser-des/config/term.js +82 -20
- package/dist/src/ser-des/config/term.js.map +1 -1
- package/dist/src/ser-des/parse-block.d.ts +1 -0
- package/dist/src/ser-des/parse-block.d.ts.map +1 -1
- package/dist/src/ser-des/parse-block.js +34 -0
- package/dist/src/ser-des/parse-block.js.map +1 -1
- package/dist/src/ser-des/parse.d.ts.map +1 -1
- package/dist/src/ser-des/parse.js +2 -0
- package/dist/src/ser-des/parse.js.map +1 -1
- package/dist/src/ser-des/types.d.ts +2 -1
- package/dist/src/ser-des/types.d.ts.map +1 -1
- package/dist/src/types/Calculation.d.ts +15 -0
- package/dist/src/types/Calculation.d.ts.map +1 -1
- package/dist/src/types/Package.d.ts +8 -0
- package/dist/src/types/Package.d.ts.map +1 -1
- package/dist/src/types/Quantity.d.ts +7 -0
- package/dist/src/types/Quantity.d.ts.map +1 -1
- package/dist/src/types/Reference.d.ts +11 -0
- package/dist/src/types/Reference.d.ts.map +1 -1
- package/dist/src/types/Requirement.d.ts +17 -0
- package/dist/src/types/Requirement.d.ts.map +1 -1
- package/dist/src/types/Standard.d.ts +2 -1
- package/dist/src/types/Standard.d.ts.map +1 -1
- package/dist/src/types/Subject.d.ts +10 -0
- package/dist/src/types/Subject.d.ts.map +1 -1
- package/dist/src/types/Term.d.ts +33 -1
- package/dist/src/types/Term.d.ts.map +1 -1
- package/dist-browser/index.mjs +1647 -1459
- package/dist-cli/primmel.mjs +2118 -166
- 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
|
|
436
|
-
// (C79)
|
|
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 (
|
|
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 (!
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
4357
|
-
// against the register (same-document targets) and acyclic
|
|
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
|
-
|
|
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
|