@primmel/primmel 1.13.0 β†’ 1.14.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.
Files changed (128) hide show
  1. package/dist/src/check-rules.d.ts.map +1 -1
  2. package/dist/src/check-rules.js +73 -0
  3. package/dist/src/check-rules.js.map +1 -1
  4. package/dist/src/check.d.ts.map +1 -1
  5. package/dist/src/check.js +693 -0
  6. package/dist/src/check.js.map +1 -1
  7. package/dist/src/model-diff.d.ts.map +1 -1
  8. package/dist/src/model-diff.js +35 -0
  9. package/dist/src/model-diff.js.map +1 -1
  10. package/dist/src/ser-des/config/applicationDeclaration.d.ts +5 -0
  11. package/dist/src/ser-des/config/applicationDeclaration.d.ts.map +1 -0
  12. package/dist/src/ser-des/config/applicationDeclaration.js +102 -0
  13. package/dist/src/ser-des/config/applicationDeclaration.js.map +1 -0
  14. package/dist/src/ser-des/config/aspect.d.ts +5 -0
  15. package/dist/src/ser-des/config/aspect.d.ts.map +1 -0
  16. package/dist/src/ser-des/config/aspect.js +128 -0
  17. package/dist/src/ser-des/config/aspect.js.map +1 -0
  18. package/dist/src/ser-des/config/calculationContext.d.ts +5 -0
  19. package/dist/src/ser-des/config/calculationContext.d.ts.map +1 -0
  20. package/dist/src/ser-des/config/calculationContext.js +62 -0
  21. package/dist/src/ser-des/config/calculationContext.js.map +1 -0
  22. package/dist/src/ser-des/config/certificateTemplate.d.ts +5 -0
  23. package/dist/src/ser-des/config/certificateTemplate.d.ts.map +1 -0
  24. package/dist/src/ser-des/config/certificateTemplate.js +195 -0
  25. package/dist/src/ser-des/config/certificateTemplate.js.map +1 -0
  26. package/dist/src/ser-des/config/evaluationDimension.d.ts +5 -0
  27. package/dist/src/ser-des/config/evaluationDimension.d.ts.map +1 -0
  28. package/dist/src/ser-des/config/evaluationDimension.js +128 -0
  29. package/dist/src/ser-des/config/evaluationDimension.js.map +1 -0
  30. package/dist/src/ser-des/config/evaluationProfile.d.ts +5 -0
  31. package/dist/src/ser-des/config/evaluationProfile.d.ts.map +1 -0
  32. package/dist/src/ser-des/config/evaluationProfile.js +61 -0
  33. package/dist/src/ser-des/config/evaluationProfile.js.map +1 -0
  34. package/dist/src/ser-des/config/identitySlot.d.ts +5 -0
  35. package/dist/src/ser-des/config/identitySlot.d.ts.map +1 -0
  36. package/dist/src/ser-des/config/identitySlot.js +120 -0
  37. package/dist/src/ser-des/config/identitySlot.js.map +1 -0
  38. package/dist/src/ser-des/config/index.d.ts.map +1 -1
  39. package/dist/src/ser-des/config/index.js +148 -0
  40. package/dist/src/ser-des/config/index.js.map +1 -1
  41. package/dist/src/ser-des/config/promiseSet.d.ts +5 -0
  42. package/dist/src/ser-des/config/promiseSet.d.ts.map +1 -0
  43. package/dist/src/ser-des/config/promiseSet.js +46 -0
  44. package/dist/src/ser-des/config/promiseSet.js.map +1 -0
  45. package/dist/src/ser-des/config/selectionRules.d.ts +9 -0
  46. package/dist/src/ser-des/config/selectionRules.d.ts.map +1 -0
  47. package/dist/src/ser-des/config/selectionRules.js +372 -0
  48. package/dist/src/ser-des/config/selectionRules.js.map +1 -0
  49. package/dist/src/ser-des/config/subject.d.ts +18 -1
  50. package/dist/src/ser-des/config/subject.d.ts.map +1 -1
  51. package/dist/src/ser-des/config/subject.js +177 -61
  52. package/dist/src/ser-des/config/subject.js.map +1 -1
  53. package/dist/src/ser-des/config/testReport.d.ts +7 -0
  54. package/dist/src/ser-des/config/testReport.d.ts.map +1 -0
  55. package/dist/src/ser-des/config/testReport.js +322 -0
  56. package/dist/src/ser-des/config/testReport.js.map +1 -0
  57. package/dist/src/ser-des/config/verificationPathway.d.ts +5 -0
  58. package/dist/src/ser-des/config/verificationPathway.d.ts.map +1 -0
  59. package/dist/src/ser-des/config/verificationPathway.js +361 -0
  60. package/dist/src/ser-des/config/verificationPathway.js.map +1 -0
  61. package/dist/src/ser-des/config/workflowConfig.d.ts +5 -0
  62. package/dist/src/ser-des/config/workflowConfig.d.ts.map +1 -0
  63. package/dist/src/ser-des/config/workflowConfig.js +129 -0
  64. package/dist/src/ser-des/config/workflowConfig.js.map +1 -0
  65. package/dist/src/ser-des/package.d.ts +10 -0
  66. package/dist/src/ser-des/package.d.ts.map +1 -1
  67. package/dist/src/ser-des/package.js +161 -12
  68. package/dist/src/ser-des/package.js.map +1 -1
  69. package/dist/src/ser-des/parse.d.ts.map +1 -1
  70. package/dist/src/ser-des/parse.js +24 -0
  71. package/dist/src/ser-des/parse.js.map +1 -1
  72. package/dist/src/ser-des/types.d.ts +27 -0
  73. package/dist/src/ser-des/types.d.ts.map +1 -1
  74. package/dist/src/types/ApplicationDeclaration.d.ts +23 -0
  75. package/dist/src/types/ApplicationDeclaration.d.ts.map +1 -0
  76. package/dist/src/types/ApplicationDeclaration.js +32 -0
  77. package/dist/src/types/ApplicationDeclaration.js.map +1 -0
  78. package/dist/src/types/Aspect.d.ts +30 -0
  79. package/dist/src/types/Aspect.d.ts.map +1 -0
  80. package/dist/src/types/Aspect.js +41 -0
  81. package/dist/src/types/Aspect.js.map +1 -0
  82. package/dist/src/types/CalculationContext.d.ts +17 -0
  83. package/dist/src/types/CalculationContext.d.ts.map +1 -0
  84. package/dist/src/types/CalculationContext.js +34 -0
  85. package/dist/src/types/CalculationContext.js.map +1 -0
  86. package/dist/src/types/CertificateTemplate.d.ts +46 -0
  87. package/dist/src/types/CertificateTemplate.d.ts.map +1 -0
  88. package/dist/src/types/CertificateTemplate.js +46 -0
  89. package/dist/src/types/CertificateTemplate.js.map +1 -0
  90. package/dist/src/types/EvaluationDimension.d.ts +32 -0
  91. package/dist/src/types/EvaluationDimension.d.ts.map +1 -0
  92. package/dist/src/types/EvaluationDimension.js +38 -0
  93. package/dist/src/types/EvaluationDimension.js.map +1 -0
  94. package/dist/src/types/EvaluationProfile.d.ts +10 -0
  95. package/dist/src/types/EvaluationProfile.d.ts.map +1 -0
  96. package/dist/src/types/EvaluationProfile.js +25 -0
  97. package/dist/src/types/EvaluationProfile.js.map +1 -0
  98. package/dist/src/types/IdentitySlot.d.ts +24 -0
  99. package/dist/src/types/IdentitySlot.d.ts.map +1 -0
  100. package/dist/src/types/IdentitySlot.js +34 -0
  101. package/dist/src/types/IdentitySlot.js.map +1 -0
  102. package/dist/src/types/PromiseSet.d.ts +8 -0
  103. package/dist/src/types/PromiseSet.d.ts.map +1 -0
  104. package/dist/src/types/PromiseSet.js +35 -0
  105. package/dist/src/types/PromiseSet.js.map +1 -0
  106. package/dist/src/types/SelectionRules.d.ts +73 -0
  107. package/dist/src/types/SelectionRules.d.ts.map +1 -0
  108. package/dist/src/types/SelectionRules.js +55 -0
  109. package/dist/src/types/SelectionRules.js.map +1 -0
  110. package/dist/src/types/Standard.d.ts +53 -0
  111. package/dist/src/types/Standard.d.ts.map +1 -1
  112. package/dist/src/types/Subject.d.ts +31 -1
  113. package/dist/src/types/Subject.d.ts.map +1 -1
  114. package/dist/src/types/TestReport.d.ts +71 -0
  115. package/dist/src/types/TestReport.d.ts.map +1 -0
  116. package/dist/src/types/TestReport.js +73 -0
  117. package/dist/src/types/TestReport.js.map +1 -0
  118. package/dist/src/types/VerificationPathway.d.ts +63 -0
  119. package/dist/src/types/VerificationPathway.d.ts.map +1 -0
  120. package/dist/src/types/VerificationPathway.js +56 -0
  121. package/dist/src/types/VerificationPathway.js.map +1 -0
  122. package/dist/src/types/WorkflowConfig.d.ts +34 -0
  123. package/dist/src/types/WorkflowConfig.d.ts.map +1 -0
  124. package/dist/src/types/WorkflowConfig.js +47 -0
  125. package/dist/src/types/WorkflowConfig.js.map +1 -0
  126. package/dist-browser/index.mjs +2135 -1017
  127. package/dist-cli/primmel.mjs +7462 -4450
  128. package/package.json +1 -1
