@primmel/primmel 1.13.0 → 1.15.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/src/check-rules.d.ts.map +1 -1
- package/dist/src/check-rules.js +86 -0
- package/dist/src/check-rules.js.map +1 -1
- package/dist/src/check.d.ts.map +1 -1
- package/dist/src/check.js +805 -0
- package/dist/src/check.js.map +1 -1
- package/dist/src/model-diff.d.ts.map +1 -1
- package/dist/src/model-diff.js +38 -0
- package/dist/src/model-diff.js.map +1 -1
- package/dist/src/ser-des/config/applicationDeclaration.d.ts +5 -0
- package/dist/src/ser-des/config/applicationDeclaration.d.ts.map +1 -0
- package/dist/src/ser-des/config/applicationDeclaration.js +102 -0
- package/dist/src/ser-des/config/applicationDeclaration.js.map +1 -0
- package/dist/src/ser-des/config/approval.d.ts.map +1 -1
- package/dist/src/ser-des/config/approval.js +56 -9
- package/dist/src/ser-des/config/approval.js.map +1 -1
- package/dist/src/ser-des/config/aspect.d.ts +5 -0
- package/dist/src/ser-des/config/aspect.d.ts.map +1 -0
- package/dist/src/ser-des/config/aspect.js +128 -0
- package/dist/src/ser-des/config/aspect.js.map +1 -0
- package/dist/src/ser-des/config/calculationContext.d.ts +5 -0
- package/dist/src/ser-des/config/calculationContext.d.ts.map +1 -0
- package/dist/src/ser-des/config/calculationContext.js +62 -0
- package/dist/src/ser-des/config/calculationContext.js.map +1 -0
- package/dist/src/ser-des/config/certificateTemplate.d.ts +5 -0
- package/dist/src/ser-des/config/certificateTemplate.d.ts.map +1 -0
- package/dist/src/ser-des/config/certificateTemplate.js +195 -0
- package/dist/src/ser-des/config/certificateTemplate.js.map +1 -0
- package/dist/src/ser-des/config/evaluationDimension.d.ts +5 -0
- package/dist/src/ser-des/config/evaluationDimension.d.ts.map +1 -0
- package/dist/src/ser-des/config/evaluationDimension.js +128 -0
- package/dist/src/ser-des/config/evaluationDimension.js.map +1 -0
- package/dist/src/ser-des/config/evaluationProfile.d.ts +5 -0
- package/dist/src/ser-des/config/evaluationProfile.d.ts.map +1 -0
- package/dist/src/ser-des/config/evaluationProfile.js +61 -0
- package/dist/src/ser-des/config/evaluationProfile.js.map +1 -0
- package/dist/src/ser-des/config/gateway.d.ts.map +1 -1
- package/dist/src/ser-des/config/gateway.js +57 -0
- package/dist/src/ser-des/config/gateway.js.map +1 -1
- package/dist/src/ser-des/config/identitySlot.d.ts +5 -0
- package/dist/src/ser-des/config/identitySlot.d.ts.map +1 -0
- package/dist/src/ser-des/config/identitySlot.js +120 -0
- package/dist/src/ser-des/config/identitySlot.js.map +1 -0
- package/dist/src/ser-des/config/index.d.ts.map +1 -1
- package/dist/src/ser-des/config/index.js +159 -0
- package/dist/src/ser-des/config/index.js.map +1 -1
- package/dist/src/ser-des/config/process.d.ts.map +1 -1
- package/dist/src/ser-des/config/process.js +46 -0
- package/dist/src/ser-des/config/process.js.map +1 -1
- package/dist/src/ser-des/config/promiseSet.d.ts +5 -0
- package/dist/src/ser-des/config/promiseSet.d.ts.map +1 -0
- package/dist/src/ser-des/config/promiseSet.js +46 -0
- package/dist/src/ser-des/config/promiseSet.js.map +1 -0
- package/dist/src/ser-des/config/selectionRules.d.ts +9 -0
- package/dist/src/ser-des/config/selectionRules.d.ts.map +1 -0
- package/dist/src/ser-des/config/selectionRules.js +372 -0
- package/dist/src/ser-des/config/selectionRules.js.map +1 -0
- package/dist/src/ser-des/config/subject.d.ts +18 -1
- package/dist/src/ser-des/config/subject.d.ts.map +1 -1
- package/dist/src/ser-des/config/subject.js +177 -61
- package/dist/src/ser-des/config/subject.js.map +1 -1
- package/dist/src/ser-des/config/testReport.d.ts +7 -0
- package/dist/src/ser-des/config/testReport.d.ts.map +1 -0
- package/dist/src/ser-des/config/testReport.js +322 -0
- package/dist/src/ser-des/config/testReport.js.map +1 -0
- package/dist/src/ser-des/config/verificationPathway.d.ts +5 -0
- package/dist/src/ser-des/config/verificationPathway.d.ts.map +1 -0
- package/dist/src/ser-des/config/verificationPathway.js +361 -0
- package/dist/src/ser-des/config/verificationPathway.js.map +1 -0
- package/dist/src/ser-des/config/workflowConfig.d.ts +5 -0
- package/dist/src/ser-des/config/workflowConfig.d.ts.map +1 -0
- package/dist/src/ser-des/config/workflowConfig.js +129 -0
- package/dist/src/ser-des/config/workflowConfig.js.map +1 -0
- package/dist/src/ser-des/config/workflowStage.d.ts +5 -0
- package/dist/src/ser-des/config/workflowStage.d.ts.map +1 -0
- package/dist/src/ser-des/config/workflowStage.js +96 -0
- package/dist/src/ser-des/config/workflowStage.js.map +1 -0
- package/dist/src/ser-des/package.d.ts +10 -0
- package/dist/src/ser-des/package.d.ts.map +1 -1
- package/dist/src/ser-des/package.js +164 -12
- package/dist/src/ser-des/package.js.map +1 -1
- package/dist/src/ser-des/parse.d.ts.map +1 -1
- package/dist/src/ser-des/parse.js +26 -0
- package/dist/src/ser-des/parse.js.map +1 -1
- package/dist/src/ser-des/types.d.ts +29 -0
- package/dist/src/ser-des/types.d.ts.map +1 -1
- package/dist/src/types/ApplicationDeclaration.d.ts +23 -0
- package/dist/src/types/ApplicationDeclaration.d.ts.map +1 -0
- package/dist/src/types/ApplicationDeclaration.js +32 -0
- package/dist/src/types/ApplicationDeclaration.js.map +1 -0
- package/dist/src/types/Approval.d.ts +5 -0
- package/dist/src/types/Approval.d.ts.map +1 -1
- package/dist/src/types/Aspect.d.ts +30 -0
- package/dist/src/types/Aspect.d.ts.map +1 -0
- package/dist/src/types/Aspect.js +41 -0
- package/dist/src/types/Aspect.js.map +1 -0
- package/dist/src/types/CalculationContext.d.ts +17 -0
- package/dist/src/types/CalculationContext.d.ts.map +1 -0
- package/dist/src/types/CalculationContext.js +34 -0
- package/dist/src/types/CalculationContext.js.map +1 -0
- package/dist/src/types/CertificateTemplate.d.ts +46 -0
- package/dist/src/types/CertificateTemplate.d.ts.map +1 -0
- package/dist/src/types/CertificateTemplate.js +46 -0
- package/dist/src/types/CertificateTemplate.js.map +1 -0
- package/dist/src/types/EvaluationDimension.d.ts +32 -0
- package/dist/src/types/EvaluationDimension.d.ts.map +1 -0
- package/dist/src/types/EvaluationDimension.js +38 -0
- package/dist/src/types/EvaluationDimension.js.map +1 -0
- package/dist/src/types/EvaluationProfile.d.ts +10 -0
- package/dist/src/types/EvaluationProfile.d.ts.map +1 -0
- package/dist/src/types/EvaluationProfile.js +25 -0
- package/dist/src/types/EvaluationProfile.js.map +1 -0
- package/dist/src/types/Gateway.d.ts +22 -0
- package/dist/src/types/Gateway.d.ts.map +1 -1
- package/dist/src/types/IdentitySlot.d.ts +24 -0
- package/dist/src/types/IdentitySlot.d.ts.map +1 -0
- package/dist/src/types/IdentitySlot.js +34 -0
- package/dist/src/types/IdentitySlot.js.map +1 -0
- package/dist/src/types/PromiseSet.d.ts +8 -0
- package/dist/src/types/PromiseSet.d.ts.map +1 -0
- package/dist/src/types/PromiseSet.js +35 -0
- package/dist/src/types/PromiseSet.js.map +1 -0
- package/dist/src/types/SelectionRules.d.ts +73 -0
- package/dist/src/types/SelectionRules.d.ts.map +1 -0
- package/dist/src/types/SelectionRules.js +55 -0
- package/dist/src/types/SelectionRules.js.map +1 -0
- package/dist/src/types/Standard.d.ts +58 -0
- package/dist/src/types/Standard.d.ts.map +1 -1
- package/dist/src/types/Subject.d.ts +31 -1
- package/dist/src/types/Subject.d.ts.map +1 -1
- package/dist/src/types/TestReport.d.ts +71 -0
- package/dist/src/types/TestReport.d.ts.map +1 -0
- package/dist/src/types/TestReport.js +73 -0
- package/dist/src/types/TestReport.js.map +1 -0
- package/dist/src/types/VerificationPathway.d.ts +63 -0
- package/dist/src/types/VerificationPathway.d.ts.map +1 -0
- package/dist/src/types/VerificationPathway.js +56 -0
- package/dist/src/types/VerificationPathway.js.map +1 -0
- package/dist/src/types/WorkflowConfig.d.ts +34 -0
- package/dist/src/types/WorkflowConfig.d.ts.map +1 -0
- package/dist/src/types/WorkflowConfig.js +47 -0
- package/dist/src/types/WorkflowConfig.js.map +1 -0
- package/dist/src/types/WorkflowStage.d.ts +22 -0
- package/dist/src/types/WorkflowStage.d.ts.map +1 -0
- package/dist/src/types/WorkflowStage.js +37 -0
- package/dist/src/types/WorkflowStage.js.map +1 -0
- package/dist/src/types/process.d.ts +21 -0
- package/dist/src/types/process.d.ts.map +1 -1
- package/dist-browser/index.mjs +2215 -1025
- package/dist-cli/primmel.mjs +8589 -5285
- 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,715 @@ 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
|
+
}
|
|
4216
|
+
// ── C142: gateway-edges-resolve (smart TODO.roadmap/40 batch 5; ────
|
|
4217
|
+
// the packages-as-SSOT epic) ─────────────────────────────────────────
|
|
4218
|
+
// The workflow routing cascade: every edge target resolves to a
|
|
4219
|
+
// declared process (per-register gated — the smart R-rule mirrors);
|
|
4220
|
+
// a gateway carrying edges declares exactly ONE default-conditioned
|
|
4221
|
+
// edge (the first-match-wins cascade needs its catch-all), and the
|
|
4222
|
+
// default edge is RECOMMENDED last (a mid-list default shadows the
|
|
4223
|
+
// edges after it — WARNING, not error: the YAML lineage carries
|
|
4224
|
+
// mid-list defaults). The conditions stay opaque strings — never
|
|
4225
|
+
// resolved (the R26 field-resolution precedent). The edge-less
|
|
4226
|
+
// gateway (a label-only declaration) carries no routing, so the
|
|
4227
|
+
// default discipline skips it.
|
|
4228
|
+
//
|
|
4229
|
+
// The workflow_stage members-resolve legs ride this rule (step 5d):
|
|
4230
|
+
// elements → the process register, approvals → the approval register,
|
|
4231
|
+
// gateways → the gateway register — per-register gated. The stage's
|
|
4232
|
+
// start_event / end_events are DOCUMENTARY tokens (dossier hazard 3,
|
|
4233
|
+
// decision (b)) — the kernel's event register is the canvas start/end
|
|
4234
|
+
// construct, a different sense — so they are never resolved.
|
|
4235
|
+
{
|
|
4236
|
+
const gwProcessIds = new Set((standard.processes ?? []).map(p => p.id));
|
|
4237
|
+
for (const g of standard.gateways ?? []) {
|
|
4238
|
+
if (g.gatewayType !== 'exclusive_gateway') {
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const edges = g.edges ?? [];
|
|
4242
|
+
const where = `exclusive_gateway ${g.id}`;
|
|
4243
|
+
edges.forEach((e, i) => {
|
|
4244
|
+
if (gwProcessIds.size > 0 && !gwProcessIds.has(e.target)) {
|
|
4245
|
+
err('C142', `${where}: edge ${i + 1} targets "${e.target}", which is not a declared process (gateway-edges-resolve)`);
|
|
4246
|
+
}
|
|
4247
|
+
});
|
|
4248
|
+
if (edges.length > 0) {
|
|
4249
|
+
const defaults = edges
|
|
4250
|
+
.map((e, i) => ({ e, i }))
|
|
4251
|
+
.filter(x => x.e.condition === 'default');
|
|
4252
|
+
if (defaults.length === 0) {
|
|
4253
|
+
err('C142', `${where}: no default edge — the first-match-wins cascade needs its catch-all (gateway-edges-resolve)`);
|
|
4254
|
+
}
|
|
4255
|
+
else if (defaults.length > 1) {
|
|
4256
|
+
err('C142', `${where}: ${defaults.length} default edges — exactly one catch-all is allowed (gateway-edges-resolve)`);
|
|
4257
|
+
}
|
|
4258
|
+
else if (defaults[0].i !== edges.length - 1) {
|
|
4259
|
+
warn('C142', `${where}: the default edge is not last — it shadows the edges after it in the first-match-wins cascade (gateway-edges-resolve)`);
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
// The workflow_stage members-resolve legs (step 5d) — the events
|
|
4264
|
+
// stay documentary, never resolved.
|
|
4265
|
+
const stageApprovalIds = new Set((standard.approvals ?? []).map(a => a.id));
|
|
4266
|
+
const stageGatewayIds = new Set((standard.gateways ?? []).map(g => g.id));
|
|
4267
|
+
for (const s of standard.workflowStages ?? []) {
|
|
4268
|
+
const where = `workflow_stage ${s.id}`;
|
|
4269
|
+
for (const el of s.elements ?? []) {
|
|
4270
|
+
if (gwProcessIds.size > 0 && !gwProcessIds.has(el)) {
|
|
4271
|
+
err('C142', `${where}: element "${el}" is not a declared process (gateway-edges-resolve)`);
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
for (const a of s.approvals ?? []) {
|
|
4275
|
+
if (stageApprovalIds.size > 0 && !stageApprovalIds.has(a)) {
|
|
4276
|
+
err('C142', `${where}: approval "${a}" is not a declared approval (gateway-edges-resolve)`);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
for (const g of s.gateways ?? []) {
|
|
4280
|
+
if (stageGatewayIds.size > 0 && !stageGatewayIds.has(g)) {
|
|
4281
|
+
err('C142', `${where}: gateway "${g}" is not a declared gateway (gateway-edges-resolve)`);
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
// ── C143: approval-references-resolve (smart TODO.roadmap/40 batch ──
|
|
4287
|
+
// 5; the packages-as-SSOT epic) ─────────────────────────────────────
|
|
4288
|
+
// The first approval rule ever (the codec was dormant — no package
|
|
4289
|
+
// declared an approval before batch 5): actor / approve_by resolve to
|
|
4290
|
+
// declared roles, the approval_record entries to declared entity-class
|
|
4291
|
+
// stores (the dataclass `store { … }` names — the workflow values are
|
|
4292
|
+
// store names like `applications`, not data_registry ids). Per-register
|
|
4293
|
+
// gated (the C58 doctrine): an empty register means the register is
|
|
4294
|
+
// not in scope, not that every reference dangles. The rule reads the
|
|
4295
|
+
// RAW reference ids (actorRef / approverRef / recordRefs) — the
|
|
4296
|
+
// resolved fields silently drop unresolvable ids.
|
|
4297
|
+
{
|
|
4298
|
+
const approvalRoleIds = new Set((standard.roles ?? []).map(r => r.id));
|
|
4299
|
+
const approvalStores = new Set();
|
|
4300
|
+
for (const c of standard.dataclasses ?? []) {
|
|
4301
|
+
if (c.store) {
|
|
4302
|
+
for (const s of c.store.split(/\s+/)) {
|
|
4303
|
+
if (s) {
|
|
4304
|
+
approvalStores.add(s);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
for (const a of standard.approvals ?? []) {
|
|
4310
|
+
const where = `approval ${a.id}`;
|
|
4311
|
+
if (a.actorRef &&
|
|
4312
|
+
approvalRoleIds.size > 0 &&
|
|
4313
|
+
!approvalRoleIds.has(a.actorRef)) {
|
|
4314
|
+
err('C143', `${where}: actor "${a.actorRef}" is not a declared role (approval-references-resolve)`);
|
|
4315
|
+
}
|
|
4316
|
+
if (a.approverRef &&
|
|
4317
|
+
approvalRoleIds.size > 0 &&
|
|
4318
|
+
!approvalRoleIds.has(a.approverRef)) {
|
|
4319
|
+
err('C143', `${where}: approver "${a.approverRef}" is not a declared role (approval-references-resolve)`);
|
|
4320
|
+
}
|
|
4321
|
+
for (const store of a.recordRefs ?? []) {
|
|
4322
|
+
if (approvalStores.size > 0 && !approvalStores.has(store)) {
|
|
4323
|
+
err('C143', `${where}: approval_record "${store}" is not a declared entity-class store (approval-references-resolve)`);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
3523
4328
|
// C34 — duality-coherence: one value structure, two roles.
|
|
3524
4329
|
for (const d of standard.duals ?? []) {
|
|
3525
4330
|
if (d.attribute && !attrIds.has(d.attribute)) {
|