package/dist/src/check.js CHANGED
@@ -486,6 +486,11 @@ function checkPackage(dir, options = {}) {
486
486
  else if (i.code === 'duplicate-id') {
487
487
  err('C96', `${i.message} (duplicate-id)`);
488
488
  }
489
+ else if (i.code === 'orphan-overlay-entry') {
490
+ // The checklist overlay's orphan-entry guard (C141's
491
+ // composition leg β€” smart TODO.roadmap/40 batch 3).
492
+ err('C141', i.message);
493
+ }
489
494
  }
490
495
  }
491
496
  catch (e) {
@@ -1030,6 +1035,97 @@ function checkPackage(dir, options = {}) {
1030
1035
  }
1031
1036
  }
1032
1037
  }
1038
+ // C42–C44 generalized to the promise_set register (smart
1039
+ // TODO.roadmap/40 batch 3; the packages-as-SSOT epic): the rec promise
1040
+ // registers carry the SAME sub-grammar as is.promises, so the same
1041
+ // discipline fires. The set id binds the owning subject: when that
1042
+ // subject composes, its characteristics resolve exactly as above; when
1043
+ // it does not, the characteristic leg is gated off (per-register
1044
+ // gating, the C58 doctrine) and attribute/behavior resolution stands.
1045
+ for (const ps of standard.promiseSets ?? []) {
1046
+ const owner = (standard.subjects ?? []).find(s => s.id === ps.id);
1047
+ const characteristicSymbols = new Map();
1048
+ for (const [k, c] of Object.entries(owner?.has.characteristics ?? {})) {
1049
+ characteristicSymbols.set(k, c.symbol ?? '');
1050
+ }
1051
+ const bareValueTargets = new Set([
1052
+ ...attrIds,
1053
+ ...Object.keys(owner?.is.designParameters ?? {}),
1054
+ ...Object.keys(owner?.has.attributes ?? {}),
1055
+ ]);
1056
+ for (const p of ps.promises ?? []) {
1057
+ const label = p.id ||
1058
+ (p.statement.length > 40
1059
+ ? p.statement.slice(0, 37) + '…'
1060
+ : p.statement);
1061
+ const where = `promise_set ${ps.id}: promise "${label}"`;
1062
+ // C2 β€” declared verified_by ids must resolve (same discipline as
1063
+ // the subject promises).
1064
+ for (const v of p.verifiedBy ?? []) {
1065
+ if (!reqIds.has(v) && !testIds.has(v)) {
1066
+ err('C2', `${where} verified_by "${v}" is not a declared requirement or conformance test`);
1067
+ }
1068
+ }
1069
+ if (!p.target) {
1070
+ if ((p.verifiedBy ?? []).length === 0) {
1071
+ warn('C43', `${where} has no target and no verified_by β€” it is unverifiable; give it a characteristic/behavior target or declare the verifying requirement/test (promise-verifiable)`);
1072
+ }
1073
+ continue;
1074
+ }
1075
+ const isCharacteristic = characteristicSymbols.has(p.target);
1076
+ const isBehavior = behaviorIds.has(p.target);
1077
+ if (!isCharacteristic && !isBehavior) {
1078
+ if (bareValueTargets.has(p.target)) {
1079
+ // C44 (narrowed) β€” same restatement leg as subject promises.
1080
+ const restatesBareValue = p.level?.kind === 'quantity' &&
1081
+ p.conditions === '' &&
1082
+ (p.verifiedBy ?? []).length === 0;
1083
+ if (restatesBareValue) {
1084
+ err('C44', `${where} only restates the declared attribute value "${p.target}" β€” a promise claims a characteristic or behavior (optionally conditioned); bare values stay origin: declared attributes (promise-not-bare-value)`);
1085
+ }
1086
+ }
1087
+ else if (owner) {
1088
+ // C42 β€” gated on the owning subject composing: without it the
1089
+ // characteristic register is out of scope and the target's
1090
+ // characteristic-ness is unknowable.
1091
+ err('C42', `${where} target "${p.target}" is not a declared characteristic or behavior (promise-target-resolves)`);
1092
+ }
1093
+ continue;
1094
+ }
1095
+ if ((p.verifiedBy ?? []).length > 0) {
1096
+ continue; // verification declared β€” C43 silent
1097
+ }
1098
+ // C43 β€” derive the verifying requirements/tests from the target.
1099
+ const names = new Set([p.target]);
1100
+ if (isCharacteristic) {
1101
+ const sym = characteristicSymbols.get(p.target);
1102
+ if (sym) {
1103
+ names.add(sym);
1104
+ }
1105
+ }
1106
+ const boundReqs = new Set();
1107
+ for (const r of standard.requirements ?? []) {
1108
+ if (requirementBindsPromiseTarget(r, names)) {
1109
+ boundReqs.add(r.id);
1110
+ }
1111
+ }
1112
+ const boundTests = new Set();
1113
+ for (const t of standard.conformanceTests ?? []) {
1114
+ if (testBindsPromiseTarget(t, names, boundReqs)) {
1115
+ boundTests.add(t.id);
1116
+ }
1117
+ }
1118
+ if (isBehavior) {
1119
+ const b = (standard.behaviors ?? []).find(x => x.id === p.target);
1120
+ for (const tid of b?.verifiedBy ?? []) {
1121
+ boundTests.add(tid);
1122
+ }
1123
+ }
1124
+ if (boundReqs.size === 0 && boundTests.size === 0) {
1125
+ warn('C43', `${where} declares no verified_by and no requirement/test binds its target "${p.target}" β€” declare the verifying requirement or test (promise-verifiable)`);
1126
+ }
1127
+ }
1128
+ }
1033
1129
  // ── C45–C47: artifacts (TODO.roadmap/09 β€” gap audit G2) ──
1034
1130
  // An artifact is a required OUTPUT OF THE SUBJECT (the instrument) β€” not
1035
1131
  // a record of the test. artifact_definition (IS: content contract +
@@ -3520,6 +3616,603 @@ function checkPackage(dir, options = {}) {
3520
3616
  }
3521
3617
  }
3522
3618
  }
3619
+ // ── C130: identity-and-aspect-references (smart TODO.roadmap/40 ─────
3620
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3621
+ // The documentary identity slots + the qualitative aspect register
3622
+ // (smart TODO.roadmap/47). Legs: the slot shape leg (at least one
3623
+ // presentation channel β€” the slot's content is presented somewhere);
3624
+ // the aspect references β€” term_ref β†’ the term register, component β†’
3625
+ // an instrument component, attribute β†’ an attribute_definition (a
3626
+ // reference, never a redeclaration β€” INV-3), and the contains entries
3627
+ // β†’ EITHER a model.identity.<slot> path into the slot register OR a
3628
+ // bare attribute/dimension id (the two namespaces coexist; each leg
3629
+ // checked only when its target register composes β€” per-register
3630
+ // gating, the C58 doctrine); and the R28 bind-path consumer leg β€” a
3631
+ // requirement/test binds_to path of the model.identity.<slot> or
3632
+ // model.aspects.<id> form names a declared slot/aspect when the
3633
+ // register is in scope.
3634
+ {
3635
+ const slotIds = new Set((standard.identitySlots ?? []).map(s => s.id));
3636
+ const aspectIds = new Set((standard.aspects ?? []).map(a => a.id));
3637
+ const termIds = new Set((standard.terms ?? []).map(t => t.id));
3638
+ const componentIds = new Set((standard.instruments ?? []).flatMap(i => (i.components ?? []).map(c => c.id)));
3639
+ for (const s of standard.identitySlots ?? []) {
3640
+ if (s.presentation.length === 0) {
3641
+ err('C130', `identity_slot ${s.id}: at least one presentation channel is required β€” the slot's content is presented somewhere (identity-and-aspect-references)`);
3642
+ }
3643
+ }
3644
+ // A contains entry resolves through the model.identity path into the
3645
+ // slot register, or as a bare id into the attribute/dimension
3646
+ // registers; null = no target register in scope (the gated skip).
3647
+ const containsResolves = (entry) => {
3648
+ if (entry.startsWith('model.identity.')) {
3649
+ const slot = entry.slice('model.identity.'.length);
3650
+ return slotIds.size > 0 ? slotIds.has(slot) : null;
3651
+ }
3652
+ if (attrIds.size > 0 || dimIds.size > 0) {
3653
+ return attrIds.has(entry) || dimIds.has(entry);
3654
+ }
3655
+ return null;
3656
+ };
3657
+ for (const a of standard.aspects ?? []) {
3658
+ const where = `aspect ${a.id}`;
3659
+ if (a.termRef && termIds.size > 0 && !termIds.has(a.termRef)) {
3660
+ err('C130', `${where}: term_ref "${a.termRef}" is not a declared term (identity-and-aspect-references)`);
3661
+ }
3662
+ if (a.component &&
3663
+ componentIds.size > 0 &&
3664
+ !componentIds.has(a.component)) {
3665
+ err('C130', `${where}: component "${a.component}" is not a declared instrument component (identity-and-aspect-references)`);
3666
+ }
3667
+ if (a.attribute && attrIds.size > 0 && !attrIds.has(a.attribute)) {
3668
+ err('C130', `${where}: attribute "${a.attribute}" is not a declared attribute_definition β€” an aspect points at a quantity, never redeclares it (identity-and-aspect-references)`);
3669
+ }
3670
+ for (const entry of a.contains ?? []) {
3671
+ if (containsResolves(entry) === false) {
3672
+ err('C130', `${where}: contains entry "${entry}" resolves to neither a model.identity slot nor a declared attribute/dimension (identity-and-aspect-references)`);
3673
+ }
3674
+ }
3675
+ }
3676
+ // The R28 bind-path consumer leg (requirement + conformance-test
3677
+ // binds_to), per-register gated.
3678
+ const bindOwners = [
3679
+ ...(standard.requirements ?? []).map(r => [`requirement ${r.id}`, r.bindsTo ?? []]),
3680
+ ...(standard.conformanceTests ?? []).map(t => [`conformance_test ${t.id}`, t.bindsTo ?? []]),
3681
+ ];
3682
+ for (const [owner, paths] of bindOwners) {
3683
+ for (const p of paths) {
3684
+ const path = String(p);
3685
+ if (path.startsWith('model.identity.') && slotIds.size > 0) {
3686
+ const slot = path.slice('model.identity.'.length);
3687
+ if (!slotIds.has(slot)) {
3688
+ err('C130', `${owner}: binds_to "${path}" β€” identity slot "${slot}" is not declared (identity-and-aspect-references)`);
3689
+ }
3690
+ }
3691
+ else if (path.startsWith('model.aspects.') && aspectIds.size > 0) {
3692
+ const aspect = path.slice('model.aspects.'.length);
3693
+ if (!aspectIds.has(aspect)) {
3694
+ err('C130', `${owner}: binds_to "${path}" β€” aspect "${aspect}" is not declared (identity-and-aspect-references)`);
3695
+ }
3696
+ }
3697
+ }
3698
+ }
3699
+ }
3700
+ // ── C131: promise-certificate-projection (smart TODO.roadmap/40 ─────
3701
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3702
+ // The certificate print projection's shape, fired on subject promises
3703
+ // AND promise_set entries alike: the content-binding XOR (at most one
3704
+ // of attribute / attributes / dimension; none = a statement row); the
3705
+ // bindings resolve (per-register gated, the C58 doctrine); the type is
3706
+ // the renderer's closed vocabulary (string | integer | number |
3707
+ // quantity | statement) β€” a check-time ERROR (the owner-settled
3708
+ // renderer contract), kept parse-total against renderer growth; the
3709
+ // label is required (the printed row carries its header). The
3710
+ // obligation vocabulary is parse-enforced upstream (no check leg).
3711
+ {
3712
+ const CERTIFICATE_TYPES = [
3713
+ 'string',
3714
+ 'integer',
3715
+ 'number',
3716
+ 'quantity',
3717
+ 'statement',
3718
+ ];
3719
+ const certs = [];
3720
+ for (const s of standard.subjects ?? []) {
3721
+ for (const p of s.is.promises ?? []) {
3722
+ if (p.certificate) {
3723
+ certs.push([`subject ${s.id}: promise "${p.id}"`, p.certificate]);
3724
+ }
3725
+ }
3726
+ }
3727
+ for (const ps of standard.promiseSets ?? []) {
3728
+ for (const p of ps.promises ?? []) {
3729
+ if (p.certificate) {
3730
+ certs.push([
3731
+ `promise_set ${ps.id}: promise "${p.id}"`,
3732
+ p.certificate,
3733
+ ]);
3734
+ }
3735
+ }
3736
+ }
3737
+ for (const [where, cert] of certs) {
3738
+ const bindings = (cert.attribute ? 1 : 0) +
3739
+ (cert.attributes.length > 0 ? 1 : 0) +
3740
+ (cert.dimension ? 1 : 0);
3741
+ if (bindings > 1) {
3742
+ err('C131', `${where}: the certificate content binds by XOR β€” at most one of attribute / attributes / dimension (promise-certificate-projection)`);
3743
+ }
3744
+ if (cert.attribute && attrIds.size > 0 && !attrIds.has(cert.attribute)) {
3745
+ err('C131', `${where}: certificate attribute "${cert.attribute}" is not a declared attribute_definition (promise-certificate-projection)`);
3746
+ }
3747
+ for (const a of cert.attributes ?? []) {
3748
+ if (attrIds.size > 0 && !attrIds.has(a)) {
3749
+ err('C131', `${where}: certificate attributes entry "${a}" is not a declared attribute_definition (promise-certificate-projection)`);
3750
+ }
3751
+ }
3752
+ if (cert.dimension && dimIds.size > 0 && !dimIds.has(cert.dimension)) {
3753
+ err('C131', `${where}: certificate dimension "${cert.dimension}" is not a declared classification dimension (promise-certificate-projection)`);
3754
+ }
3755
+ if (!cert.type) {
3756
+ err('C131', `${where}: the certificate type is required β€” the renderer's row kind (promise-certificate-projection)`);
3757
+ }
3758
+ else if (!CERTIFICATE_TYPES.includes(cert.type)) {
3759
+ err('C131', `${where}: certificate type "${cert.type}" is outside the renderer's closed vocabulary (valid: ${CERTIFICATE_TYPES.join(', ')}) (promise-certificate-projection)`);
3760
+ }
3761
+ if (!cert.label) {
3762
+ err('C131', `${where}: the certificate label is required β€” the printed row carries its header (promise-certificate-projection)`);
3763
+ }
3764
+ }
3765
+ }
3766
+ // ── C132: application-declaration-references (smart TODO.roadmap/40 ─
3767
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3768
+ // The applicant-facing documentation register: the declaration_form
3769
+ // names a declared form WHEN the form register is in composition scope
3770
+ // (per-register gating, the C58 doctrine), and the document ids are
3771
+ // unique within the declaration. The obligation vocabulary is
3772
+ // parse-enforced upstream (no check leg).
3773
+ {
3774
+ const formIds = new Set((standard.forms ?? []).map(f => f.id));
3775
+ for (const d of standard.applicationDeclarations ?? []) {
3776
+ const where = `application_declaration ${d.id}`;
3777
+ if (d.declarationForm &&
3778
+ formIds.size > 0 &&
3779
+ !formIds.has(d.declarationForm)) {
3780
+ err('C132', `${where}: declaration_form "${d.declarationForm}" is not a declared form (application-declaration-references)`);
3781
+ }
3782
+ const seen = new Set();
3783
+ for (const doc of d.documents ?? []) {
3784
+ if (seen.has(doc.id)) {
3785
+ err('C132', `${where}: document "${doc.id}" is declared twice (application-declaration-references)`);
3786
+ }
3787
+ seen.add(doc.id);
3788
+ }
3789
+ }
3790
+ }
3791
+ // ── C133: calculation-context-references (smart TODO.roadmap/40 ─────
3792
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3793
+ // The evaluation-side wiring: the source is `computed` (expression
3794
+ // required) or a subject-chain source β€” classification.<dimension>
3795
+ // (resolves, gated) or parameters.<attribute> (resolves, gated). The
3796
+ // legacy schema tokens (dimensions, application, application.specs,
3797
+ // lookup) and anything else ERROR β€” the modern subject-chain spelling
3798
+ // is forced. The expression's free identifiers name the context's own
3799
+ // fields β€” a WARNING leg: runtime-bound inputs (r60's `f`, r144's
3800
+ // `measured_value`) are legitimate non-field references (function
3801
+ // calls exempt β€” an identifier followed by `(` is a callee).
3802
+ {
3803
+ for (const cc of standard.calculationContexts ?? []) {
3804
+ const fieldNames = new Set((cc.fields ?? []).map(f => f.id));
3805
+ for (const f of cc.fields ?? []) {
3806
+ const where = `calculation_context ${cc.id}: field ${f.id}`;
3807
+ if (f.source === 'computed') {
3808
+ if (!f.expression) {
3809
+ err('C133', `${where}: source computed requires the expression facet (calculation-context-references)`);
3810
+ }
3811
+ }
3812
+ else if (f.source.startsWith('classification.') ||
3813
+ f.source.startsWith('parameters.')) {
3814
+ if (f.expression) {
3815
+ err('C133', `${where}: a non-computed source carries no expression (calculation-context-references)`);
3816
+ }
3817
+ if (f.source.startsWith('classification.')) {
3818
+ const dim = f.source.slice('classification.'.length);
3819
+ if (dimIds.size > 0 && !dimIds.has(dim)) {
3820
+ err('C133', `${where}: source "${f.source}" β€” "${dim}" is not a declared classification dimension (calculation-context-references)`);
3821
+ }
3822
+ }
3823
+ else {
3824
+ const attr = f.source.slice('parameters.'.length);
3825
+ if (attrIds.size > 0 && !attrIds.has(attr)) {
3826
+ err('C133', `${where}: source "${f.source}" β€” "${attr}" is not a declared attribute_definition (calculation-context-references)`);
3827
+ }
3828
+ }
3829
+ }
3830
+ else {
3831
+ err('C133', `${where}: source "${f.source}" is not a subject-chain source (classification.<dimension>, parameters.<attribute>, computed) (calculation-context-references)`);
3832
+ }
3833
+ if (f.expression) {
3834
+ const idents = new Set();
3835
+ const re = /[A-Za-z_][A-Za-z0-9_]*/g;
3836
+ let m;
3837
+ while ((m = re.exec(f.expression)) !== null) {
3838
+ if (!/^\s*\(/.test(f.expression.slice(re.lastIndex))) {
3839
+ idents.add(m[0]);
3840
+ }
3841
+ }
3842
+ for (const ident of idents) {
3843
+ if (!fieldNames.has(ident)) {
3844
+ warn('C133', `${where}: expression identifier "${ident}" is not a field of this context β€” a runtime-bound input (declare it as a field or bind it at runtime) (calculation-context-references)`);
3845
+ }
3846
+ }
3847
+ }
3848
+ }
3849
+ }
3850
+ }
3851
+ // ── C134: evaluation-dimension-references (smart TODO.roadmap/40 ────
3852
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3853
+ // The form-facing classification field schema: the enum facet resolves
3854
+ // to a declared classification dimension (error, per-register gated),
3855
+ // and the field NAME resolves to an is_dimension attribute_definition
3856
+ // OR a dimension id β€” the two coexisting conventions (owner decision:
3857
+ // r129's legacy camelCase names carry as-is), a WARNING when neither
3858
+ // (gated on the registers composing). The type/setting vocabularies
3859
+ // are parse-enforced upstream (no check legs).
3860
+ {
3861
+ const dimensionedAttrIds = new Set((standard.attributeDefinitions ?? [])
3862
+ .filter(a => a.isDimension === true)
3863
+ .map(a => a.id));
3864
+ for (const ed of standard.evaluationDimensions ?? []) {
3865
+ for (const f of ed.fields ?? []) {
3866
+ const where = `evaluation_dimensions ${ed.id}: field ${f.id}`;
3867
+ if (f.enumRef && dimIds.size > 0 && !dimIds.has(f.enumRef)) {
3868
+ err('C134', `${where}: enum "${f.enumRef}" is not a declared classification dimension (evaluation-dimension-references)`);
3869
+ }
3870
+ if ((dimensionedAttrIds.size > 0 || dimIds.size > 0) &&
3871
+ !dimensionedAttrIds.has(f.id) &&
3872
+ !dimIds.has(f.id)) {
3873
+ warn('C134', `${where}: the field name "${f.id}" resolves to neither an is_dimension attribute_definition nor a classification dimension β€” the legacy camelCase convention (evaluation-dimension-references)`);
3874
+ }
3875
+ }
3876
+ }
3877
+ }
3878
+ // ── C135: evaluation-profile-coherence (smart TODO.roadmap/40 ───────
3879
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3880
+ // The named dimension-value presets: every dimensions key resolves to
3881
+ // a declared classification dimension and every value to one of that
3882
+ // dimension's declared values (per-register gated, the smart R4/R8
3883
+ // applicability mirror; an OPEN dimension β€” no declared value set β€”
3884
+ // accepts any value).
3885
+ {
3886
+ for (const p of standard.evaluationProfiles ?? []) {
3887
+ const where = `evaluation_profile ${p.id}`;
3888
+ for (const [dim, value] of Object.entries(p.dimensions ?? {})) {
3889
+ if (dimIds.size > 0 && !dimIds.has(dim)) {
3890
+ err('C135', `${where}: dimensions key "${dim}" is not a declared classification dimension (evaluation-profile-coherence)`);
3891
+ continue;
3892
+ }
3893
+ const values = dimIds.get(dim);
3894
+ if (values && values.size > 0 && !values.has(value)) {
3895
+ err('C135', `${where}: dimensions entry "${dim}" names value "${value}", which the dimension does not declare (evaluation-profile-coherence)`);
3896
+ }
3897
+ }
3898
+ }
3899
+ }
3900
+ // ── C136: certificate-template-references (smart TODO.roadmap/40 ────
3901
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3902
+ // The certificate rendering contract: the dimension_labels pattern
3903
+ // placeholders ({dim} / {dim:sep}) resolve to declared classification
3904
+ // dimensions (error, per-register gated); the characteristic bindings
3905
+ // resolve (attribute/attributes β†’ attribute_definitions, dimension β†’
3906
+ // classification dimensions, per-register gated) with the XOR shape
3907
+ // leg (≀ 1 of attribute | attributes | dimension β€” none is the
3908
+ // statement row); the characteristic type carries the renderer-driven
3909
+ // closed vocabulary (string | integer | number | quantity |
3910
+ // statement) at ERROR (the owner-settled decision β€” parse stays
3911
+ // total against renderer growth); the number_format placeholders
3912
+ // check against the known token vocabulary at WARNING (open for
3913
+ // program-specific prefixes like twin-cert's literal TW-1).
3914
+ {
3915
+ const CERTIFICATE_NUMBER_TOKENS = new Set([
3916
+ 'shortName',
3917
+ 'edition',
3918
+ 'scheme',
3919
+ 'authority',
3920
+ 'year2',
3921
+ 'seq',
3922
+ ]);
3923
+ const CERTIFICATE_CHARACTERISTIC_TYPES = new Set([
3924
+ 'string',
3925
+ 'integer',
3926
+ 'number',
3927
+ 'quantity',
3928
+ 'statement',
3929
+ ]);
3930
+ const placeholderRe = /\{([^{}:/]+)(?::[^{}]*)?\}/g;
3931
+ for (const t of standard.certificateTemplates ?? []) {
3932
+ const where = `certificate_template ${t.id}`;
3933
+ if (t.numberFormat) {
3934
+ let m;
3935
+ while ((m = placeholderRe.exec(t.numberFormat)) !== null) {
3936
+ if (!CERTIFICATE_NUMBER_TOKENS.has(m[1])) {
3937
+ warn('C136', `${where}: number_format placeholder "{${m[1]}}" is not a known token (${[...CERTIFICATE_NUMBER_TOKENS].join(', ')}) β€” a program-specific spelling (certificate-template-references)`);
3938
+ }
3939
+ }
3940
+ }
3941
+ if (t.dimensionLabels && dimIds.size > 0) {
3942
+ let m;
3943
+ while ((m = placeholderRe.exec(t.dimensionLabels.pattern)) !== null) {
3944
+ if (!dimIds.has(m[1])) {
3945
+ err('C136', `${where}: dimension_labels placeholder "{${m[1]}}" is not a declared classification dimension (certificate-template-references)`);
3946
+ }
3947
+ }
3948
+ }
3949
+ for (const c of t.characteristics ?? []) {
3950
+ const cwhere = `${where}: characteristic ${c.id}`;
3951
+ if (c.type && !CERTIFICATE_CHARACTERISTIC_TYPES.has(c.type)) {
3952
+ err('C136', `${cwhere}: type "${c.type}" is outside the renderer vocabulary (string, integer, number, quantity, statement) (certificate-template-references)`);
3953
+ }
3954
+ const bindings = (c.attribute ? 1 : 0) +
3955
+ (c.attributes.length > 0 ? 1 : 0) +
3956
+ (c.dimension ? 1 : 0);
3957
+ if (bindings > 1) {
3958
+ err('C136', `${cwhere}: carries ${bindings} content bindings β€” the XOR is attribute | attributes | dimension | none (certificate-template-references)`);
3959
+ }
3960
+ if (c.attribute && attrIds.size > 0 && !attrIds.has(c.attribute)) {
3961
+ err('C136', `${cwhere}: attribute "${c.attribute}" is not a declared attribute_definition (certificate-template-references)`);
3962
+ }
3963
+ for (const a of c.attributes) {
3964
+ if (attrIds.size > 0 && !attrIds.has(a)) {
3965
+ err('C136', `${cwhere}: attributes entry "${a}" is not a declared attribute_definition (certificate-template-references)`);
3966
+ }
3967
+ }
3968
+ if (c.dimension && dimIds.size > 0 && !dimIds.has(c.dimension)) {
3969
+ err('C136', `${cwhere}: dimension "${c.dimension}" is not a declared classification dimension (certificate-template-references)`);
3970
+ }
3971
+ }
3972
+ }
3973
+ }
3974
+ // ── C137: workflow-config-references (smart TODO.roadmap/40 ────────
3975
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
3976
+ // The certification workflow step register: the actor resolves against
3977
+ // the composed role register (per-register gated; role ids are
3978
+ // snake_case β€” the YAML hyphenation is the emitter's); the inputs/
3979
+ // outputs entries resolve against the data-class register ONLY when
3980
+ // the entry is a single clean token (composite strings like
3981
+ // "TestReport containing FormInstance" stay documentary β€” skipped,
3982
+ // never errored); the gates are prose predicates β€” never resolved
3983
+ // (the R26 field-resolution precedent). The phase vocabulary is
3984
+ // parse-enforced upstream (no check leg).
3985
+ {
3986
+ const roleIds = new Set((standard.roles ?? []).map(r => r.id));
3987
+ // Class ids may carry a namespace suffix (`Application#data`) while
3988
+ // the workflow's inputs/outputs spell the bare entity name β€” index
3989
+ // both spellings (the semantic-status leg's precedent).
3990
+ const dataclassIds = new Set();
3991
+ for (const c of standard.dataclasses ?? []) {
3992
+ dataclassIds.add(c.id);
3993
+ if (c.id.includes('#')) {
3994
+ dataclassIds.add(c.id.slice(0, c.id.indexOf('#')));
3995
+ }
3996
+ }
3997
+ const CLEAN_TOKEN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3998
+ for (const w of standard.workflowConfigs ?? []) {
3999
+ for (const s of w.steps ?? []) {
4000
+ const where = `workflow_config ${w.id}: step ${s.id}`;
4001
+ if (s.actor && roleIds.size > 0 && !roleIds.has(s.actor)) {
4002
+ err('C137', `${where}: actor "${s.actor}" is not a declared role (workflow-config-references)`);
4003
+ }
4004
+ for (const [facet, list] of [
4005
+ ['inputs', s.inputs],
4006
+ ['outputs', s.outputs],
4007
+ ]) {
4008
+ for (const entry of list) {
4009
+ if (!CLEAN_TOKEN.test(entry)) {
4010
+ continue; // a composite documentary string β€” never resolved
4011
+ }
4012
+ if (dataclassIds.size > 0 && !dataclassIds.has(entry)) {
4013
+ err('C137', `${where}: ${facet} entry "${entry}" is not a declared data class (workflow-config-references)`);
4014
+ }
4015
+ }
4016
+ }
4017
+ }
4018
+ }
4019
+ }
4020
+ // ── C138: verification-pathway-references (smart TODO.roadmap/40 ───
4021
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
4022
+ // The verification pathways beyond type evaluation: the tests resolve
4023
+ // to conformance_tests and the assessment covers to requirements
4024
+ // (per-register gated β€” the smart R22 mirror); the trigger action
4025
+ // resolves to a transition action of an in-scope LIFECYCLE
4026
+ // state_machine (gated); the trigger event is required iff kind
4027
+ // signal; the validity window carries β‰₯ 1 of years/months. The kind /
4028
+ // trigger-kind / limits-mode vocabularies are parse-enforced upstream
4029
+ // (no check legs).
4030
+ {
4031
+ const lifecycleActions = new Set();
4032
+ for (const sm of standard.stateMachines ?? []) {
4033
+ if (sm.kind !== 'lifecycle') {
4034
+ continue;
4035
+ }
4036
+ for (const t of sm.transitions ?? []) {
4037
+ if (t.actionName) {
4038
+ lifecycleActions.add(t.actionName);
4039
+ }
4040
+ }
4041
+ }
4042
+ for (const p of standard.verificationPathways ?? []) {
4043
+ const where = `verification_pathway ${p.id}`;
4044
+ for (const t of p.tests ?? []) {
4045
+ if (testIds.size > 0 && !testIds.has(t)) {
4046
+ err('C138', `${where}: tests entry "${t}" is not a declared conformance_test (verification-pathway-references)`);
4047
+ }
4048
+ }
4049
+ for (const r of p.assessment?.covers ?? []) {
4050
+ if (reqIds.size > 0 && !reqIds.has(r)) {
4051
+ err('C138', `${where}: assessment covers entry "${r}" is not a declared requirement (verification-pathway-references)`);
4052
+ }
4053
+ }
4054
+ const window = p.validity?.window;
4055
+ if (window && window.years <= 0 && window.months <= 0) {
4056
+ err('C138', `${where}: the validity window carries neither years nor months (verification-pathway-references)`);
4057
+ }
4058
+ for (const t of p.validity?.triggers ?? []) {
4059
+ const twhere = `${where}: trigger ${t.id}`;
4060
+ if (t.kind === 'signal' && !t.event) {
4061
+ err('C138', `${twhere}: kind signal requires the event facet (verification-pathway-references)`);
4062
+ }
4063
+ if (t.kind === 'timer' && t.event) {
4064
+ err('C138', `${twhere}: kind timer carries no event facet β€” the period is the window's (verification-pathway-references)`);
4065
+ }
4066
+ if (t.action &&
4067
+ lifecycleActions.size > 0 &&
4068
+ !lifecycleActions.has(t.action)) {
4069
+ err('C138', `${twhere}: action "${t.action}" is not a transition action of an in-scope lifecycle state_machine (verification-pathway-references)`);
4070
+ }
4071
+ }
4072
+ }
4073
+ }
4074
+ // ── C139: selection-rule-references (smart TODO.roadmap/40 ─────────
4075
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
4076
+ // The selection rules: the lab criterion's applies_when/match
4077
+ // model_field resolve against the attribute OR dimension registers
4078
+ // (the two coexisting conventions β€” gated on either register
4079
+ // composing); the match operator's conditional facets hold
4080
+ // (has_capability β‡’ required_capability; capability_from_field /
4081
+ // capability_covers_value / capability_prefix_match β‡’
4082
+ // required_capability_prefix + model_field; always_pass β‡’ nothing
4083
+ // else); the capability strings are free-form lab-side labels β€”
4084
+ // never resolved. The sample/governance entries carry shape legs
4085
+ // only (rule / rationale / applicability non-empty). The vocabularies
4086
+ // are parse-enforced upstream (no check legs).
4087
+ {
4088
+ const fieldResolvable = (f) => attrIds.has(f) || dimIds.has(f);
4089
+ const fieldRegistersCompose = attrIds.size > 0 || dimIds.size > 0;
4090
+ for (const c of standard.labSelectionCriteria ?? []) {
4091
+ const where = `lab_selection_criterion ${c.id}`;
4092
+ if (c.appliesWhen?.modelField &&
4093
+ fieldRegistersCompose &&
4094
+ !fieldResolvable(c.appliesWhen.modelField)) {
4095
+ err('C139', `${where}: applies_when model_field "${c.appliesWhen.modelField}" resolves to neither an attribute_definition nor a classification dimension (selection-rule-references)`);
4096
+ }
4097
+ const m = c.match;
4098
+ if (m) {
4099
+ if (m.modelField &&
4100
+ fieldRegistersCompose &&
4101
+ !fieldResolvable(m.modelField)) {
4102
+ err('C139', `${where}: match model_field "${m.modelField}" resolves to neither an attribute_definition nor a classification dimension (selection-rule-references)`);
4103
+ }
4104
+ if (m.operator === 'has_capability' && !m.requiredCapability) {
4105
+ err('C139', `${where}: operator has_capability requires the required_capability facet (selection-rule-references)`);
4106
+ }
4107
+ if ((m.operator === 'capability_from_field' ||
4108
+ m.operator === 'capability_covers_value' ||
4109
+ m.operator === 'capability_prefix_match') &&
4110
+ (!m.requiredCapabilityPrefix || !m.modelField)) {
4111
+ err('C139', `${where}: operator ${m.operator} requires required_capability_prefix and model_field (selection-rule-references)`);
4112
+ }
4113
+ if (m.operator === 'always_pass' &&
4114
+ (m.requiredCapability ||
4115
+ m.requiredCapabilityPrefix ||
4116
+ m.modelField ||
4117
+ m.labCapabilityPrefix)) {
4118
+ err('C139', `${where}: operator always_pass carries no other match facet (selection-rule-references)`);
4119
+ }
4120
+ }
4121
+ }
4122
+ for (const r of standard.sampleSelectionRules ?? []) {
4123
+ const where = `sample_selection_rule ${r.id}`;
4124
+ for (const [facet, v] of [
4125
+ ['rule', r.rule],
4126
+ ['rationale', r.rationale],
4127
+ ['applicability', r.applicability],
4128
+ ]) {
4129
+ if (!v) {
4130
+ err('C139', `${where}: the ${facet} facet is required (selection-rule-references)`);
4131
+ }
4132
+ }
4133
+ }
4134
+ for (const r of standard.specimenGovernanceRules ?? []) {
4135
+ const where = `specimen_governance_rule ${r.id}`;
4136
+ for (const [facet, v] of [
4137
+ ['rule', r.rule],
4138
+ ['rationale', r.rationale],
4139
+ ['applicability', r.applicability],
4140
+ ]) {
4141
+ if (!v) {
4142
+ err('C139', `${where}: the ${facet} facet is required (selection-rule-references)`);
4143
+ }
4144
+ }
4145
+ }
4146
+ }
4147
+ // ── C140: test-report-skeleton-references (smart TODO.roadmap/40 ───
4148
+ // batch 3; the packages-as-SSOT epic) ────────────────────────────────
4149
+ // The evaluation-report structure: the form entry id resolves to a
4150
+ // declared form, the conformance_test facet to a conformance_test, the
4151
+ // requirements to requirements (all per-register gated β€” the smart
4152
+ // R29/R35 mirrors); a conditional form carries its inclusion rule
4153
+ // (applicability) or a documentary note (warning). The required
4154
+ // vocabulary is parse-enforced upstream (no check leg).
4155
+ {
4156
+ const formIds = new Set((standard.forms ?? []).map(f => f.id));
4157
+ const checkFormEntries = (where, entries) => {
4158
+ for (const f of entries) {
4159
+ const fwhere = `${where}: form ${f.id}`;
4160
+ if (formIds.size > 0 && !formIds.has(f.id)) {
4161
+ err('C140', `${fwhere}: the entry id is not a declared form (test-report-skeleton-references)`);
4162
+ }
4163
+ if (f.conformanceTest &&
4164
+ testIds.size > 0 &&
4165
+ !testIds.has(f.conformanceTest)) {
4166
+ err('C140', `${fwhere}: conformance_test "${f.conformanceTest}" is not a declared conformance_test (test-report-skeleton-references)`);
4167
+ }
4168
+ for (const r of f.requirements) {
4169
+ if (reqIds.size > 0 && !reqIds.has(r)) {
4170
+ err('C140', `${fwhere}: requirements entry "${r}" is not a declared requirement (test-report-skeleton-references)`);
4171
+ }
4172
+ }
4173
+ if (f.required === 'conditional' &&
4174
+ f.applicability.length === 0 &&
4175
+ !f.notes) {
4176
+ warn('C140', `${fwhere}: required conditional carries neither applicability nor notes β€” the inclusion rule is undocumented (test-report-skeleton-references)`);
4177
+ }
4178
+ }
4179
+ };
4180
+ for (const s of standard.testReportSkeletons ?? []) {
4181
+ for (const section of s.sections ?? []) {
4182
+ const where = `test_report_skeleton ${s.id}: section ${section.id}`;
4183
+ checkFormEntries(where, section.forms ?? []);
4184
+ for (const sub of section.subsections ?? []) {
4185
+ checkFormEntries(`${where}: subsection "${sub.title}"`, sub.forms ?? []);
4186
+ }
4187
+ }
4188
+ }
4189
+ }
4190
+ // ── C141: checklist-entry-shape (smart TODO.roadmap/40 batch 3; ────
4191
+ // the packages-as-SSOT epic) ─────────────────────────────────────────
4192
+ // The OIML-CS test-report checklist (PD-05 Β§4.4.3): the element
4193
+ // letter rides the a–r vocabulary at WARNING (check-time, never
4194
+ // parse-enforced β€” the element count can grow); the entry ids are
4195
+ // unique per checklist; the overlay orphan-entry leg fires at
4196
+ // COMPOSITION (the package.ts merge loop reports
4197
+ // orphan-overlay-entry, surfaced under this rule above). The
4198
+ // obligation vocabulary is parse-enforced upstream; the
4199
+ // source/validation strings are engine expressions β€” documentary,
4200
+ // never resolved.
4201
+ {
4202
+ for (const c of standard.testReportChecklists ?? []) {
4203
+ const where = `test_report_checklist ${c.id}`;
4204
+ const seen = new Set();
4205
+ for (const e of c.entries ?? []) {
4206
+ if (seen.has(e.id)) {
4207
+ err('C141', `${where}: entry "${e.id}" is declared twice (checklist-entry-shape)`);
4208
+ }
4209
+ seen.add(e.id);
4210
+ if (e.element !== null && !/^[a-r]$/.test(e.element)) {
4211
+ warn('C141', `${where}: entry "${e.id}" element "${e.element}" is outside the PD-05 Β§4.4.3 vocabulary (a–r) β€” the element count can grow, confirm the extension (checklist-entry-shape)`);
4212
+ }
4213
+ }
4214
+ }
4215
+ }
3523
4216
  // C34 β€” duality-coherence: one value structure, two roles.
3524
4217
  for (const d of standard.duals ?? []) {
3525
4218
  if (d.attribute && !attrIds.has(d.attribute)) {