@primmel/primmel 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +40 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -1
- package/dist/index.js.map +1 -1
- package/dist/src/check-allowlist.d.ts +56 -0
- package/dist/src/check-allowlist.d.ts.map +1 -0
- package/dist/src/check-allowlist.js +307 -0
- package/dist/src/check-allowlist.js.map +1 -0
- package/dist/src/check-rules.d.ts +17 -0
- package/dist/src/check-rules.d.ts.map +1 -0
- package/dist/src/check-rules.js +351 -0
- package/dist/src/check-rules.js.map +1 -0
- package/dist/src/check.d.ts +115 -0
- package/dist/src/check.d.ts.map +1 -0
- package/dist/src/check.js +4164 -0
- package/dist/src/check.js.map +1 -0
- package/dist/src/export/rdf-competency-questions.d.ts +11 -0
- package/dist/src/export/rdf-competency-questions.d.ts.map +1 -0
- package/dist/src/export/rdf-competency-questions.js +99 -0
- package/dist/src/export/rdf-competency-questions.js.map +1 -0
- package/dist/src/export/rdf-shapes.d.ts +3 -0
- package/dist/src/export/rdf-shapes.d.ts.map +1 -0
- package/dist/src/export/rdf-shapes.js +225 -0
- package/dist/src/export/rdf-shapes.js.map +1 -0
- package/dist/src/export/rdf-vocabulary.d.ts +125 -0
- package/dist/src/export/rdf-vocabulary.d.ts.map +1 -0
- package/dist/src/export/rdf-vocabulary.js +230 -0
- package/dist/src/export/rdf-vocabulary.js.map +1 -0
- package/dist/src/export/rdf.d.ts +92 -0
- package/dist/src/export/rdf.d.ts.map +1 -0
- package/dist/src/export/rdf.js +707 -0
- package/dist/src/export/rdf.js.map +1 -0
- package/dist/src/export/reqif.d.ts +81 -0
- package/dist/src/export/reqif.d.ts.map +1 -0
- package/dist/src/export/reqif.js +703 -0
- package/dist/src/export/reqif.js.map +1 -0
- package/dist/src/instance-resolution.d.ts +68 -0
- package/dist/src/instance-resolution.d.ts.map +1 -0
- package/dist/src/instance-resolution.js +239 -0
- package/dist/src/instance-resolution.js.map +1 -0
- package/dist/src/mapping-coverage.d.ts +220 -0
- package/dist/src/mapping-coverage.d.ts.map +1 -0
- package/dist/src/mapping-coverage.js +528 -0
- package/dist/src/mapping-coverage.js.map +1 -0
- package/dist/src/model-diff.d.ts +198 -0
- package/dist/src/model-diff.d.ts.map +1 -0
- package/dist/src/model-diff.js +914 -0
- package/dist/src/model-diff.js.map +1 -0
- package/dist/src/operational-state.d.ts +112 -0
- package/dist/src/operational-state.d.ts.map +1 -0
- package/dist/src/operational-state.js +162 -0
- package/dist/src/operational-state.js.map +1 -0
- package/dist/src/package-diff.d.ts +36 -0
- package/dist/src/package-diff.d.ts.map +1 -0
- package/dist/src/package-diff.js +104 -0
- package/dist/src/package-diff.js.map +1 -0
- package/dist/src/ser-des/config/acceptance.d.ts +10 -0
- package/dist/src/ser-des/config/acceptance.d.ts.map +1 -0
- package/dist/src/ser-des/config/acceptance.js +163 -0
- package/dist/src/ser-des/config/acceptance.js.map +1 -0
- package/dist/src/ser-des/config/activityArchetype.d.ts +5 -0
- package/dist/src/ser-des/config/activityArchetype.d.ts.map +1 -0
- package/dist/src/ser-des/config/activityArchetype.js +78 -0
- package/dist/src/ser-des/config/activityArchetype.js.map +1 -0
- package/dist/src/ser-des/config/artifact.d.ts +16 -0
- package/dist/src/ser-des/config/artifact.d.ts.map +1 -0
- package/dist/src/ser-des/config/artifact.js +392 -0
- package/dist/src/ser-des/config/artifact.js.map +1 -0
- package/dist/src/ser-des/config/calculation.d.ts.map +1 -1
- package/dist/src/ser-des/config/calculation.js +214 -12
- package/dist/src/ser-des/config/calculation.js.map +1 -1
- package/dist/src/ser-des/config/comment.d.ts +34 -0
- package/dist/src/ser-des/config/comment.d.ts.map +1 -0
- package/dist/src/ser-des/config/comment.js +121 -0
- package/dist/src/ser-des/config/comment.js.map +1 -0
- package/dist/src/ser-des/config/competenceKind.d.ts +8 -0
- package/dist/src/ser-des/config/competenceKind.d.ts.map +1 -0
- package/dist/src/ser-des/config/competenceKind.js +266 -0
- package/dist/src/ser-des/config/competenceKind.js.map +1 -0
- package/dist/src/ser-des/config/composition.d.ts +6 -0
- package/dist/src/ser-des/config/composition.d.ts.map +1 -0
- package/dist/src/ser-des/config/composition.js +165 -0
- package/dist/src/ser-des/config/composition.js.map +1 -0
- package/dist/src/ser-des/config/conformanceClass.d.ts +9 -0
- package/dist/src/ser-des/config/conformanceClass.d.ts.map +1 -0
- package/dist/src/ser-des/config/conformanceClass.js +128 -0
- package/dist/src/ser-des/config/conformanceClass.js.map +1 -0
- package/dist/src/ser-des/config/conformanceTest.d.ts +6 -0
- package/dist/src/ser-des/config/conformanceTest.d.ts.map +1 -0
- package/dist/src/ser-des/config/conformanceTest.js +929 -0
- package/dist/src/ser-des/config/conformanceTest.js.map +1 -0
- package/dist/src/ser-des/config/constraint.d.ts +5 -0
- package/dist/src/ser-des/config/constraint.d.ts.map +1 -0
- package/dist/src/ser-des/config/constraint.js +108 -0
- package/dist/src/ser-des/config/constraint.js.map +1 -0
- package/dist/src/ser-des/config/data.d.ts.map +1 -1
- package/dist/src/ser-des/config/data.js +105 -12
- package/dist/src/ser-des/config/data.js.map +1 -1
- package/dist/src/ser-des/config/design.d.ts +9 -0
- package/dist/src/ser-des/config/design.d.ts.map +1 -0
- package/dist/src/ser-des/config/design.js +847 -0
- package/dist/src/ser-des/config/design.js.map +1 -0
- package/dist/src/ser-des/config/discrepancyRecord.d.ts +5 -0
- package/dist/src/ser-des/config/discrepancyRecord.d.ts.map +1 -0
- package/dist/src/ser-des/config/discrepancyRecord.js +109 -0
- package/dist/src/ser-des/config/discrepancyRecord.js.map +1 -0
- package/dist/src/ser-des/config/dual.d.ts +9 -0
- package/dist/src/ser-des/config/dual.d.ts.map +1 -0
- package/dist/src/ser-des/config/dual.js +84 -0
- package/dist/src/ser-des/config/dual.js.map +1 -0
- package/dist/src/ser-des/config/field-parser.d.ts +92 -7
- package/dist/src/ser-des/config/field-parser.d.ts.map +1 -1
- package/dist/src/ser-des/config/field-parser.js +832 -29
- package/dist/src/ser-des/config/field-parser.js.map +1 -1
- package/dist/src/ser-des/config/form.d.ts.map +1 -1
- package/dist/src/ser-des/config/form.js +355 -134
- package/dist/src/ser-des/config/form.js.map +1 -1
- package/dist/src/ser-des/config/formulasUsed.d.ts +9 -0
- package/dist/src/ser-des/config/formulasUsed.d.ts.map +1 -0
- package/dist/src/ser-des/config/formulasUsed.js +126 -0
- package/dist/src/ser-des/config/formulasUsed.js.map +1 -0
- package/dist/src/ser-des/config/index.d.ts.map +1 -1
- package/dist/src/ser-des/config/index.js +180 -1
- package/dist/src/ser-des/config/index.js.map +1 -1
- package/dist/src/ser-des/config/instance.d.ts +22 -0
- package/dist/src/ser-des/config/instance.d.ts.map +1 -0
- package/dist/src/ser-des/config/instance.js +266 -0
- package/dist/src/ser-des/config/instance.js.map +1 -0
- package/dist/src/ser-des/config/invariant.d.ts +9 -0
- package/dist/src/ser-des/config/invariant.d.ts.map +1 -0
- package/dist/src/ser-des/config/invariant.js +143 -0
- package/dist/src/ser-des/config/invariant.js.map +1 -0
- package/dist/src/ser-des/config/mapProfile.d.ts.map +1 -1
- package/dist/src/ser-des/config/mapProfile.js +199 -14
- package/dist/src/ser-des/config/mapProfile.js.map +1 -1
- package/dist/src/ser-des/config/metadata.js +6 -6
- package/dist/src/ser-des/config/metadata.js.map +1 -1
- package/dist/src/ser-des/config/monitor.d.ts +9 -0
- package/dist/src/ser-des/config/monitor.d.ts.map +1 -0
- package/dist/src/ser-des/config/monitor.js +322 -0
- package/dist/src/ser-des/config/monitor.js.map +1 -0
- package/dist/src/ser-des/config/note.d.ts.map +1 -1
- package/dist/src/ser-des/config/note.js +16 -1
- package/dist/src/ser-des/config/note.js.map +1 -1
- package/dist/src/ser-des/config/packageManifest.d.ts +5 -0
- package/dist/src/ser-des/config/packageManifest.d.ts.map +1 -0
- package/dist/src/ser-des/config/packageManifest.js +349 -0
- package/dist/src/ser-des/config/packageManifest.js.map +1 -0
- package/dist/src/ser-des/config/passport.d.ts +9 -0
- package/dist/src/ser-des/config/passport.d.ts.map +1 -0
- package/dist/src/ser-des/config/passport.js +191 -0
- package/dist/src/ser-des/config/passport.js.map +1 -0
- package/dist/src/ser-des/config/process.d.ts +5 -3
- package/dist/src/ser-des/config/process.d.ts.map +1 -1
- package/dist/src/ser-des/config/process.js +915 -8
- package/dist/src/ser-des/config/process.js.map +1 -1
- package/dist/src/ser-des/config/quantity.d.ts +21 -0
- package/dist/src/ser-des/config/quantity.d.ts.map +1 -0
- package/dist/src/ser-des/config/quantity.js +114 -0
- package/dist/src/ser-des/config/quantity.js.map +1 -0
- package/dist/src/ser-des/config/quantityRegister.d.ts +9 -0
- package/dist/src/ser-des/config/quantityRegister.d.ts.map +1 -0
- package/dist/src/ser-des/config/quantityRegister.js +202 -0
- package/dist/src/ser-des/config/quantityRegister.js.map +1 -0
- package/dist/src/ser-des/config/reference.d.ts.map +1 -1
- package/dist/src/ser-des/config/reference.js +6 -0
- package/dist/src/ser-des/config/reference.js.map +1 -1
- package/dist/src/ser-des/config/referenceMaterial.d.ts +5 -0
- package/dist/src/ser-des/config/referenceMaterial.d.ts.map +1 -0
- package/dist/src/ser-des/config/referenceMaterial.js +296 -0
- package/dist/src/ser-des/config/referenceMaterial.js.map +1 -0
- package/dist/src/ser-des/config/requirement.d.ts +16 -0
- package/dist/src/ser-des/config/requirement.d.ts.map +1 -0
- package/dist/src/ser-des/config/requirement.js +579 -0
- package/dist/src/ser-des/config/requirement.js.map +1 -0
- package/dist/src/ser-des/config/role.d.ts.map +1 -1
- package/dist/src/ser-des/config/role.js +12 -0
- package/dist/src/ser-des/config/role.js.map +1 -1
- package/dist/src/ser-des/config/series.d.ts +9 -0
- package/dist/src/ser-des/config/series.d.ts.map +1 -0
- package/dist/src/ser-des/config/series.js +121 -0
- package/dist/src/ser-des/config/series.js.map +1 -0
- package/dist/src/ser-des/config/sourceDiscrepancy.d.ts +11 -0
- package/dist/src/ser-des/config/sourceDiscrepancy.d.ts.map +1 -0
- package/dist/src/ser-des/config/sourceDiscrepancy.js +83 -0
- package/dist/src/ser-des/config/sourceDiscrepancy.js.map +1 -0
- package/dist/src/ser-des/config/stateMachine.d.ts.map +1 -1
- package/dist/src/ser-des/config/stateMachine.js +189 -38
- package/dist/src/ser-des/config/stateMachine.js.map +1 -1
- package/dist/src/ser-des/config/subform.d.ts.map +1 -1
- package/dist/src/ser-des/config/subform.js +25 -33
- package/dist/src/ser-des/config/subform.js.map +1 -1
- package/dist/src/ser-des/config/subject.d.ts +58 -0
- package/dist/src/ser-des/config/subject.d.ts.map +1 -0
- package/dist/src/ser-des/config/subject.js +2093 -0
- package/dist/src/ser-des/config/subject.js.map +1 -0
- package/dist/src/ser-des/config/symbol.d.ts.map +1 -1
- package/dist/src/ser-des/config/symbol.js +130 -6
- package/dist/src/ser-des/config/symbol.js.map +1 -1
- package/dist/src/ser-des/config/table.d.ts.map +1 -1
- package/dist/src/ser-des/config/table.js +373 -7
- package/dist/src/ser-des/config/table.js.map +1 -1
- package/dist/src/ser-des/config/term.d.ts.map +1 -1
- package/dist/src/ser-des/config/term.js +107 -0
- package/dist/src/ser-des/config/term.js.map +1 -1
- package/dist/src/ser-des/config/testPointSet.d.ts +5 -0
- package/dist/src/ser-des/config/testPointSet.d.ts.map +1 -0
- package/dist/src/ser-des/config/testPointSet.js +199 -0
- package/dist/src/ser-des/config/testPointSet.js.map +1 -0
- package/dist/src/ser-des/config/testSequence.d.ts +9 -0
- package/dist/src/ser-des/config/testSequence.d.ts.map +1 -0
- package/dist/src/ser-des/config/testSequence.js +206 -0
- package/dist/src/ser-des/config/testSequence.js.map +1 -0
- package/dist/src/ser-des/config/text.d.ts +5 -0
- package/dist/src/ser-des/config/text.d.ts.map +1 -0
- package/dist/src/ser-des/config/text.js +106 -0
- package/dist/src/ser-des/config/text.js.map +1 -0
- package/dist/src/ser-des/config/twin.d.ts +31 -0
- package/dist/src/ser-des/config/twin.d.ts.map +1 -0
- package/dist/src/ser-des/config/twin.js +362 -0
- package/dist/src/ser-des/config/twin.js.map +1 -0
- package/dist/src/ser-des/config/verdict.d.ts +5 -0
- package/dist/src/ser-des/config/verdict.d.ts.map +1 -0
- package/dist/src/ser-des/config/verdict.js +159 -0
- package/dist/src/ser-des/config/verdict.js.map +1 -0
- package/dist/src/ser-des/config/viewProfile.d.ts.map +1 -1
- package/dist/src/ser-des/config/viewProfile.js +7 -0
- package/dist/src/ser-des/config/viewProfile.js.map +1 -1
- package/dist/src/ser-des/includes.d.ts.map +1 -1
- package/dist/src/ser-des/includes.js +5 -2
- package/dist/src/ser-des/includes.js.map +1 -1
- package/dist/src/ser-des/index.d.ts +12 -0
- package/dist/src/ser-des/index.d.ts.map +1 -1
- package/dist/src/ser-des/index.js +42 -0
- package/dist/src/ser-des/index.js.map +1 -1
- package/dist/src/ser-des/package.d.ts +53 -0
- package/dist/src/ser-des/package.d.ts.map +1 -0
- package/dist/src/ser-des/package.js +404 -0
- package/dist/src/ser-des/package.js.map +1 -0
- package/dist/src/ser-des/parse-block.d.ts.map +1 -1
- package/dist/src/ser-des/parse-block.js +7 -1
- package/dist/src/ser-des/parse-block.js.map +1 -1
- package/dist/src/ser-des/parse.d.ts.map +1 -1
- package/dist/src/ser-des/parse.js +45 -0
- package/dist/src/ser-des/parse.js.map +1 -1
- package/dist/src/ser-des/prm.d.ts +53 -0
- package/dist/src/ser-des/prm.d.ts.map +1 -0
- package/dist/src/ser-des/prm.js +257 -0
- package/dist/src/ser-des/prm.js.map +1 -0
- package/dist/src/ser-des/resolve.d.ts.map +1 -1
- package/dist/src/ser-des/resolve.js +1 -0
- package/dist/src/ser-des/resolve.js.map +1 -1
- package/dist/src/ser-des/tokenize.d.ts +11 -0
- package/dist/src/ser-des/tokenize.d.ts.map +1 -1
- package/dist/src/ser-des/tokenize.js +28 -1
- package/dist/src/ser-des/tokenize.js.map +1 -1
- package/dist/src/ser-des/types.d.ts +56 -1
- package/dist/src/ser-des/types.d.ts.map +1 -1
- package/dist/src/spelling.d.ts +35 -0
- package/dist/src/spelling.d.ts.map +1 -0
- package/dist/src/spelling.js +140 -0
- package/dist/src/spelling.js.map +1 -0
- package/dist/src/text-coverage.d.ts +164 -0
- package/dist/src/text-coverage.d.ts.map +1 -0
- package/dist/src/text-coverage.js +679 -0
- package/dist/src/text-coverage.js.map +1 -0
- package/dist/src/time.d.ts +55 -0
- package/dist/src/time.d.ts.map +1 -0
- package/dist/src/time.js +223 -0
- package/dist/src/time.js.map +1 -0
- package/dist/src/type-expr.d.ts +31 -0
- package/dist/src/type-expr.d.ts.map +1 -0
- package/dist/src/type-expr.js +110 -0
- package/dist/src/type-expr.js.map +1 -0
- package/dist/src/types/Acceptance.d.ts +31 -0
- package/dist/src/types/Acceptance.d.ts.map +1 -0
- package/dist/src/types/Acceptance.js +12 -0
- package/dist/src/types/Acceptance.js.map +1 -0
- package/dist/src/types/ActivityArchetype.d.ts +14 -0
- package/dist/src/types/ActivityArchetype.d.ts.map +1 -0
- package/dist/src/types/ActivityArchetype.js +17 -0
- package/dist/src/types/ActivityArchetype.js.map +1 -0
- package/dist/src/types/Artifact.d.ts +117 -0
- package/dist/src/types/Artifact.d.ts.map +1 -0
- package/dist/src/types/Artifact.js +35 -0
- package/dist/src/types/Artifact.js.map +1 -0
- package/dist/src/types/Calculation.d.ts +49 -0
- package/dist/src/types/Calculation.d.ts.map +1 -1
- package/dist/src/types/Comment.d.ts +23 -0
- package/dist/src/types/Comment.d.ts.map +1 -0
- package/dist/src/types/Comment.js +3 -0
- package/dist/src/types/Comment.js.map +1 -0
- package/dist/src/types/CompetenceKind.d.ts +70 -0
- package/dist/src/types/CompetenceKind.d.ts.map +1 -0
- package/dist/src/types/CompetenceKind.js +3 -0
- package/dist/src/types/CompetenceKind.js.map +1 -0
- package/dist/src/types/Composition.d.ts +80 -0
- package/dist/src/types/Composition.d.ts.map +1 -0
- package/dist/src/types/Composition.js +44 -0
- package/dist/src/types/Composition.js.map +1 -0
- package/dist/src/types/ConformanceClass.d.ts +22 -0
- package/dist/src/types/ConformanceClass.d.ts.map +1 -0
- package/dist/src/types/ConformanceClass.js +7 -0
- package/dist/src/types/ConformanceClass.js.map +1 -0
- package/dist/src/types/ConformanceTest.d.ts +206 -0
- package/dist/src/types/ConformanceTest.d.ts.map +1 -0
- package/dist/src/types/ConformanceTest.js +3 -0
- package/dist/src/types/ConformanceTest.js.map +1 -0
- package/dist/src/types/Constraint.d.ts +34 -0
- package/dist/src/types/Constraint.d.ts.map +1 -0
- package/dist/src/types/Constraint.js +3 -0
- package/dist/src/types/Constraint.js.map +1 -0
- package/dist/src/types/Design.d.ts +147 -0
- package/dist/src/types/Design.d.ts.map +1 -0
- package/dist/src/types/Design.js +10 -0
- package/dist/src/types/Design.js.map +1 -0
- package/dist/src/types/DiscrepancyRecord.d.ts +42 -0
- package/dist/src/types/DiscrepancyRecord.d.ts.map +1 -0
- package/dist/src/types/DiscrepancyRecord.js +3 -0
- package/dist/src/types/DiscrepancyRecord.js.map +1 -0
- package/dist/src/types/Form.d.ts +127 -1
- package/dist/src/types/Form.d.ts.map +1 -1
- package/dist/src/types/FormulasUsed.d.ts +95 -0
- package/dist/src/types/FormulasUsed.d.ts.map +1 -0
- package/dist/src/types/FormulasUsed.js +64 -0
- package/dist/src/types/FormulasUsed.js.map +1 -0
- package/dist/src/types/Instance.d.ts +105 -0
- package/dist/src/types/Instance.d.ts.map +1 -0
- package/dist/src/types/Instance.js +40 -0
- package/dist/src/types/Instance.js.map +1 -0
- package/dist/src/types/Invariant.d.ts +82 -0
- package/dist/src/types/Invariant.d.ts.map +1 -0
- package/dist/src/types/Invariant.js +50 -0
- package/dist/src/types/Invariant.js.map +1 -0
- package/dist/src/types/MapProfile.d.ts +36 -1
- package/dist/src/types/MapProfile.d.ts.map +1 -1
- package/dist/src/types/Monitor.d.ts +144 -0
- package/dist/src/types/Monitor.d.ts.map +1 -0
- package/dist/src/types/Monitor.js +74 -0
- package/dist/src/types/Monitor.js.map +1 -0
- package/dist/src/types/Note.d.ts +4 -1
- package/dist/src/types/Note.d.ts.map +1 -1
- package/dist/src/types/Package.d.ts +161 -0
- package/dist/src/types/Package.d.ts.map +1 -0
- package/dist/src/types/Package.js +3 -0
- package/dist/src/types/Package.js.map +1 -0
- package/dist/src/types/Passport.d.ts +125 -0
- package/dist/src/types/Passport.d.ts.map +1 -0
- package/dist/src/types/Passport.js +84 -0
- package/dist/src/types/Passport.js.map +1 -0
- package/dist/src/types/Quantity.d.ts +99 -0
- package/dist/src/types/Quantity.d.ts.map +1 -0
- package/dist/src/types/Quantity.js +24 -0
- package/dist/src/types/Quantity.js.map +1 -0
- package/dist/src/types/Reference.d.ts +2 -0
- package/dist/src/types/Reference.d.ts.map +1 -1
- package/dist/src/types/ReferenceMaterial.d.ts +53 -0
- package/dist/src/types/ReferenceMaterial.d.ts.map +1 -0
- package/dist/src/types/ReferenceMaterial.js +14 -0
- package/dist/src/types/ReferenceMaterial.js.map +1 -0
- package/dist/src/types/Requirement.d.ts +111 -0
- package/dist/src/types/Requirement.d.ts.map +1 -0
- package/dist/src/types/Requirement.js +10 -0
- package/dist/src/types/Requirement.js.map +1 -0
- package/dist/src/types/Role.d.ts +4 -0
- package/dist/src/types/Role.d.ts.map +1 -1
- package/dist/src/types/Series.d.ts +31 -0
- package/dist/src/types/Series.d.ts.map +1 -0
- package/dist/src/types/Series.js +14 -0
- package/dist/src/types/Series.js.map +1 -0
- package/dist/src/types/SourceDiscrepancy.d.ts +21 -0
- package/dist/src/types/SourceDiscrepancy.d.ts.map +1 -0
- package/dist/src/types/SourceDiscrepancy.js +13 -0
- package/dist/src/types/SourceDiscrepancy.js.map +1 -0
- package/dist/src/types/Standard.d.ts +68 -0
- package/dist/src/types/Standard.d.ts.map +1 -1
- package/dist/src/types/StateMachine.d.ts +43 -0
- package/dist/src/types/StateMachine.d.ts.map +1 -1
- package/dist/src/types/StateMachine.js +7 -0
- package/dist/src/types/StateMachine.js.map +1 -1
- package/dist/src/types/Subject.d.ts +432 -0
- package/dist/src/types/Subject.d.ts.map +1 -0
- package/dist/src/types/Subject.js +13 -0
- package/dist/src/types/Subject.js.map +1 -0
- package/dist/src/types/Symbol.d.ts +31 -1
- package/dist/src/types/Symbol.d.ts.map +1 -1
- package/dist/src/types/Table.d.ts +43 -0
- package/dist/src/types/Table.d.ts.map +1 -1
- package/dist/src/types/Term.d.ts +29 -0
- package/dist/src/types/Term.d.ts.map +1 -1
- package/dist/src/types/TestPointSet.d.ts +33 -0
- package/dist/src/types/TestPointSet.d.ts.map +1 -0
- package/dist/src/types/TestPointSet.js +11 -0
- package/dist/src/types/TestPointSet.js.map +1 -0
- package/dist/src/types/TestSequence.d.ts +105 -0
- package/dist/src/types/TestSequence.d.ts.map +1 -0
- package/dist/src/types/TestSequence.js +65 -0
- package/dist/src/types/TestSequence.js.map +1 -0
- package/dist/src/types/Text.d.ts +21 -0
- package/dist/src/types/Text.d.ts.map +1 -0
- package/dist/src/types/Text.js +38 -0
- package/dist/src/types/Text.js.map +1 -0
- package/dist/src/types/Time.d.ts +43 -0
- package/dist/src/types/Time.d.ts.map +1 -0
- package/dist/src/types/Time.js +14 -0
- package/dist/src/types/Time.js.map +1 -0
- package/dist/src/types/Twin.d.ts +155 -0
- package/dist/src/types/Twin.d.ts.map +1 -0
- package/dist/src/types/Twin.js +102 -0
- package/dist/src/types/Twin.js.map +1 -0
- package/dist/src/types/Verdict.d.ts +40 -0
- package/dist/src/types/Verdict.d.ts.map +1 -0
- package/dist/src/types/Verdict.js +14 -0
- package/dist/src/types/Verdict.js.map +1 -0
- package/dist/src/types/ViewProfile.d.ts +6 -0
- package/dist/src/types/ViewProfile.d.ts.map +1 -1
- package/dist/src/types/data.d.ts +27 -0
- package/dist/src/types/data.d.ts.map +1 -1
- package/dist/src/types/process.d.ts +231 -0
- package/dist/src/types/process.d.ts.map +1 -1
- package/dist-browser/index.mjs +6695 -662
- package/dist-cli/primmel.mjs +21453 -0
- package/package.json +61 -5
|
@@ -0,0 +1,4164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
3
|
+
// primmel check — cross-layer linter (Primmel v2 plan, W8).
|
|
4
|
+
//
|
|
5
|
+
// First-class package checks beyond parse-time validation:
|
|
6
|
+
// C1 attribute scope vs bind path scope (forms)
|
|
7
|
+
// C2 reference targets resolve (req targets, test targets, form
|
|
8
|
+
// conformance ids, capability req/test refs, behavior verified_by,
|
|
9
|
+
// requirement binds_to)
|
|
10
|
+
// C3 classification dimension enums (dimension ids + values referenced
|
|
11
|
+
// in applicability blocks and test subjects exist; match all|exact on
|
|
12
|
+
// a single-cardinality dimension warns — it reduces to match any)
|
|
13
|
+
// C4 store names unique (storable classes)
|
|
14
|
+
// C5 every requirement is verified — targeted by ≥1 test, provision'd
|
|
15
|
+
// by a process, or declaring a non-test verification method
|
|
16
|
+
// (refined, TODO.roadmap/17); every test targets ≥1 req
|
|
17
|
+
// C6 subject anatomy: aspect declared under the wrong family (or
|
|
18
|
+
// undeclared aspect kind) is an error (anatomy-family)
|
|
19
|
+
// C7 every has.characteristics entry names its derivation
|
|
20
|
+
// (anatomy-characteristic-derivation)
|
|
21
|
+
// C8 every does.behavior ref resolves to a declared behavior
|
|
22
|
+
// (anatomy-behavior-resolves)
|
|
23
|
+
// C9 a subject's extends target is a declared subject
|
|
24
|
+
// (subject-extends-resolves)
|
|
25
|
+
// C10 executable process: exactly one start event (process-one-start)
|
|
26
|
+
// C11 end events on every terminal path — mandatory on empty gateway
|
|
27
|
+
// branches; flow edges reference declared steps (process-terminal-end)
|
|
28
|
+
// C12 gateway edge conditions + step I/O name declared registers
|
|
29
|
+
// (process-flow-names-resolve)
|
|
30
|
+
// C13 executable steps realize the signature: OUT written (error),
|
|
31
|
+
// IN read (warning) (process-signature-realized)
|
|
32
|
+
// C14 a recurrence (flow cycle) passes through a timer event WITH a
|
|
33
|
+
// declared period — no unguarded infinite self-loops
|
|
34
|
+
// (process-timer-recurrence)
|
|
35
|
+
// C15 a timer_event declares its recurrence period — a period-less
|
|
36
|
+
// timer is not a loop guard (process-timer-period)
|
|
37
|
+
// C16 step ids are unique within one does body
|
|
38
|
+
// (process-step-ids-unique)
|
|
39
|
+
// C17 instance values respect the attribute scope discipline: stated at
|
|
40
|
+
// the declared scope or lower; sample-scope only in test_context of a
|
|
41
|
+
// sample-level instance; test_context/classification level checks
|
|
42
|
+
// (instance-scope)
|
|
43
|
+
// C18 every instance carries definition_versions pins (INV-8)
|
|
44
|
+
// (instance-version-pin)
|
|
45
|
+
// C19 instance chain links (model/group/family) resolve and are acyclic
|
|
46
|
+
// (chain-acyclic)
|
|
47
|
+
// C20 every instance's `of` resolves to a declared subject or instrument
|
|
48
|
+
// (instance-of-resolves)
|
|
49
|
+
// C21 mapping-resolves: both ends of every mapping resolve — the source
|
|
50
|
+
// is a declared component; the target's namespace matches the profile
|
|
51
|
+
// namespace and a declared `Namespace#ElementID` alias element exists
|
|
52
|
+
// C22 mapping-direction: mappings run implementation → reference only
|
|
53
|
+
// (no namespaced source; no mapping into the model's own namespace)
|
|
54
|
+
// C23 mapping-calculus-consistency: an authored coverage assertion that
|
|
55
|
+
// disagrees with the computed calculus is an error
|
|
56
|
+
// C24 import-not-mapping: an import (uses/extends) may not be expressed
|
|
57
|
+
// as a mapping, nor a mapping as an import
|
|
58
|
+
// C25 mapping-description: a mapping without description is a warning
|
|
59
|
+
// at audit strictness
|
|
60
|
+
// C26 view-read-only: a view names declared elements and reads against
|
|
61
|
+
// a declared map namespace — it never invents or edits mappings
|
|
62
|
+
// C27 uses-resolves: every uses/extends entry resolves to a package
|
|
63
|
+
// (needs the resolvePackage locator); extends → uses deprecation
|
|
64
|
+
// is a warning under the same rule
|
|
65
|
+
// C28 uses-no-redefine: a downstream package may reference upstream ids
|
|
66
|
+
// but never redeclare them
|
|
67
|
+
// C29 uses-cycle: the uses graph is acyclic
|
|
68
|
+
// C30 provides-consumed-or-waived: every provides entry is required by a
|
|
69
|
+
// downstream package or explicitly waived (warning)
|
|
70
|
+
// C31 requires-satisfied: every requires entry names a composed package
|
|
71
|
+
// id or one of its provides entries
|
|
72
|
+
// C32 inv1-no-bare-quantity: a value stated for a declared physical
|
|
73
|
+
// quantity (attribute unit, quantity_kind, or QuantityValue
|
|
74
|
+
// value_type) carries a unit — an empty-string unit token counts as
|
|
75
|
+
// bare; numeric condition-set entries carry a unit too (free-text
|
|
76
|
+
// values stay legal) — INV-1, no bare numbers (TODO.roadmap/06)
|
|
77
|
+
// C33 quantity-coherence: register integrity (unit kinds resolve, no
|
|
78
|
+
// cross-register redefinition) + comparison coherence on quantity
|
|
79
|
+
// KINDS (instance values vs attribute definitions, condition-set
|
|
80
|
+
// entries, symbols, verdicts) — mass vs time fails; unmapped units
|
|
81
|
+
// are warnings
|
|
82
|
+
// C34 duality-coherence: a dual carries ≥1 role; each stated role of a
|
|
83
|
+
// dual bound to a declared physical quantity carries a unit (or an
|
|
84
|
+
// explicit kind) — INV-1; both roles stated ⇒ same quantity kind
|
|
85
|
+
// (compatible unit); tolerance belongs to designed, uncertainty to
|
|
86
|
+
// exhibited (warnings)
|
|
87
|
+
// C35 time-format: timer-event recurrence periods are ISO 8601
|
|
88
|
+
// durations (closes task 02's deferred format check); instance
|
|
89
|
+
// values for time-typed attributes (date/datetime/duration/period)
|
|
90
|
+
// match their ISO 8601 shape
|
|
91
|
+
// C36 map-type: a `map<K, V>` field/value type is well-formed
|
|
92
|
+
// (K = string or enum id, V a valid type expression)
|
|
93
|
+
// C37 state-fires-resolve: every step `fires` names a declared
|
|
94
|
+
// transition action of the process's bound state machine
|
|
95
|
+
// (TODO.roadmap/07)
|
|
96
|
+
// C38 state-family-separation: no cross-references between the
|
|
97
|
+
// operational and lifecycle machine families — no lifecycle cascade
|
|
98
|
+
// into an operational machine, no operational cascade into a
|
|
99
|
+
// lifecycle machine, no subject has.state bound to a lifecycle
|
|
100
|
+
// machine
|
|
101
|
+
// C39 state-machine-states-referenced: every `#state` literal in a
|
|
102
|
+
// `self.state` gate resolves against the bound machine's states
|
|
103
|
+
// C40 anatomy-state-resolves: a subject's has.state names a declared
|
|
104
|
+
// state machine (family checked by C38)
|
|
105
|
+
// C41 precondition-on-violation-known: a precondition's on_violation
|
|
106
|
+
// parses as a free string; the only known outcome is "invalid"
|
|
107
|
+
// (others warn) — a state gate declared on_violation fail ALWAYS
|
|
108
|
+
// warns, naming the doctrine: a violated run-validity precondition
|
|
109
|
+
// voids the run, it never fails the instrument
|
|
110
|
+
// C42 promise-target-resolves: a promise's target is a declared
|
|
111
|
+
// characteristic (of the owning subject) or behavior
|
|
112
|
+
// C43 promise-verifiable: no verified_by declared AND no verifying
|
|
113
|
+
// requirement/test derivable (requirements/tests binding the same
|
|
114
|
+
// target) — a warning at authoring (TODO.roadmap/08)
|
|
115
|
+
// C44 promise-not-bare-value: a promise that merely RESTATES a declared
|
|
116
|
+
// attribute value (attribute target + bare quantity level, no
|
|
117
|
+
// conditions, no verification linkage) is an error — bare parameter
|
|
118
|
+
// values stay `origin: declared` attributes. Targeting an
|
|
119
|
+
// attribute/dimension with a genuine claim ABOUT it (conditioned,
|
|
120
|
+
// envelope/range, symbolic level, or verified) is legal.
|
|
121
|
+
// C45 artifact-def-contract: an artifact definition's content contract
|
|
122
|
+
// is well-formed — ≥1 named field, every field typed, no duplicate
|
|
123
|
+
// fields, media entries refine declared fields, produced_when is
|
|
124
|
+
// per_measurement | per_interval <ISO-8601 duration> | on_event
|
|
125
|
+
// <event> (TODO.roadmap/09)
|
|
126
|
+
// C46 artifact-instance-resolves: subject is.artifacts / has.
|
|
127
|
+
// artifact_instances slots resolve; a conformance test's
|
|
128
|
+
// produces_artifacts resolve; an instance's `of` resolves to a
|
|
129
|
+
// declared artifact definition; content fields ⊆ the contract;
|
|
130
|
+
// required contract fields populated; the producer (by) named;
|
|
131
|
+
// per_measurement instances link a producing run/report
|
|
132
|
+
// C47 artifact-evidence-separation: the MECE firewall — an artifact is
|
|
133
|
+
// an output OF the instrument, an EvidenceRecord is a record OF the
|
|
134
|
+
// test; no artifact contract field name doubles as a conformance-test
|
|
135
|
+
// variable/observable or form field (and vice versa)
|
|
136
|
+
// C48 characteristic-one-home: with subject characteristics declared,
|
|
137
|
+
// a `verdict` construct carrying its own `derive` defines a
|
|
138
|
+
// verdict-quantity derivation outside the primary model — an error
|
|
139
|
+
// (TODO.roadmap/10; the OIML SMART verdict-no-shadow /
|
|
140
|
+
// verdict-restatement spirit, re-pointed at the new home)
|
|
141
|
+
// C49 characteristic-behavior-link: a characteristic's behavior ref
|
|
142
|
+
// resolves to a declared behavior
|
|
143
|
+
// C50 characteristic-derivation-inputs: an ocl{…} derivation's reads
|
|
144
|
+
// resolve — self.<p> against the subject's parameters, bare names
|
|
145
|
+
// against the behavior-I/O vocabulary (symbols, attributes,
|
|
146
|
+
// subject parameters)
|
|
147
|
+
// C58 activity-kind-resolves: a process's activity_kind ids resolve
|
|
148
|
+
// against a declared activity_archetype register when one is in
|
|
149
|
+
// scope; the register's own parent references resolve within the
|
|
150
|
+
// register (TODO.roadmap/39; silent in a register-less package)
|
|
151
|
+
// C59 segregation-members-resolve: a process's segregation constraints
|
|
152
|
+
// (TODO.roadmap/39b — ISO/IEC 17065 role segregation) are
|
|
153
|
+
// well-formed: pair members resolve to declared processes (or the
|
|
154
|
+
// reserved case_personnel token), are distinct, and include the
|
|
155
|
+
// owning process
|
|
156
|
+
// C74 process-io-type-coherence (TODO.roadmap/38): typed transition
|
|
157
|
+
// boundaries — one name (signature in/out, registers) carries ONE
|
|
158
|
+
// type; two declarations of the same name whose quantity
|
|
159
|
+
// kinds/units disagree (mass vs time) make every boundary that
|
|
160
|
+
// hands the name off unsound (∘: t₁: A→B, t₂: B→C ⊢ t₂∘t₁: A→C)
|
|
161
|
+
// C75 process-flow-io-cover (TODO.roadmap/38): the step-chain dataflow
|
|
162
|
+
// covers every read — a step read (or edge-condition read) of a
|
|
163
|
+
// declared name that is neither provided (IN parameter, instance
|
|
164
|
+
// value, state) nor written on every incoming flow path is an
|
|
165
|
+
// error; a write no step reads, no condition references, and no
|
|
166
|
+
// OUT parameter names is a dead-output warning (a capture-step
|
|
167
|
+
// write lands in evidence, so it is never dead)
|
|
168
|
+
// C76 subprocess-signature-bound (TODO.roadmap/38): a `calls` step
|
|
169
|
+
// names a declared process (resolved on the composed package — a
|
|
170
|
+
// call across a `uses` boundary is checked post-merge); when the
|
|
171
|
+
// callee declares a signature, the with { in / out } block binds
|
|
172
|
+
// every IN from a caller register and maps every OUT back to one,
|
|
173
|
+
// kind-compatible on both legs
|
|
174
|
+
// C60 serve-targets-resolve (TODO.roadmap/32, doctrine §14.12): every
|
|
175
|
+
// serve binding names a declared aspect of the owning subject
|
|
176
|
+
// ([level.]{parameters|classification|test_context}.<key>, a bare
|
|
177
|
+
// attribute/characteristic/dimension name, state,
|
|
178
|
+
// environmental_context) and a declared, unambiguous endpoint
|
|
179
|
+
// operation of value-channel kind (query | subscribe); the served
|
|
180
|
+
// aspect and the operation payload are unit/quantity-kind coherent.
|
|
181
|
+
// The endpoint legs ride the same rule: an operation's serves names
|
|
182
|
+
// resolve as subject aspects, its does names as declared behaviors
|
|
183
|
+
// C61 payload-schema-quantity: every endpoint operation declares a
|
|
184
|
+
// known kind (query | subscribe | invoke) and a payload schema that
|
|
185
|
+
// is a QuantityValue per INV-1 — quantity kind, unit (the register's
|
|
186
|
+
// dimensionless id when non-quantity), and timestamp true (a value
|
|
187
|
+
// without a time is not evidence, §14.3)
|
|
188
|
+
// C62 access-scope-covers-serves: every endpoint operation is covered
|
|
189
|
+
// by exactly one access scope (public | registered | authority);
|
|
190
|
+
// access entries name declared operations of the endpoint (§14.12:
|
|
191
|
+
// every endpoint operation has an access scope)
|
|
192
|
+
// C63 freshness-required-on-live-bindings: a serve binding without
|
|
193
|
+
// fresh_within is an error (§14.12 — no stale semantics, no live
|
|
194
|
+
// binding); the window must parse (shorthand 5s/1min/1h or ISO 8601
|
|
195
|
+
// with fixed-length components)
|
|
196
|
+
// C64 endpoint-profile-resolves: an endpoint's profile names a declared
|
|
197
|
+
// connector_profile or a built-in (rest_json, mqtt, opc_ua,
|
|
198
|
+
// file_drop) — the model is protocol-neutral; profiles bind
|
|
199
|
+
// protocols (§14.4)
|
|
200
|
+
// C65 monitor-subject-resolves (TODO.roadmap/34, doctrine §14.5/§14.12):
|
|
201
|
+
// a monitor's over set is non-empty and every ref names a declared
|
|
202
|
+
// subject (a monitor watching nothing judges nothing)
|
|
203
|
+
// C66 monitor-trigger-wellformed: at least one trigger — without
|
|
204
|
+
// triggers "continuous" has no clock (§14.5 step 1); a timer's every
|
|
205
|
+
// window parses (the freshness-window syntax); a signal names its
|
|
206
|
+
// signal; a change names an aspect resolving against a monitored
|
|
207
|
+
// subject (the serve aspect vocabulary — ONE resolver)
|
|
208
|
+
// C67 monitor-evaluate-resolves: evaluate refs resolve to
|
|
209
|
+
// requirements/promises applicable to the monitored subjects
|
|
210
|
+
// (§14.12) — all / applicable_to(…) expand per twin at runtime;
|
|
211
|
+
// explicit refs resolve (requirement ids against the package's
|
|
212
|
+
// requirements, promise ids against the monitored subjects'
|
|
213
|
+
// promises)
|
|
214
|
+
// C68 monitor-fail-escalation: a monitor without an escalation path
|
|
215
|
+
// for `fail` is a warning (§14.12, verbatim) — pass accrues history;
|
|
216
|
+
// fail/invalid must act
|
|
217
|
+
// C69 monitor-escalation-resolves: escalation outcomes are verdict
|
|
218
|
+
// outcomes (pass | fail | indeterminate | invalid); actions are
|
|
219
|
+
// notify | flag_certificate | open_service_case; a notify names a
|
|
220
|
+
// declared role
|
|
221
|
+
// C70 monitor-emit-sinks: evidence and verdicts streams are each
|
|
222
|
+
// emitted exactly once with a named sink (§14.5 step 6: appended to
|
|
223
|
+
// the workspace — facts only; permanent)
|
|
224
|
+
//
|
|
225
|
+
// ── Coverage audits (TODO.roadmap/17, concept doc §11.5) ──
|
|
226
|
+
// The aspect↔requirement↔test↔form↔verdict closure. The requirement→test
|
|
227
|
+
// link is C5 (base, refined): a requirement is covered when a
|
|
228
|
+
// conformance test targets it, a process validate_provision's it, or it
|
|
229
|
+
// declares a non-test verification method (definitional / examination /
|
|
230
|
+
// inspection / documentation / computational / deferred — deliberate
|
|
231
|
+
// exclusions are recorded, not gaps); an undeclared gap is always a
|
|
232
|
+
// finding. The remaining links are the coverage family:
|
|
233
|
+
// C51 coverage-test-evidence: a conformance test leaving no evidence —
|
|
234
|
+
// no form declares it (conformance_process), no result_forms, no
|
|
235
|
+
// report_rows, and no form covers a requirement it targets —
|
|
236
|
+
// following inherits_from chains (AUDIT level, warning)
|
|
237
|
+
// C52 coverage-form-judgment: a test-evidence form (binding a
|
|
238
|
+
// conformance process) with no pass_fail, no field
|
|
239
|
+
// evaluation/verdict, and no requirements binding — evidence
|
|
240
|
+
// gathered and never weighed (AUDIT level, warning)
|
|
241
|
+
// C53 coverage-uses-bound: every requirement limit.uses input is
|
|
242
|
+
// bound — unprefixed ids resolve against attributes / symbols /
|
|
243
|
+
// requirements / dimensions / behaviors ('load' is the declared
|
|
244
|
+
// per-load-point free variable, deep-audit-r60 F6); observable: and
|
|
245
|
+
// formula: prefixes resolve against symbols and calculations
|
|
246
|
+
// (NORMAL level, error)
|
|
247
|
+
// C54 coverage-lookup-table-exists: table:/profile: uses prefixes and
|
|
248
|
+
// calculation lookup profiles resolve to declared tables/profiles
|
|
249
|
+
// (lookupMPE/lookupProfile targets) (NORMAL level, error)
|
|
250
|
+
// C55 coverage-budget: the package's declared coverage_budget caps its
|
|
251
|
+
// C51/C52 warning count — exceeded is an error, slack warns (the
|
|
252
|
+
// allowlist only shrinks)
|
|
253
|
+
// C56 allowlist-malformed: every allowlist entry names a real rule id,
|
|
254
|
+
// a non-empty match glob, a reason, and an audit_ref (§11.9)
|
|
255
|
+
// C57 allowlist-stale: an allowlist entry whose rule is active at the
|
|
256
|
+
// current level but matches no issue is an error — the data was
|
|
257
|
+
// fixed, the entry must die (§11.3)
|
|
258
|
+
//
|
|
259
|
+
// ── Edition lifecycle (TODO.roadmap/28, doctrine ch. 13 §13.4/§13.7) ──
|
|
260
|
+
// Versioning relations live on the package manifest, never in subject
|
|
261
|
+
// models; editions are packagings orthogonal to the core.
|
|
262
|
+
// C77 edition-status: a current/preview edition packages the edition
|
|
263
|
+
// register's newest entry (the status enum itself is a parser error,
|
|
264
|
+
// like `kind`)
|
|
265
|
+
// C78 edition-validity-window: the manifest validity window is
|
|
266
|
+
// well-formed ISO 8601; `to` not before `from`
|
|
267
|
+
// C79 edition-supersedes-resolves: supersedes/replaces targets are
|
|
268
|
+
// well-formed URNs, never the package itself, resolve against the
|
|
269
|
+
// edition register (same-document targets), and the supersedes graph
|
|
270
|
+
// over sibling manifests is acyclic
|
|
271
|
+
// C80 edition-pin-resolves (INV-8): every instance's
|
|
272
|
+
// definition_versions pin resolves against the package's edition
|
|
273
|
+
// register — an unresolvable pin breaks re-execution (§13.5)
|
|
274
|
+
// C81 product-maps-resolves: a product reference package declares its
|
|
275
|
+
// manufacturer, product designation, and the standards-reference
|
|
276
|
+
// packages it maps to (maps_to) — the register resolves and agrees
|
|
277
|
+
// with the model's map profiles (TODO.roadmap/36, doctrine ch. 15)
|
|
278
|
+
// C82 product-unmapped-promises: an IS promise of a product reference
|
|
279
|
+
// package that no mapping sources is a brochure claim — a warning
|
|
280
|
+
// at authoring (doctrine ch. 15 §15.2)
|
|
281
|
+
// C83 abstract-import-pinned: a uses edge to a product reference
|
|
282
|
+
// package carries a version pin (uses { <id>@<edition> }) that
|
|
283
|
+
// resolves against the product's edition register — no unpinned
|
|
284
|
+
// reference consumption (doctrine ch. 15 §15.3)
|
|
285
|
+
//
|
|
286
|
+
// Levels (TODO.roadmap/17): the DEFAULT level runs the normal-level
|
|
287
|
+
// rules at their catalog severities. --audit additionally runs the
|
|
288
|
+
// audit-level rules (C25, C51, C52) and enforces the coverage budget.
|
|
289
|
+
// --strict promotes every warning to an error — EXCEPT KNOWN
|
|
290
|
+
// (allowlisted) issues and budget-covered C51/C52 warnings (the budget
|
|
291
|
+
// is their allowance). The package allowlist lives in
|
|
292
|
+
// <dir>/.primmel-allowlist.prl (see check-allowlist.ts); the rule
|
|
293
|
+
// catalog lives in check-rules.ts (`primmel check --rules`).
|
|
294
|
+
//
|
|
295
|
+
// Manifest-only stopgap (TODO.roadmap/05, until task 13 retires the
|
|
296
|
+
// textual-include transport): when checkPackage runs WITHOUT a
|
|
297
|
+
// resolvePackage locator, full composition cannot run — but the shipped
|
|
298
|
+
// manifests still declare uses/requires/provides, so a dangling uses id
|
|
299
|
+
// would go completely unreported. checkManifestResolution() closes that
|
|
300
|
+
// hole: it parses the package's manifest plus the manifests of its
|
|
301
|
+
// SIBLING package directories (id match) and verifies, WITHOUT composing
|
|
302
|
+
// content, that every uses/extends entry resolves, the graph is acyclic,
|
|
303
|
+
// and every requires entry names a closure package id or one of its
|
|
304
|
+
// provides entries. Issues report under the same C27/C29/C31 codes with
|
|
305
|
+
// a "(manifest-only …)" marker.
|
|
306
|
+
// Each check returns issues: { check, severity, message }.
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
308
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
309
|
+
exports.checkPackage = checkPackage;
|
|
310
|
+
exports.checkManifestResolution = checkManifestResolution;
|
|
311
|
+
exports.checkSpellingCodes = checkSpellingCodes;
|
|
312
|
+
exports.checkEditionLifecycle = checkEditionLifecycle;
|
|
313
|
+
const fs_1 = require("fs");
|
|
314
|
+
const path_1 = require("path");
|
|
315
|
+
const package_1 = require("./ser-des/package");
|
|
316
|
+
const prm_1 = require("./ser-des/prm");
|
|
317
|
+
const mapping_coverage_1 = require("./mapping-coverage");
|
|
318
|
+
const time_1 = require("./time");
|
|
319
|
+
const type_expr_1 = require("./type-expr");
|
|
320
|
+
const model_diff_1 = require("./model-diff");
|
|
321
|
+
const Twin_1 = require("./types/Twin");
|
|
322
|
+
const Composition_1 = require("./types/Composition");
|
|
323
|
+
const Monitor_1 = require("./types/Monitor");
|
|
324
|
+
const Passport_1 = require("./types/Passport");
|
|
325
|
+
const TestSequence_1 = require("./types/TestSequence");
|
|
326
|
+
const FormulasUsed_1 = require("./types/FormulasUsed");
|
|
327
|
+
const tokenize_1 = require("./ser-des/tokenize");
|
|
328
|
+
const operational_state_1 = require("./operational-state");
|
|
329
|
+
const check_rules_1 = require("./check-rules");
|
|
330
|
+
const check_allowlist_1 = require("./check-allowlist");
|
|
331
|
+
const text_coverage_1 = require("./text-coverage");
|
|
332
|
+
const spelling_1 = require("./spelling");
|
|
333
|
+
const BIND_SCOPES = {
|
|
334
|
+
'family.parameters': 'family',
|
|
335
|
+
'group.parameters': 'group',
|
|
336
|
+
'model.parameters': 'model',
|
|
337
|
+
'sample.test_context': 'sample',
|
|
338
|
+
};
|
|
339
|
+
/** Identity paths (not AttributeDefinitions — fields on the entity classes). */
|
|
340
|
+
const IDENTITY_PREFIXES = [
|
|
341
|
+
'model.identity.',
|
|
342
|
+
'model.model_designation',
|
|
343
|
+
'model.hardware_revision',
|
|
344
|
+
'family.family_designation',
|
|
345
|
+
'manufacturer.',
|
|
346
|
+
'sample.serial_number',
|
|
347
|
+
'sample.sample_number',
|
|
348
|
+
'sample.status',
|
|
349
|
+
'sample.condition',
|
|
350
|
+
'application.',
|
|
351
|
+
'test_report.',
|
|
352
|
+
'test_request.',
|
|
353
|
+
];
|
|
354
|
+
/** Enum-name aliases: a classification path may use the enum name while the
|
|
355
|
+
* attribute/dimension id differs (app resolves both spellings). */
|
|
356
|
+
const DIM_ALIASES = {
|
|
357
|
+
humidity_class: 'humidity_symbol',
|
|
358
|
+
};
|
|
359
|
+
/** A bare numeric literal (same shape as ser-des/config/quantity.ts NUMERIC). */
|
|
360
|
+
const NUMERIC_TOKEN = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
361
|
+
function isIdentityPath(path) {
|
|
362
|
+
return IDENTITY_PREFIXES.some(p => path.startsWith(p));
|
|
363
|
+
}
|
|
364
|
+
function attrId(standard, id) {
|
|
365
|
+
return (standard.attributeDefinitions ?? []).find((a) => a.id === id);
|
|
366
|
+
}
|
|
367
|
+
function checkPackage(dir, options = {}) {
|
|
368
|
+
const issues = [];
|
|
369
|
+
const err = (check, message) => issues.push({ check, severity: 'error', message });
|
|
370
|
+
const warn = (check, message) => issues.push({ check, severity: 'warning', message });
|
|
371
|
+
// ── C27–C31: `uses` composition (TODO.roadmap/05) ──────────────────
|
|
372
|
+
// Composition errors are HARD load errors (the merge cannot proceed),
|
|
373
|
+
// so the loader throws CompositionError; the linter reports them as
|
|
374
|
+
// rule-identified issues instead. Composition WARNINGS (unconsumed
|
|
375
|
+
// provides, extends deprecation) come back in the load's issue list.
|
|
376
|
+
// The parse-time duplicate-id issues ride the same list — surfaced
|
|
377
|
+
// under C96 (the E11 review finding: every id-keyed collection's
|
|
378
|
+
// uniqueness delegation — C84 constraints, C94 formulas-used — is
|
|
379
|
+
// invisible if the linter drops them here).
|
|
380
|
+
const COMPOSITION_RULE_CHECKS = {
|
|
381
|
+
'uses-resolves': 'C27',
|
|
382
|
+
'uses-no-redefine': 'C28',
|
|
383
|
+
'uses-cycle': 'C29',
|
|
384
|
+
'requires-satisfied': 'C31',
|
|
385
|
+
};
|
|
386
|
+
let standard;
|
|
387
|
+
try {
|
|
388
|
+
const loaded = (0, package_1.loadPackageWithIssues)(dir, {
|
|
389
|
+
resolvePackage: options.resolvePackage,
|
|
390
|
+
});
|
|
391
|
+
standard = loaded.standard;
|
|
392
|
+
for (const i of loaded.issues) {
|
|
393
|
+
if (i.code === 'provides-unconsumed') {
|
|
394
|
+
warn('C30', i.message);
|
|
395
|
+
}
|
|
396
|
+
else if (i.code === 'extends-deprecated') {
|
|
397
|
+
warn('C27', i.message);
|
|
398
|
+
}
|
|
399
|
+
else if (i.code === 'duplicate-id') {
|
|
400
|
+
err('C96', `${i.message} (duplicate-id)`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
if (e instanceof package_1.CompositionError) {
|
|
406
|
+
err(COMPOSITION_RULE_CHECKS[e.rule] ?? 'C27', e.message);
|
|
407
|
+
return issues;
|
|
408
|
+
}
|
|
409
|
+
throw e;
|
|
410
|
+
}
|
|
411
|
+
// Manifest-only stopgap: without a locator the composition rules cannot
|
|
412
|
+
// run, but the declared imports can still be verified against the
|
|
413
|
+
// manifests of the SIBLING package directories — a dangling uses id or
|
|
414
|
+
// an unsatisfiable requires token is caught in CI without composing
|
|
415
|
+
// content (TODO.roadmap/05, until task 13 retires textual includes).
|
|
416
|
+
if (!options.resolvePackage) {
|
|
417
|
+
const m = standard.packageManifest;
|
|
418
|
+
if (m && ((0, package_1.effectiveUses)(m).length > 0 || (m.requires ?? []).length > 0)) {
|
|
419
|
+
issues.push(...checkManifestResolution(dir));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── C77–C80: edition lifecycle (TODO.roadmap/28, doctrine §13.4/§13.7) ──
|
|
423
|
+
// Versioning relations live on the package manifest, never in subject
|
|
424
|
+
// models: status/register coherence (C77), validity windows (C78),
|
|
425
|
+
// supersedes/replaces resolution + acyclicity across sibling manifests
|
|
426
|
+
// (C79), and the INV-8 pin — every instance's definition_versions
|
|
427
|
+
// resolves against the package's edition register (C80).
|
|
428
|
+
issues.push(...checkEditionLifecycle(dir, standard));
|
|
429
|
+
// ── C89: ISO 24229 spelling codes (TODO.roadmap/25, doctrine ch. 10) ──
|
|
430
|
+
issues.push(...checkSpellingCodes(standard));
|
|
431
|
+
const reqIds = new Set((standard.requirements ?? []).map((r) => r.id));
|
|
432
|
+
const testIds = new Set((standard.conformanceTests ?? []).map((t) => t.id));
|
|
433
|
+
const attrIds = new Set((standard.attributeDefinitions ?? []).map((a) => a.id));
|
|
434
|
+
const behaviorIds = new Set((standard.behaviors ?? []).map((b) => b.id));
|
|
435
|
+
const subjectIds = new Set((standard.subjects ?? []).map((s) => s.id));
|
|
436
|
+
// Observables live in the symbols registry, not the attribute layer —
|
|
437
|
+
// binds_to / limit.uses may reference them (e.g. sample.test_context
|
|
438
|
+
// quantities that are measured test outputs).
|
|
439
|
+
const symbolIds = new Set((standard.symbols ?? []).map((s) => s.id));
|
|
440
|
+
const dimIds = new Map();
|
|
441
|
+
const dimCardinality = new Map();
|
|
442
|
+
for (const inst of standard.instruments ?? []) {
|
|
443
|
+
for (const d of inst.dimensions ?? []) {
|
|
444
|
+
dimIds.set(d.id, new Set(d.values.map(v => v.id)));
|
|
445
|
+
if (d.cardinality) {
|
|
446
|
+
dimCardinality.set(d.id, d.cardinality);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// C1 — bind path scope vs attribute scope
|
|
451
|
+
for (const form of standard.forms ?? []) {
|
|
452
|
+
const checkField = (f) => {
|
|
453
|
+
if (f.bind) {
|
|
454
|
+
const parts = String(f.bind).split('.');
|
|
455
|
+
const prefix = parts.slice(0, 2).join('.');
|
|
456
|
+
const id = parts[2];
|
|
457
|
+
const scope = BIND_SCOPES[prefix];
|
|
458
|
+
if (isIdentityPath(String(f.bind))) {
|
|
459
|
+
// identity binds are always valid
|
|
460
|
+
}
|
|
461
|
+
else if (scope) {
|
|
462
|
+
if (!attrIds.has(id)) {
|
|
463
|
+
err('C1', `form ${form.id}: bind "${f.bind}" — attribute "${id}" not defined`);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const a = attrId(standard, id);
|
|
467
|
+
if (a && a.scope && a.scope !== scope) {
|
|
468
|
+
err('C1', `form ${form.id}: bind "${f.bind}" — attribute scope "${a.scope}" ≠ path scope "${scope}"`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
(f.fields ?? []).forEach(checkField);
|
|
474
|
+
};
|
|
475
|
+
(form.fields ?? []).forEach(checkField);
|
|
476
|
+
}
|
|
477
|
+
// C2 — reference targets resolve
|
|
478
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
479
|
+
for (const target of t.targets ?? []) {
|
|
480
|
+
if (!reqIds.has(target)) {
|
|
481
|
+
err('C2', `conformance test ${t.id}: target "${target}" is not a declared requirement`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (t.inheritsFrom && !testIds.has(t.inheritsFrom)) {
|
|
485
|
+
err('C2', `conformance test ${t.id}: inherits_from "${t.inheritsFrom}" not found`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const form of standard.forms ?? []) {
|
|
489
|
+
for (const pid of form.conformanceProcessIds ??
|
|
490
|
+
(form.conformanceProcessId ? [form.conformanceProcessId] : [])) {
|
|
491
|
+
if (!testIds.has(pid)) {
|
|
492
|
+
err('C2', `form ${form.id}: conformance_process "${pid}" not found`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const c of standard.capabilities ?? []) {
|
|
497
|
+
for (const r of c.satisfiesRequirements ?? []) {
|
|
498
|
+
if (!reqIds.has(r)) {
|
|
499
|
+
err('C2', `capability ${c.id}: satisfies_requirements "${r}" not found`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const t of c.verifiedByTests ?? []) {
|
|
503
|
+
if (!testIds.has(t)) {
|
|
504
|
+
err('C2', `capability ${c.id}: verified_by_tests "${t}" not found`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const b of standard.behaviors ?? []) {
|
|
509
|
+
for (const t of b.verifiedBy ?? []) {
|
|
510
|
+
if (!testIds.has(t)) {
|
|
511
|
+
err('C2', `behavior ${b.id}: verified_by "${t}" not found`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// A verdict's behavior link (TODO.roadmap/10) resolves to a declared
|
|
516
|
+
// behavior — the transport form of C49 characteristic-behavior-link.
|
|
517
|
+
for (const v of standard.verdicts ?? []) {
|
|
518
|
+
if (v.behavior && !behaviorIds.has(v.behavior)) {
|
|
519
|
+
err('C2', `verdict ${v.id}: behavior "${v.behavior}" is not a declared behavior`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const r of standard.requirements ?? []) {
|
|
523
|
+
for (const p of r.bindsTo ?? []) {
|
|
524
|
+
const parts = String(p).split('.');
|
|
525
|
+
const id = parts[2];
|
|
526
|
+
if (!id) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (parts[1] === 'classification') {
|
|
530
|
+
// classification paths reference DIMENSION ids (with enum-name aliases)
|
|
531
|
+
const dim = DIM_ALIASES[id] ?? id;
|
|
532
|
+
if (!dimIds.has(dim) && !attrIds.has(id) && !attrIds.has(dim)) {
|
|
533
|
+
err('C2', `requirement ${r.id}: binds_to "${p}" — dimension "${id}" not declared`);
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (parts[1] === 'behaviors') {
|
|
538
|
+
// behavior paths reference the behaviors registry (documentary binds)
|
|
539
|
+
if (!behaviorIds.has(id)) {
|
|
540
|
+
err('C2', `requirement ${r.id}: binds_to "${p}" — behavior "${id}" not declared`);
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (isIdentityPath(String(p))) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!attrIds.has(id) && !symbolIds.has(id)) {
|
|
548
|
+
err('C2', `requirement ${r.id}: binds_to "${p}" — attribute "${id}" not defined`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// NOTE: limit.uses resolution is the coverage family's anchoring leg
|
|
552
|
+
// — C53 (coverage-uses-bound), below. C2 keeps the binds_to paths.
|
|
553
|
+
}
|
|
554
|
+
// C3 — dimension ids + values exist
|
|
555
|
+
// test_subject reserves two non-dimension annotation keys (cc.yaml keeps
|
|
556
|
+
// the block open via additionalProperties): `component` names the
|
|
557
|
+
// instrument component under test (e.g. R 91's ego speed meter),
|
|
558
|
+
// `description` annotates the subject block. Neither is a dimension.
|
|
559
|
+
const TEST_SUBJECT_RESERVED = new Set(['component', 'description']);
|
|
560
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
561
|
+
for (const [dim, value] of Object.entries(t.testSubject ?? {})) {
|
|
562
|
+
if (TEST_SUBJECT_RESERVED.has(dim)) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (!dimIds.has(dim)) {
|
|
566
|
+
err('C3', `conformance test ${t.id}: test_subject dimension "${dim}" not declared`);
|
|
567
|
+
}
|
|
568
|
+
else if (!dimIds.get(dim).has(String(value))) {
|
|
569
|
+
err('C3', `conformance test ${t.id}: test_subject ${dim}="${value}" not in the dimension's values`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const rc of standard.requirementClasses ?? []) {
|
|
574
|
+
void rc;
|
|
575
|
+
}
|
|
576
|
+
for (const r of standard.requirements ?? []) {
|
|
577
|
+
for (const a of r.applicability ?? []) {
|
|
578
|
+
if (!dimIds.has(a.dimension)) {
|
|
579
|
+
err('C3', `requirement ${r.id}: applicability dimension "${a.dimension}" not declared`);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
for (const v of a.values ?? []) {
|
|
583
|
+
if (!dimIds.get(a.dimension).has(v)) {
|
|
584
|
+
err('C3', `requirement ${r.id}: applicability ${a.dimension}="${v}" not in the dimension's values`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Universal/exact matching is the set-dimension semantics — on a
|
|
588
|
+
// single-cardinality dimension the selection holds at most one
|
|
589
|
+
// value, so `match all` and `match exact` both reduce to plain
|
|
590
|
+
// membership (equivalent to the default any). Flag the redundant
|
|
591
|
+
// mode so the data author either sets the dimension's cardinality
|
|
592
|
+
// or drops the mode.
|
|
593
|
+
if ((a.match === 'all' || a.match === 'exact') &&
|
|
594
|
+
dimCardinality.get(a.dimension) !== 'set') {
|
|
595
|
+
warn('C3', `requirement ${r.id}: applicability ${a.dimension} match ${a.match} on a single-cardinality dimension — only meaningful on set dimensions ('all' reduces to 'any'; a multi-value 'exact' is unsatisfiable on one value)`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// C4 — store names unique
|
|
601
|
+
const stores = new Map();
|
|
602
|
+
for (const c of standard.dataclasses ?? []) {
|
|
603
|
+
if (c.store) {
|
|
604
|
+
if (stores.has(c.store)) {
|
|
605
|
+
err('C4', `store "${c.store}" declared by both ${stores.get(c.store)} and ${c.id}`);
|
|
606
|
+
}
|
|
607
|
+
stores.set(c.store, c.id);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// C5 — coverage: req ⇄ test linkage (refined, TODO.roadmap/17). A
|
|
611
|
+
// requirement is COVERED — no finding — when any of:
|
|
612
|
+
// (a) a conformance test targets it (the base law);
|
|
613
|
+
// (b) a process validate_provision's it (the OIML-CS `/req/cs/*`
|
|
614
|
+
// provisions are verified BY THE PROCESS, never by a conformance
|
|
615
|
+
// test — the abstract-process doctrine of the smart linker's
|
|
616
|
+
// R19);
|
|
617
|
+
// (c) it declares a non-test verification method (definitional /
|
|
618
|
+
// examination / inspection / documentation / computational /
|
|
619
|
+
// deferred) — a deliberate exclusion is a legitimate answer,
|
|
620
|
+
// recorded, not a gap (concept doc §11.5).
|
|
621
|
+
// An undeclared gap is always a finding (§11.9): a requirement with no
|
|
622
|
+
// targeting test AND no recorded non-test verification warns. The
|
|
623
|
+
// reverse leg is unchanged: a test targeting nothing warns.
|
|
624
|
+
const covered = new Set();
|
|
625
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
626
|
+
for (const target of t.targets ?? []) {
|
|
627
|
+
covered.add(target);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const provisioned = new Set();
|
|
631
|
+
for (const p of standard.processes ?? []) {
|
|
632
|
+
for (const id of p.provisionRefs ?? []) {
|
|
633
|
+
provisioned.add(id);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const NON_TEST_VERIFICATION = new Set([
|
|
637
|
+
'definitional',
|
|
638
|
+
'examination',
|
|
639
|
+
'inspection',
|
|
640
|
+
'documentation',
|
|
641
|
+
'computational',
|
|
642
|
+
'deferred',
|
|
643
|
+
// The smart schema's verification-method enum (data/schemas/rc.yaml)
|
|
644
|
+
// additionally allows 'implicit' — reconcile the two vocabularies so
|
|
645
|
+
// an implicit-verified requirement is a recorded exclusion, never a
|
|
646
|
+
// false-positive C5 (unused today).
|
|
647
|
+
'implicit',
|
|
648
|
+
]);
|
|
649
|
+
for (const r of standard.requirements ?? []) {
|
|
650
|
+
if (covered.has(r.id) || provisioned.has(r.id)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (NON_TEST_VERIFICATION.has(r.verificationMethod)) {
|
|
654
|
+
continue; // deliberate exclusion — recorded, not a gap
|
|
655
|
+
}
|
|
656
|
+
warn('C5', `requirement ${r.id}: no conformance test targets it and it declares no non-test verification — an undeclared coverage gap is always a finding (req-test-coverage)`);
|
|
657
|
+
}
|
|
658
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
659
|
+
if ((t.targets ?? []).length === 0) {
|
|
660
|
+
warn('C5', `conformance test ${t.id}: targets no requirement`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// C6 — subject anatomy: every aspect declared under exactly one family
|
|
664
|
+
// (anatomy-family). The parser records wrong-family and undeclared
|
|
665
|
+
// aspect keys on Subject.misplacedAspects; value-level markers catch
|
|
666
|
+
// the two classic content confusions (a test-dependent value among
|
|
667
|
+
// design parameters; a condition tier among attributes). NOTE: those
|
|
668
|
+
// two value-level checks (the test-dependent marker in
|
|
669
|
+
// design_parameters; condition-tier keys in has.attributes) are
|
|
670
|
+
// HEURISTICS beyond the spec's literal wording — the spec mandates
|
|
671
|
+
// aspect→family placement only.
|
|
672
|
+
const ASPECT_FAMILY = {
|
|
673
|
+
metadata: 'is',
|
|
674
|
+
provenance: 'is',
|
|
675
|
+
structure: 'is',
|
|
676
|
+
design_parameters: 'is',
|
|
677
|
+
designed_conditions: 'is',
|
|
678
|
+
promises: 'is',
|
|
679
|
+
artifacts: 'is',
|
|
680
|
+
endpoint: 'is',
|
|
681
|
+
attributes: 'has',
|
|
682
|
+
dimensions: 'has',
|
|
683
|
+
state: 'has',
|
|
684
|
+
characteristics: 'has',
|
|
685
|
+
environmental_context: 'has',
|
|
686
|
+
artifact_instances: 'has',
|
|
687
|
+
serve: 'has',
|
|
688
|
+
behavior: 'does',
|
|
689
|
+
};
|
|
690
|
+
const CONDITION_TIERS = new Set(['reference', 'rated', 'limiting']);
|
|
691
|
+
for (const s of standard.subjects ?? []) {
|
|
692
|
+
for (const m of s.misplacedAspects ?? []) {
|
|
693
|
+
const home = ASPECT_FAMILY[m.aspect];
|
|
694
|
+
if (home && home !== m.family) {
|
|
695
|
+
err('C6', `subject ${s.id}: aspect "${m.aspect}" declared under family "${m.family}" — it belongs to "${home}" (anatomy-family)`);
|
|
696
|
+
}
|
|
697
|
+
else if (!home) {
|
|
698
|
+
err('C6', `subject ${s.id}: undeclared aspect "${m.aspect}" in family "${m.family}" (anatomy-family)`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
for (const [k, v] of Object.entries(s.is.designParameters ?? {})) {
|
|
702
|
+
if (/test[-_]dependent/.test(v)) {
|
|
703
|
+
err('C6', `subject ${s.id}: design parameter "${k}" is test-dependent — exhibited values belong to has.attributes (anatomy-family)`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
for (const k of Object.keys(s.has.attributes ?? {})) {
|
|
707
|
+
if (CONDITION_TIERS.has(k)) {
|
|
708
|
+
err('C6', `subject ${s.id}: attribute "${k}" is a condition tier — designed tiers belong to is.designed_conditions (anatomy-family)`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// C7 — every characteristic names its derivation from behavior I/O
|
|
712
|
+
// (anatomy-characteristic-derivation).
|
|
713
|
+
for (const [k, c] of Object.entries(s.has.characteristics ?? {})) {
|
|
714
|
+
if (!c.derivation) {
|
|
715
|
+
err('C7', `subject ${s.id}: characteristic "${k}" names no derivation from behavior I/O (anatomy-characteristic-derivation)`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// C8 — every does behavior resolves to a declared behavior
|
|
719
|
+
// (anatomy-behavior-resolves).
|
|
720
|
+
for (const b of s.does.behaviors ?? []) {
|
|
721
|
+
if (!behaviorIds.has(b)) {
|
|
722
|
+
err('C8', `subject ${s.id}: behavior "${b}" is not a declared behavior (anatomy-behavior-resolves)`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// C9 — a subject's extends target resolves to a declared subject id
|
|
726
|
+
// (subject-extends-resolves). Successfully merged chains clear
|
|
727
|
+
// `extends` at resolve time, so a surviving link is exactly an
|
|
728
|
+
// unresolved one (missing parent, or a broken cycle).
|
|
729
|
+
if (s.extends && !subjectIds.has(s.extends)) {
|
|
730
|
+
warn('C9', `subject ${s.id}: extends "${s.extends}" is not a declared subject (subject-extends-resolves)`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// ── C42–C44: promises (TODO.roadmap/08) ──
|
|
734
|
+
// A promise is a manufacturer claim on a characteristic or a behavior —
|
|
735
|
+
// possibly envelope-shaped, possibly conditional — that evaluation
|
|
736
|
+
// verifies and the certificate prints as promises-as-verified (doctrine
|
|
737
|
+
// ch. 02 §2.3, ch. 15 §15.2). It is NOT a declared attribute value: a
|
|
738
|
+
// claim stated as one bare parameter value stays an `origin: declared`
|
|
739
|
+
// attribute (C44 enforces the distinctness).
|
|
740
|
+
//
|
|
741
|
+
// Verification linkage (C43): a promise may declare `verified_by`
|
|
742
|
+
// requirement/test ids (their resolution is C2, above). When it does
|
|
743
|
+
// not, the linter DERIVES candidates — requirements/tests binding the
|
|
744
|
+
// same target: a requirement binds a characteristic target when its
|
|
745
|
+
// limit.uses or a binds_to leaf names the characteristic or its symbol,
|
|
746
|
+
// and binds a behavior target via a `…behaviors.<id>` binds_to path; a
|
|
747
|
+
// test binds a target when it targets a binding requirement, names the
|
|
748
|
+
// target in an acceptance criterion/variable/observable, or (behaviors)
|
|
749
|
+
// is listed in the behavior's own verified_by. No verification declared
|
|
750
|
+
// or derived is a warning at authoring.
|
|
751
|
+
const requirementBindsPromiseTarget = (r, names) => {
|
|
752
|
+
for (const u of r.limit?.uses ?? []) {
|
|
753
|
+
const leaf = u.split('.').pop() ?? u;
|
|
754
|
+
if (names.has(u) || names.has(leaf)) {
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const p of r.bindsTo ?? []) {
|
|
759
|
+
const leaf = String(p).split('.').pop() ?? '';
|
|
760
|
+
if (names.has(leaf)) {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
};
|
|
766
|
+
const testBindsPromiseTarget = (t, names, boundReqs) => {
|
|
767
|
+
for (const target of t.targets ?? []) {
|
|
768
|
+
if (boundReqs.has(target)) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const ac of t.acceptanceCriteria ?? []) {
|
|
773
|
+
if (names.has(ac.item)) {
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (const v of t.variables ?? []) {
|
|
778
|
+
if (names.has(v.name)) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
for (const o of t.observables ?? []) {
|
|
783
|
+
if (names.has(o.name)) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
};
|
|
789
|
+
for (const s of standard.subjects ?? []) {
|
|
790
|
+
// Characteristics of THIS subject (extends chains already merged):
|
|
791
|
+
// name → symbol, both spellings legal in promise targets/derivations.
|
|
792
|
+
const characteristicSymbols = new Map();
|
|
793
|
+
for (const [k, c] of Object.entries(s.has.characteristics ?? {})) {
|
|
794
|
+
characteristicSymbols.set(k, c.symbol ?? '');
|
|
795
|
+
}
|
|
796
|
+
// Bare-value targets (C44): declared attribute values — attribute
|
|
797
|
+
// definitions, the subject's design parameters, its has.attributes.
|
|
798
|
+
const bareValueTargets = new Set([
|
|
799
|
+
...attrIds,
|
|
800
|
+
...Object.keys(s.is.designParameters ?? {}),
|
|
801
|
+
...Object.keys(s.has.attributes ?? {}),
|
|
802
|
+
]);
|
|
803
|
+
for (const p of s.is.promises ?? []) {
|
|
804
|
+
const label = p.id ||
|
|
805
|
+
(p.statement.length > 40
|
|
806
|
+
? p.statement.slice(0, 37) + '…'
|
|
807
|
+
: p.statement);
|
|
808
|
+
// C2 — declared verified_by ids must resolve to a requirement or a
|
|
809
|
+
// conformance test (same discipline as behavior verified_by).
|
|
810
|
+
for (const v of p.verifiedBy ?? []) {
|
|
811
|
+
if (!reqIds.has(v) && !testIds.has(v)) {
|
|
812
|
+
err('C2', `subject ${s.id}: promise "${label}" verified_by "${v}" is not a declared requirement or conformance test`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (!p.target) {
|
|
816
|
+
// Statement-only shorthand: no target to derive verification
|
|
817
|
+
// against — unless verified_by is declared, the promise is
|
|
818
|
+
// unverifiable (C43, warning at authoring).
|
|
819
|
+
if ((p.verifiedBy ?? []).length === 0) {
|
|
820
|
+
warn('C43', `subject ${s.id}: promise "${label}" has no target and no verified_by — it is unverifiable; give it a characteristic/behavior target or declare the verifying requirement/test (promise-verifiable)`);
|
|
821
|
+
}
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const isCharacteristic = characteristicSymbols.has(p.target);
|
|
825
|
+
const isBehavior = behaviorIds.has(p.target);
|
|
826
|
+
if (!isCharacteristic && !isBehavior) {
|
|
827
|
+
if (bareValueTargets.has(p.target)) {
|
|
828
|
+
// C44 (narrowed) — the crime is RESTATING a bare value as a
|
|
829
|
+
// claim, not TARGETING an attribute: fire only when the promise
|
|
830
|
+
// merely restates the declared attribute value — a bare
|
|
831
|
+
// quantity level with no conditions and no verification
|
|
832
|
+
// linkage. A genuine claim ABOUT an attribute (conditioned,
|
|
833
|
+
// envelope/range, symbolic level, or verified) is legal; a
|
|
834
|
+
// characteristic/behavior name wins the collision (above).
|
|
835
|
+
const restatesBareValue = p.level?.kind === 'quantity' &&
|
|
836
|
+
p.conditions === '' &&
|
|
837
|
+
(p.verifiedBy ?? []).length === 0;
|
|
838
|
+
if (restatesBareValue) {
|
|
839
|
+
err('C44', `subject ${s.id}: promise "${label}" 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)`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
// C42 — the target resolves to nothing the subject can claim.
|
|
844
|
+
err('C42', `subject ${s.id}: promise "${label}" target "${p.target}" is not a declared characteristic or behavior (promise-target-resolves)`);
|
|
845
|
+
}
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if ((p.verifiedBy ?? []).length > 0) {
|
|
849
|
+
continue; // verification declared — C43 silent
|
|
850
|
+
}
|
|
851
|
+
// C43 — derive the verifying requirements/tests from the target.
|
|
852
|
+
const names = new Set([p.target]);
|
|
853
|
+
if (isCharacteristic) {
|
|
854
|
+
const sym = characteristicSymbols.get(p.target);
|
|
855
|
+
if (sym) {
|
|
856
|
+
names.add(sym);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const boundReqs = new Set();
|
|
860
|
+
for (const r of standard.requirements ?? []) {
|
|
861
|
+
if (requirementBindsPromiseTarget(r, names)) {
|
|
862
|
+
boundReqs.add(r.id);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const boundTests = new Set();
|
|
866
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
867
|
+
if (testBindsPromiseTarget(t, names, boundReqs)) {
|
|
868
|
+
boundTests.add(t.id);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (isBehavior) {
|
|
872
|
+
const b = (standard.behaviors ?? []).find(x => x.id === p.target);
|
|
873
|
+
for (const tid of b?.verifiedBy ?? []) {
|
|
874
|
+
boundTests.add(tid);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (boundReqs.size === 0 && boundTests.size === 0) {
|
|
878
|
+
warn('C43', `subject ${s.id}: promise "${label}" declares no verified_by and no requirement/test binds its target "${p.target}" — declare the verifying requirement or test (promise-verifiable)`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// ── C45–C47: artifacts (TODO.roadmap/09 — gap audit G2) ──
|
|
883
|
+
// An artifact is a required OUTPUT OF THE SUBJECT (the instrument) — not
|
|
884
|
+
// a record of the test. artifact_definition (IS: content contract +
|
|
885
|
+
// produced-when) is referenced from the subject's is.artifacts slot;
|
|
886
|
+
// artifact_instance (HAS/evidence: one produced output checked against
|
|
887
|
+
// the contract) from has.artifact_instances.
|
|
888
|
+
const artifactDefs = new Map((standard.artifactDefinitions ?? []).map(d => [d.id, d]));
|
|
889
|
+
const artifactInstIds = new Set((standard.artifactInstances ?? []).map(a => a.id));
|
|
890
|
+
// C45 — artifact-def-contract: the content contract is well-formed.
|
|
891
|
+
for (const d of standard.artifactDefinitions ?? []) {
|
|
892
|
+
const c = d.contentContract;
|
|
893
|
+
if (c.fields.length === 0) {
|
|
894
|
+
err('C45', `artifact definition ${d.id}: content contract declares no fields — a definition must contract SOME content (artifact-def-contract)`);
|
|
895
|
+
}
|
|
896
|
+
const seenFields = new Set();
|
|
897
|
+
for (const f of c.fields) {
|
|
898
|
+
if (seenFields.has(f.name)) {
|
|
899
|
+
err('C45', `artifact definition ${d.id}: duplicate contract field "${f.name}" (artifact-def-contract)`);
|
|
900
|
+
}
|
|
901
|
+
seenFields.add(f.name);
|
|
902
|
+
if (!f.type) {
|
|
903
|
+
err('C45', `artifact definition ${d.id}: contract field "${f.name}" has no type — fields are named AND typed (artifact-def-contract)`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
for (const m of c.media) {
|
|
907
|
+
if (!seenFields.has(m.field)) {
|
|
908
|
+
err('C45', `artifact definition ${d.id}: media entry refines "${m.field}", which is not a declared contract field (artifact-def-contract)`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const pw = d.producedWhen;
|
|
912
|
+
const KNOWN_PRODUCED_WHEN = new Set([
|
|
913
|
+
'per_measurement',
|
|
914
|
+
'per_interval',
|
|
915
|
+
'on_event',
|
|
916
|
+
]);
|
|
917
|
+
if (!KNOWN_PRODUCED_WHEN.has(pw.kind)) {
|
|
918
|
+
err('C45', `artifact definition ${d.id}: produced_when "${pw.kind}" is not one of per_measurement | per_interval | on_event (artifact-def-contract)`);
|
|
919
|
+
}
|
|
920
|
+
else if (pw.kind === 'per_interval' && !(0, time_1.isDuration)(pw.interval ?? '')) {
|
|
921
|
+
err('C45', `artifact definition ${d.id}: produced_when per_interval "${pw.interval ?? ''}" is not an ISO-8601 duration (artifact-def-contract)`);
|
|
922
|
+
}
|
|
923
|
+
else if (pw.kind === 'on_event' && !pw.event) {
|
|
924
|
+
err('C45', `artifact definition ${d.id}: produced_when on_event names no event (artifact-def-contract)`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// C46 — artifact-instance-resolves: subject slots resolve; an instance's
|
|
928
|
+
// `of` resolves; its content satisfies the contract; produced-when is
|
|
929
|
+
// satisfiable by the linking run/report.
|
|
930
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
931
|
+
for (const a of t.producesArtifacts ?? []) {
|
|
932
|
+
if (!artifactDefs.has(a)) {
|
|
933
|
+
err('C46', `conformance test ${t.id}: produces_artifacts "${a}" is not a declared artifact_definition (artifact-instance-resolves)`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (const s of standard.subjects ?? []) {
|
|
938
|
+
for (const a of s.is.artifacts ?? []) {
|
|
939
|
+
if (!artifactDefs.has(a)) {
|
|
940
|
+
err('C46', `subject ${s.id}: is.artifacts entry "${a}" is not a declared artifact_definition (artifact-instance-resolves)`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
for (const a of s.has.artifactInstances ?? []) {
|
|
944
|
+
if (!artifactInstIds.has(a)) {
|
|
945
|
+
err('C46', `subject ${s.id}: has.artifact_instances entry "${a}" is not a declared artifact_instance (artifact-instance-resolves)`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
for (const a of standard.artifactInstances ?? []) {
|
|
950
|
+
const def = artifactDefs.get(a.of);
|
|
951
|
+
if (!def) {
|
|
952
|
+
err('C46', `artifact instance ${a.id}: of "${a.of}" is not a declared artifact_definition (artifact-instance-resolves)`);
|
|
953
|
+
continue; // no contract to check the content against
|
|
954
|
+
}
|
|
955
|
+
if (!a.by) {
|
|
956
|
+
err('C46', `artifact instance ${a.id}: no producer (by) — an artifact instance is unattributable without its producing subject (artifact-instance-resolves)`);
|
|
957
|
+
}
|
|
958
|
+
const contractFields = new Map(def.contentContract.fields.map(f => [f.name, f]));
|
|
959
|
+
for (const key of Object.keys(a.content)) {
|
|
960
|
+
if (!contractFields.has(key)) {
|
|
961
|
+
err('C46', `artifact instance ${a.id}: content field "${key}" is not in the contract of ${def.id} (artifact-instance-resolves)`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (const [name, f] of contractFields) {
|
|
965
|
+
if (!f.optional && !(name in a.content)) {
|
|
966
|
+
err('C46', `artifact instance ${a.id}: required contract field "${name}" of ${def.id} is not populated (artifact-instance-resolves)`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// produced-when leg: per_measurement means the instance exists BY a
|
|
970
|
+
// measurement — an instance linking no run/report cannot demonstrate
|
|
971
|
+
// the produced-when rule is satisfied.
|
|
972
|
+
if (def.producedWhen.kind === 'per_measurement' && a.links.length === 0) {
|
|
973
|
+
err('C46', `artifact instance ${a.id}: ${def.id} is produced per_measurement but the instance links no run/report — link the producing measurement run or report (artifact-instance-resolves)`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// C47 — artifact-evidence-separation (the MECE firewall): an artifact is
|
|
977
|
+
// an output OF the instrument; an EvidenceRecord is a record OF the test.
|
|
978
|
+
// No artifact contract field name may double as a test-run record slot
|
|
979
|
+
// (conformance-test variable/observable, form field) — a collision means
|
|
980
|
+
// the instrument's output is being duplicated into an evidence-record
|
|
981
|
+
// shape (or test-run evidence is being passed off as the artifact).
|
|
982
|
+
const evidenceSlots = new Map(); // name → where declared
|
|
983
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
984
|
+
for (const v of t.variables ?? []) {
|
|
985
|
+
if (v.name && !evidenceSlots.has(v.name)) {
|
|
986
|
+
evidenceSlots.set(v.name, `a variable of conformance test ${t.id}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const o of t.observables ?? []) {
|
|
990
|
+
if (o.name && !evidenceSlots.has(o.name)) {
|
|
991
|
+
evidenceSlots.set(o.name, `an observable of conformance test ${t.id}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const collectFormFieldNames = (fields, formId) => {
|
|
996
|
+
for (const f of fields) {
|
|
997
|
+
if (f.name && !evidenceSlots.has(f.name)) {
|
|
998
|
+
evidenceSlots.set(f.name, `a field of form ${formId}`);
|
|
999
|
+
}
|
|
1000
|
+
collectFormFieldNames(f.fields ?? [], formId);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
for (const form of standard.forms ?? []) {
|
|
1004
|
+
collectFormFieldNames(form.fields ?? [], form.id);
|
|
1005
|
+
}
|
|
1006
|
+
for (const d of standard.artifactDefinitions ?? []) {
|
|
1007
|
+
for (const f of d.contentContract.fields) {
|
|
1008
|
+
const where = evidenceSlots.get(f.name);
|
|
1009
|
+
if (where) {
|
|
1010
|
+
err('C47', `artifact definition ${d.id}: contract field "${f.name}" is also ${where} — an artifact is an output OF the instrument, not a record OF the test; keep artifact content out of evidence-record shapes (and vice versa) (artifact-evidence-separation)`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// ── C48–C50: characteristics (TODO.roadmap/10) ──────────────────────
|
|
1015
|
+
// Characteristics are the quantitative interface of the subject
|
|
1016
|
+
// (doctrine ch. 02 §2.7): DEFINED in the primary model (the subject's
|
|
1017
|
+
// has.characteristics), referenced everywhere else — promises claim
|
|
1018
|
+
// them, requirements constrain them, tests compute them, verdicts
|
|
1019
|
+
// judge them.
|
|
1020
|
+
// C48 — characteristic-one-home: once a standard declares subject
|
|
1021
|
+
// characteristics (the primary-model register), a `verdict` construct
|
|
1022
|
+
// carrying its own `derive` defines a verdict-quantity derivation
|
|
1023
|
+
// OUTSIDE the primary model — the one-home violation the OIML SMART
|
|
1024
|
+
// linker's verdict-no-shadow / verdict-restatement rules guard,
|
|
1025
|
+
// re-pointed at the new home. Packages without subject characteristics
|
|
1026
|
+
// (the v2 instrument trees, whose verdict registry is the
|
|
1027
|
+
// specification-side transport of model/characteristics.yaml) stay
|
|
1028
|
+
// silent: the rule fires only where a primary-model characteristic
|
|
1029
|
+
// register exists.
|
|
1030
|
+
const anySubjectCharacteristics = (standard.subjects ?? []).some(s => Object.keys(s.has.characteristics ?? {}).length > 0);
|
|
1031
|
+
if (anySubjectCharacteristics) {
|
|
1032
|
+
for (const v of standard.verdicts ?? []) {
|
|
1033
|
+
if (v.derive) {
|
|
1034
|
+
err('C48', `verdict ${v.id}: carries a derivation outside the primary model — a verdict quantity derives ONCE in the subject's has.characteristics and is referenced everywhere else (characteristic-one-home)`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// C49 — characteristic-behavior-link: a characteristic's behavior ref
|
|
1039
|
+
// resolves to a declared behavior (closing the
|
|
1040
|
+
// behavior→I/O→characteristic chain).
|
|
1041
|
+
for (const s of standard.subjects ?? []) {
|
|
1042
|
+
for (const [k, c] of Object.entries(s.has.characteristics ?? {})) {
|
|
1043
|
+
if (c.behavior && !behaviorIds.has(c.behavior)) {
|
|
1044
|
+
err('C49', `subject ${s.id}: characteristic "${k}" behavior "${c.behavior}" is not a declared behavior (characteristic-behavior-link)`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// C50 — characteristic-derivation-inputs: the identifiers an ocl{…}
|
|
1049
|
+
// derivation reads resolve to the subject's quantitative vocabulary —
|
|
1050
|
+
// self.<p> reads against the subject's parameters (design parameters,
|
|
1051
|
+
// has.attributes, attribute definitions); bare names against the
|
|
1052
|
+
// behavior-I/O vocabulary (the standard's symbols — the observables
|
|
1053
|
+
// measured of the behavior — plus attribute definitions and the
|
|
1054
|
+
// subject's parameters). Prose derivations are not machine-checkable
|
|
1055
|
+
// and are skipped.
|
|
1056
|
+
const OCL_KEYWORDS = new Set([
|
|
1057
|
+
'self',
|
|
1058
|
+
'and',
|
|
1059
|
+
'or',
|
|
1060
|
+
'xor',
|
|
1061
|
+
'not',
|
|
1062
|
+
'implies',
|
|
1063
|
+
'true',
|
|
1064
|
+
'false',
|
|
1065
|
+
'null',
|
|
1066
|
+
'if',
|
|
1067
|
+
'then',
|
|
1068
|
+
'else',
|
|
1069
|
+
'endif',
|
|
1070
|
+
'let',
|
|
1071
|
+
'in',
|
|
1072
|
+
'invalid',
|
|
1073
|
+
]);
|
|
1074
|
+
const derivationReads = (derivation) => {
|
|
1075
|
+
const body = derivation
|
|
1076
|
+
.trim()
|
|
1077
|
+
.replace(/^ocl\{/, '')
|
|
1078
|
+
.replace(/\}$/, '');
|
|
1079
|
+
const selfReads = [];
|
|
1080
|
+
const bareReads = [];
|
|
1081
|
+
for (const m of body.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
|
|
1082
|
+
const id = m[0];
|
|
1083
|
+
if (OCL_KEYWORDS.has(id)) {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const idx = m.index ?? 0;
|
|
1087
|
+
const rest = body.slice(idx + id.length);
|
|
1088
|
+
if (rest.trimStart().startsWith('(')) {
|
|
1089
|
+
continue;
|
|
1090
|
+
} // function call
|
|
1091
|
+
const before = body.slice(0, idx);
|
|
1092
|
+
if (before.endsWith('self.')) {
|
|
1093
|
+
selfReads.push(id);
|
|
1094
|
+
}
|
|
1095
|
+
else if (before.endsWith('.')) {
|
|
1096
|
+
continue; // navigation segment into a resolved root
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
bareReads.push(id);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return { selfReads, bareReads };
|
|
1103
|
+
};
|
|
1104
|
+
for (const s of standard.subjects ?? []) {
|
|
1105
|
+
const subjectParams = new Set([
|
|
1106
|
+
...Object.keys(s.is.designParameters ?? {}),
|
|
1107
|
+
...Object.keys(s.has.attributes ?? {}),
|
|
1108
|
+
...attrIds,
|
|
1109
|
+
]);
|
|
1110
|
+
const ioVocabulary = new Set([
|
|
1111
|
+
...symbolIds,
|
|
1112
|
+
...attrIds,
|
|
1113
|
+
...Object.keys(s.is.designParameters ?? {}),
|
|
1114
|
+
...Object.keys(s.has.attributes ?? {}),
|
|
1115
|
+
]);
|
|
1116
|
+
for (const [k, c] of Object.entries(s.has.characteristics ?? {})) {
|
|
1117
|
+
if (!/^ocl\{[\s\S]*\}$/.test(c.derivation.trim())) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const { selfReads, bareReads } = derivationReads(c.derivation);
|
|
1121
|
+
for (const id of selfReads) {
|
|
1122
|
+
if (!subjectParams.has(id)) {
|
|
1123
|
+
err('C50', `subject ${s.id}: characteristic "${k}" derivation reads self.${id}, which is not a declared subject parameter (design parameter, has.attribute, or attribute definition) — in \`${c.derivation}\` (characteristic-derivation-inputs)`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
for (const id of bareReads) {
|
|
1127
|
+
if (!ioVocabulary.has(id)) {
|
|
1128
|
+
err('C50', `subject ${s.id}: characteristic "${k}" derivation reads "${id}", which is not a declared symbol, attribute, or subject parameter (behavior I/O) — in \`${c.derivation}\` (characteristic-derivation-inputs)`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// ── C58: activity-kind-resolves (TODO.roadmap/39) ─────────────────
|
|
1134
|
+
// A process's `activity_kind { … }` classifies it against the ISO/IEC
|
|
1135
|
+
// 17000 functional-approach activity taxonomy (selection; determination
|
|
1136
|
+
// — testing/inspection/audit/validation/verification/peer assessment;
|
|
1137
|
+
// review; decision; attestation — declaration/certification/
|
|
1138
|
+
// accreditation; surveillance …). Classification, not inheritance —
|
|
1139
|
+
// multi-kind is deliberate (ISO/IEC 17065 §7.4 "evaluation" = selection
|
|
1140
|
+
// + determination). Every tagged kind must resolve against a declared
|
|
1141
|
+
// activity_archetype register WHEN ONE IS IN SCOPE (composed via uses);
|
|
1142
|
+
// the rule stays silent in a register-less package — classification is
|
|
1143
|
+
// opt-in and a register-less tree cannot adjudicate. The register's own
|
|
1144
|
+
// `parent` references must likewise resolve to declared archetypes.
|
|
1145
|
+
// Mirror of the OIML SMART linker's R23 activity-kind-resolves.
|
|
1146
|
+
const activityArchetypeIds = new Set((standard.activityArchetypes ?? []).map(a => a.id));
|
|
1147
|
+
if (activityArchetypeIds.size > 0) {
|
|
1148
|
+
// The register's own parent references resolve within the register.
|
|
1149
|
+
for (const a of standard.activityArchetypes ?? []) {
|
|
1150
|
+
if (a.parent !== '' && !activityArchetypeIds.has(a.parent)) {
|
|
1151
|
+
err('C58', `activity_archetype ${a.id}: parent "${a.parent}" is not a declared activity archetype (activity-kind-resolves)`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
for (const p of standard.processes ?? []) {
|
|
1155
|
+
for (const kind of p.activityKinds ?? []) {
|
|
1156
|
+
if (!activityArchetypeIds.has(kind)) {
|
|
1157
|
+
err('C58', `process ${p.id}: activity_kind "${kind}" is not a declared activity archetype (activity-kind-resolves)`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// ── C59: segregation-members-resolve (TODO.roadmap/39b) ────────────
|
|
1163
|
+
// A process's `segregation { constraint … }` declares the ISO/IEC 17065
|
|
1164
|
+
// role-segregation constraints as first-class structure (7.5.1 reviewer
|
|
1165
|
+
// ∉ evaluation, 7.6.2 decider ∉ evaluation, 7.13.5 complaint resolution
|
|
1166
|
+
// ∉ case activities, 7.13.6/4.2.10 consultancy bars). Well-formedness:
|
|
1167
|
+
// (a) every pair member resolves to a declared process id or the
|
|
1168
|
+
// reserved `case_personnel` token (the personnel involved in the
|
|
1169
|
+
// certification activities of the case a complaint relates to); (b) the
|
|
1170
|
+
// two members are distinct; (c) the owning process is a member of its
|
|
1171
|
+
// own pair; (d) the kind is declared. Pair members are process ids,
|
|
1172
|
+
// never roles — a scheme may bind one role to evaluation, review AND
|
|
1173
|
+
// decision, so the norms quantify over process involvement. Per-
|
|
1174
|
+
// assignment runtime enforcement is the platform's business (task 44).
|
|
1175
|
+
// Mirror of the OIML SMART linker's R24 abstract-process-segregation.
|
|
1176
|
+
{
|
|
1177
|
+
const SEGREGATION_CASE_PERSONNEL = 'case_personnel';
|
|
1178
|
+
const segregationProcessIds = new Set((standard.processes ?? []).map(p => p.id));
|
|
1179
|
+
for (const p of standard.processes ?? []) {
|
|
1180
|
+
for (const s of p.segregation ?? []) {
|
|
1181
|
+
if (s.kind !== 'case_personnel_disjoint' &&
|
|
1182
|
+
s.kind !== 'consultancy_bar') {
|
|
1183
|
+
err('C59', `process ${p.id}: segregation constraint "${s.id}" declares kind "${s.kind || '(none)'}" — valid kinds: case_personnel_disjoint, consultancy_bar (segregation-members-resolve)`);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (s.kind === 'case_personnel_disjoint') {
|
|
1187
|
+
if (s.pair.length !== 2) {
|
|
1188
|
+
err('C59', `process ${p.id}: segregation constraint "${s.id}" is case_personnel_disjoint but declares ${s.pair.length} pair members — exactly two are required (segregation-members-resolve)`);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const [a, b] = s.pair;
|
|
1192
|
+
if (a === b) {
|
|
1193
|
+
err('C59', `process ${p.id}: segregation constraint "${s.id}" declares "${a}" disjoint from itself — the two personnel sets must be distinct (segregation-members-resolve)`);
|
|
1194
|
+
}
|
|
1195
|
+
for (const member of [a, b]) {
|
|
1196
|
+
if (member !== SEGREGATION_CASE_PERSONNEL &&
|
|
1197
|
+
!segregationProcessIds.has(member)) {
|
|
1198
|
+
err('C59', `process ${p.id}: segregation constraint "${s.id}" names pair member "${member}", which is not a declared process (nor the reserved "${SEGREGATION_CASE_PERSONNEL}" token) (segregation-members-resolve)`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (a !== p.id && b !== p.id) {
|
|
1202
|
+
err('C59', `process ${p.id}: segregation constraint "${s.id}" is declared on process "${p.id}" but neither pair member is "${p.id}" — the constrained process owns its segregation constraints (segregation-members-resolve)`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
// ── C84: constraint-shape (TODO.roadmap/51 — BUG.R60-SSOT gap 7) ─────
|
|
1209
|
+
// The subject-intrinsic constraint's DECLARATION shape — the kernel
|
|
1210
|
+
// mirror of the OIML SMART constraints.yaml schema
|
|
1211
|
+
// (data/schemas/constraints.yaml; the resolution legs stay smart-side
|
|
1212
|
+
// as linker rule R32 constraint-references): the stereotype is always
|
|
1213
|
+
// «inv» (the subject's declaration-level invariant; requirements are
|
|
1214
|
+
// the «req» counterpart), the check is ONE OCL boolean expression, the
|
|
1215
|
+
// violation_meaning is REQUIRED (the judgment records what a violation
|
|
1216
|
+
// means, never a bare id), and on_violation is invalid (void
|
|
1217
|
+
// measurement) or indeterminate (the declaration cannot be judged) —
|
|
1218
|
+
// never a fail. A declared source names both doc and clause (clause-URN
|
|
1219
|
+
// provenance). Duplicate constraint ids are the parse-time duplicate-id
|
|
1220
|
+
// rule (surfaced as C96), not a C84 leg.
|
|
1221
|
+
{
|
|
1222
|
+
const OCL_CHECK = /^ocl\{[\s\S]*\}$/;
|
|
1223
|
+
for (const c of standard.constraints ?? []) {
|
|
1224
|
+
if (c.stereotype !== 'inv') {
|
|
1225
|
+
err('C84', `constraint ${c.id}: stereotype "${c.stereotype || '(none)'}" — a constraint is always «inv», the subject's declaration-level invariant (constraint-shape)`);
|
|
1226
|
+
}
|
|
1227
|
+
if (!OCL_CHECK.test(c.check)) {
|
|
1228
|
+
err('C84', `constraint ${c.id}: check is not one OCL boolean expression ocl{…} over the subject's declared anatomy (constraint-shape)`);
|
|
1229
|
+
}
|
|
1230
|
+
if (!c.violationMeaning || c.violationMeaning.trim() === '') {
|
|
1231
|
+
err('C84', `constraint ${c.id}: violation_meaning is required — the invalidated judgment records what the violation means, never a bare id (constraint-shape)`);
|
|
1232
|
+
}
|
|
1233
|
+
if (c.onViolation !== 'invalid' && c.onViolation !== 'indeterminate') {
|
|
1234
|
+
err('C84', `constraint ${c.id}: on_violation "${c.onViolation || '(none)'}" — valid: invalid (void measurement), indeterminate (the declaration cannot be judged); never a fail (constraint-shape)`);
|
|
1235
|
+
}
|
|
1236
|
+
if (c.source && (!c.source.doc || !c.source.clause)) {
|
|
1237
|
+
err('C84', `constraint ${c.id}: source names ${!c.source.doc ? 'no doc' : 'no clause'} — clause-URN provenance carries both doc and clause (constraint-shape)`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// ── The serve aspect vocabulary (TODO.roadmap/32/34) — shared by the
|
|
1242
|
+
// twin checks (C60) and the monitor checks (C66): ONE resolver, derived
|
|
1243
|
+
// from the subject's declared aspects.
|
|
1244
|
+
const TWIN_CHAIN_LEVELS = new Set(['family', 'group', 'model', 'sample']);
|
|
1245
|
+
// The `state` aspect resolves through the subject's bound machine
|
|
1246
|
+
// (has.state) OR through the package's operational machines: a
|
|
1247
|
+
// twin-interface subject block (the smart side's model/twin.yaml
|
|
1248
|
+
// emission, TODO.refactor/16) is PARTIAL anatomy — it carries the
|
|
1249
|
+
// endpoint/serve declarations without re-stating the state binding,
|
|
1250
|
+
// and the state channel binds at runtime (task 33).
|
|
1251
|
+
const packageHasOperationalMachine = (standard.stateMachines ?? []).some(sm => sm.kind === 'operational');
|
|
1252
|
+
/**
|
|
1253
|
+
* Resolve a serve aspect path against the owning subject — the task-03
|
|
1254
|
+
* scope vocabulary: an optional chain-level prefix (family | group |
|
|
1255
|
+
* model | sample), then parameters.<attr> | test_context.<attr> |
|
|
1256
|
+
* classification.<dim> | characteristics.<name>, or a bare
|
|
1257
|
+
* attribute/characteristic/dimension name; the reserved HAS aspects
|
|
1258
|
+
* `state` (the subject's declared state machine) and
|
|
1259
|
+
* `environmental_context` (the logged actual conditions — always
|
|
1260
|
+
* servable, §14.3) close the vocabulary.
|
|
1261
|
+
*/
|
|
1262
|
+
const resolveServeAspect = (path, s) => {
|
|
1263
|
+
let rest = path;
|
|
1264
|
+
const parts = path.split('.');
|
|
1265
|
+
if (parts.length > 1 && TWIN_CHAIN_LEVELS.has(parts[0])) {
|
|
1266
|
+
rest = parts.slice(1).join('.');
|
|
1267
|
+
}
|
|
1268
|
+
if (rest === 'state') {
|
|
1269
|
+
return s.has.state || packageHasOperationalMachine
|
|
1270
|
+
? { kind: 'state' }
|
|
1271
|
+
: null;
|
|
1272
|
+
}
|
|
1273
|
+
if (rest === 'environmental_context') {
|
|
1274
|
+
return { kind: 'environmental_context' };
|
|
1275
|
+
}
|
|
1276
|
+
const segs = rest.split('.');
|
|
1277
|
+
if (segs.length === 2) {
|
|
1278
|
+
const [area, key] = segs;
|
|
1279
|
+
if (area === 'parameters' || area === 'test_context') {
|
|
1280
|
+
const a = attrId(standard, key);
|
|
1281
|
+
return a
|
|
1282
|
+
? { kind: 'attribute', unit: a.unit, quantityKind: a.quantityKind }
|
|
1283
|
+
: null;
|
|
1284
|
+
}
|
|
1285
|
+
if (area === 'classification') {
|
|
1286
|
+
return dimIds.has(key) || key in (s.has.dimensions ?? {})
|
|
1287
|
+
? { kind: 'dimension' }
|
|
1288
|
+
: null;
|
|
1289
|
+
}
|
|
1290
|
+
if (area === 'characteristics') {
|
|
1291
|
+
const c = (s.has.characteristics ?? {})[key];
|
|
1292
|
+
return c
|
|
1293
|
+
? {
|
|
1294
|
+
kind: 'characteristic',
|
|
1295
|
+
unit: c.unit,
|
|
1296
|
+
quantityKind: c.quantityKind,
|
|
1297
|
+
}
|
|
1298
|
+
: null;
|
|
1299
|
+
}
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
if (segs.length === 1 && rest) {
|
|
1303
|
+
const a = attrId(standard, rest);
|
|
1304
|
+
if (a) {
|
|
1305
|
+
return {
|
|
1306
|
+
kind: 'attribute',
|
|
1307
|
+
unit: a.unit,
|
|
1308
|
+
quantityKind: a.quantityKind,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const c = (s.has.characteristics ?? {})[rest];
|
|
1312
|
+
if (c) {
|
|
1313
|
+
return {
|
|
1314
|
+
kind: 'characteristic',
|
|
1315
|
+
unit: c.unit,
|
|
1316
|
+
quantityKind: c.quantityKind,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
if (dimIds.has(rest) || rest in (s.has.dimensions ?? {})) {
|
|
1320
|
+
return { kind: 'dimension' };
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return null;
|
|
1324
|
+
};
|
|
1325
|
+
// ── C60–C64: the twin interface (TODO.roadmap/32 — doctrine ch. 14 §14.4) ──
|
|
1326
|
+
// The live twin's integration language: endpoint declarations (IS) and
|
|
1327
|
+
// serve bindings (HAS) on the subject anatomy, plus the connector-profile
|
|
1328
|
+
// registry. The doctrine's validation rules (§14.12, first/second/fifth
|
|
1329
|
+
// bullets): every serve names a declared aspect and a declared operation
|
|
1330
|
+
// with unit coherence; every endpoint operation has an access scope and a
|
|
1331
|
+
// payload schema (QuantityValue with timestamp); a live binding without
|
|
1332
|
+
// fresh_within is an error. Freshness SEMANTICS (stale ⇒ indeterminate)
|
|
1333
|
+
// are runtime — the smart app's verdict service owns them; these rules
|
|
1334
|
+
// guarantee the declarations the runtime needs.
|
|
1335
|
+
{
|
|
1336
|
+
const KNOWN_KINDS = new Set(Twin_1.ENDPOINT_OPERATION_KINDS);
|
|
1337
|
+
const KNOWN_SCOPES = new Set(Twin_1.ENDPOINT_ACCESS_SCOPES);
|
|
1338
|
+
const declaredProfiles = new Set([
|
|
1339
|
+
...Object.keys(Twin_1.BUILTIN_CONNECTOR_PROFILES),
|
|
1340
|
+
...(standard.connectorProfiles ?? []).map(p => p.id),
|
|
1341
|
+
]);
|
|
1342
|
+
for (const s of standard.subjects ?? []) {
|
|
1343
|
+
const endpoints = s.is.endpoints ?? [];
|
|
1344
|
+
// Operation index across the subject's endpoints (name → owners):
|
|
1345
|
+
// serve bindings resolve against the subject's whole API surface.
|
|
1346
|
+
const operations = new Map();
|
|
1347
|
+
for (const e of endpoints) {
|
|
1348
|
+
for (const op of e.operations ?? []) {
|
|
1349
|
+
const list = operations.get(op.name) ?? [];
|
|
1350
|
+
list.push({
|
|
1351
|
+
endpoint: e.id,
|
|
1352
|
+
kind: op.kind,
|
|
1353
|
+
...(op.payload?.unit ? { payloadUnit: op.payload.unit } : {}),
|
|
1354
|
+
...(op.payload?.quantityKind
|
|
1355
|
+
? { payloadKind: op.payload.quantityKind }
|
|
1356
|
+
: {}),
|
|
1357
|
+
});
|
|
1358
|
+
operations.set(op.name, list);
|
|
1359
|
+
}
|
|
1360
|
+
// C64 — endpoint-profile-resolves: the model is protocol-neutral;
|
|
1361
|
+
// profiles bind protocols (§14.4). An endpoint with no profile, or
|
|
1362
|
+
// one outside the registry (declared ∪ built-in), binds nothing.
|
|
1363
|
+
if (!e.profile) {
|
|
1364
|
+
err('C64', `subject ${s.id}: endpoint ${e.id} declares no connector profile — the model is protocol-neutral; profiles bind protocols (endpoint-profile-resolves)`);
|
|
1365
|
+
}
|
|
1366
|
+
else if (!declaredProfiles.has(e.profile)) {
|
|
1367
|
+
err('C64', `subject ${s.id}: endpoint ${e.id} profile "${e.profile}" is not a declared connector_profile nor a built-in (${Object.keys(Twin_1.BUILTIN_CONNECTOR_PROFILES).join(', ')}) (endpoint-profile-resolves)`);
|
|
1368
|
+
}
|
|
1369
|
+
for (const op of e.operations ?? []) {
|
|
1370
|
+
// C61 — payload-schema-quantity: the operation's declared schema
|
|
1371
|
+
// is well-formed (§14.4/§14.12): a known kind, and a payload that
|
|
1372
|
+
// is a QuantityValue per INV-1 — quantity kind, unit (`1` when
|
|
1373
|
+
// dimensionless), timestamp (a value without a time is not
|
|
1374
|
+
// evidence).
|
|
1375
|
+
if (!KNOWN_KINDS.has(op.kind)) {
|
|
1376
|
+
err('C61', `subject ${s.id}: endpoint ${e.id} operation ${op.name} kind "${op.kind || '(none)'}" is not one of query | subscribe | invoke (payload-schema-quantity)`);
|
|
1377
|
+
}
|
|
1378
|
+
if (!op.payload) {
|
|
1379
|
+
err('C61', `subject ${s.id}: endpoint ${e.id} operation ${op.name} declares no payload schema — every endpoint operation has a payload schema (QuantityValue with timestamp, §14.12) (payload-schema-quantity)`);
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
if (!op.payload.quantityKind) {
|
|
1383
|
+
err('C61', `subject ${s.id}: endpoint ${e.id} operation ${op.name} payload names no quantity_kind — INV-1: no bare numbers (payload-schema-quantity)`);
|
|
1384
|
+
}
|
|
1385
|
+
if (!op.payload.unit) {
|
|
1386
|
+
err('C61', `subject ${s.id}: endpoint ${e.id} operation ${op.name} payload names no unit — a QuantityValue always carries a unit (the register's dimensionless id when non-quantity) (payload-schema-quantity)`);
|
|
1387
|
+
}
|
|
1388
|
+
if (!op.payload.timestamp) {
|
|
1389
|
+
err('C61', `subject ${s.id}: endpoint ${e.id} operation ${op.name} payload does not declare timestamp true — a value without a time is not evidence (§14.3) (payload-schema-quantity)`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
// C60 (endpoint legs) — the operation's own targets resolve:
|
|
1393
|
+
// serves names against the subject's aspects, does names against
|
|
1394
|
+
// declared behaviors (an invoke with no resolvable process
|
|
1395
|
+
// triggers nothing).
|
|
1396
|
+
for (const name of op.serves ?? []) {
|
|
1397
|
+
if (!resolveServeAspect(name, s)) {
|
|
1398
|
+
err('C60', `subject ${s.id}: endpoint ${e.id} operation ${op.name} serves "${name}", which is not a declared aspect of the subject (serve-targets-resolve)`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
for (const name of op.does ?? []) {
|
|
1402
|
+
if (!behaviorIds.has(name)) {
|
|
1403
|
+
err('C60', `subject ${s.id}: endpoint ${e.id} operation ${op.name} does "${name}", which is not a declared behavior (serve-targets-resolve)`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// C62 — access-scope-covers-serves: every operation is covered by
|
|
1408
|
+
// exactly one access scope (§14.12: every endpoint operation has an
|
|
1409
|
+
// access scope); access entries name declared operations of the
|
|
1410
|
+
// endpoint; scopes are public | registered | authority.
|
|
1411
|
+
const coverage = new Map();
|
|
1412
|
+
for (const [scope, ops] of Object.entries(e.access ?? {})) {
|
|
1413
|
+
if (!KNOWN_SCOPES.has(scope)) {
|
|
1414
|
+
err('C62', `subject ${s.id}: endpoint ${e.id} access scope "${scope}" is not one of public | registered | authority (access-scope-covers-serves)`);
|
|
1415
|
+
}
|
|
1416
|
+
for (const name of ops ?? []) {
|
|
1417
|
+
if (!e.operations.some(op => op.name === name)) {
|
|
1418
|
+
err('C62', `subject ${s.id}: endpoint ${e.id} access ${scope} names "${name}", which is not a declared operation of the endpoint (access-scope-covers-serves)`);
|
|
1419
|
+
}
|
|
1420
|
+
coverage.set(name, (coverage.get(name) ?? 0) + 1);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const op of e.operations ?? []) {
|
|
1424
|
+
const n = coverage.get(op.name) ?? 0;
|
|
1425
|
+
if (n === 0) {
|
|
1426
|
+
err('C62', `subject ${s.id}: endpoint ${e.id} operation ${op.name} has no access scope — every endpoint operation has an access scope (§14.12) (access-scope-covers-serves)`);
|
|
1427
|
+
}
|
|
1428
|
+
else if (n > 1) {
|
|
1429
|
+
err('C62', `subject ${s.id}: endpoint ${e.id} operation ${op.name} is covered by ${n} access scopes — exactly one scope per operation (access-scope-covers-serves)`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
for (const b of s.has.serves ?? []) {
|
|
1434
|
+
// C60 — serve-targets-resolve: the binding names a declared aspect
|
|
1435
|
+
// and a declared operation (§14.12); the serving operation is a
|
|
1436
|
+
// value channel (query | subscribe — an invoke triggers a process,
|
|
1437
|
+
// it serves no value); the served aspect and the operation's
|
|
1438
|
+
// payload are unit-coherent.
|
|
1439
|
+
const aspect = resolveServeAspect(b.aspect, s);
|
|
1440
|
+
if (!aspect) {
|
|
1441
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" does not resolve to a declared aspect of the subject ([level.]{parameters|classification|test_context}.<key>, a bare attribute/characteristic/dimension name, state, environmental_context) (serve-targets-resolve)`);
|
|
1442
|
+
}
|
|
1443
|
+
const owners = operations.get(b.via) ?? [];
|
|
1444
|
+
if (owners.length === 0) {
|
|
1445
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" via "${b.via}" names no declared operation of the subject's endpoints (serve-targets-resolve)`);
|
|
1446
|
+
}
|
|
1447
|
+
else if (owners.length > 1) {
|
|
1448
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" via "${b.via}" is ambiguous — the operation is declared on ${owners.length} endpoints (${owners.map(o => o.endpoint).join(', ')}) (serve-targets-resolve)`);
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
const op = owners[0];
|
|
1452
|
+
if (op.kind === 'invoke') {
|
|
1453
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" via "${b.via}" targets an invoke operation — invoke triggers a process; a serve binding binds a value channel (query | subscribe) (serve-targets-resolve)`);
|
|
1454
|
+
}
|
|
1455
|
+
if (aspect &&
|
|
1456
|
+
op.payloadUnit &&
|
|
1457
|
+
aspect.unit &&
|
|
1458
|
+
op.payloadUnit !== aspect.unit) {
|
|
1459
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" unit "${aspect.unit}" ≠ operation ${b.via} payload unit "${op.payloadUnit}" — unit coherence between aspect and payload is required (§14.12) (serve-targets-resolve)`);
|
|
1460
|
+
}
|
|
1461
|
+
if (aspect &&
|
|
1462
|
+
op.payloadKind &&
|
|
1463
|
+
aspect.quantityKind &&
|
|
1464
|
+
op.payloadKind !== aspect.quantityKind) {
|
|
1465
|
+
err('C60', `subject ${s.id}: serve "${b.aspect}" quantity kind "${aspect.quantityKind}" ≠ operation ${b.via} payload quantity kind "${op.payloadKind}" (serve-targets-resolve)`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
// C63 — freshness-required-on-live-bindings: a live binding without
|
|
1469
|
+
// fresh_within is an error (§14.12 — no stale semantics, no live
|
|
1470
|
+
// binding); the window must parse (shorthand 5s/1min/1h or ISO
|
|
1471
|
+
// 8601 with fixed-length components).
|
|
1472
|
+
if (!b.freshWithin) {
|
|
1473
|
+
err('C63', `subject ${s.id}: serve "${b.aspect}" via "${b.via}" declares no fresh_within — a live binding without a freshness window is an error (§14.12: no stale semantics, no live binding) (freshness-required-on-live-bindings)`);
|
|
1474
|
+
}
|
|
1475
|
+
else if ((0, time_1.parseFreshnessWindow)(b.freshWithin) === null) {
|
|
1476
|
+
err('C63', `subject ${s.id}: serve "${b.aspect}" fresh_within "${b.freshWithin}" is not a parseable freshness window (shorthand 500ms/5s/1min/1h/1d or ISO 8601 with fixed-length components, e.g. PT5S) (freshness-required-on-live-bindings)`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// C100–C102 — the composition facet (TODO.integration/14): a
|
|
1480
|
+
// composite subject's composed_of checks on all three legs.
|
|
1481
|
+
const composed = s.is.composedOf;
|
|
1482
|
+
if (composed) {
|
|
1483
|
+
const componentIds = new Set(composed.components.map(c => c.id));
|
|
1484
|
+
for (const c of composed.components) {
|
|
1485
|
+
// C100 — composition-components-resolve: an inline
|
|
1486
|
+
// `pkg/subject` product reference names a subject of THIS
|
|
1487
|
+
// package (a bare package id resolves cross-package at the
|
|
1488
|
+
// supply-chain gate — the C81 discipline, registered here).
|
|
1489
|
+
if (c.product.includes('/')) {
|
|
1490
|
+
const sub = c.product
|
|
1491
|
+
.slice(c.product.indexOf('/') + 1)
|
|
1492
|
+
.split('@')[0];
|
|
1493
|
+
// The reference is kebab-form, the subject id PascalCase —
|
|
1494
|
+
// resolve alphanumerically (sample-line ≡ SampleLine); the
|
|
1495
|
+
// @edition pin is the C83 discipline, stripped here.
|
|
1496
|
+
const norm = (x) => x.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
1497
|
+
if (!(standard.subjects ?? []).some(x => norm(x.id) === norm(sub))) {
|
|
1498
|
+
err('C100', `subject ${s.id}: composed_of component ${c.id} product "${c.product}" names no subject of this package (composition-components-resolve)`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (!c.endpoint) {
|
|
1502
|
+
err('C100', `subject ${s.id}: composed_of component ${c.id} declares no endpoint — the component's projection must serve on a named endpoint (composition-components-resolve)`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// C101 — composition-decomposition-covers: every serve of the
|
|
1506
|
+
// composite subject is covered by the decomposition exactly once.
|
|
1507
|
+
const covered = new Map();
|
|
1508
|
+
for (const d of composed.decomposition) {
|
|
1509
|
+
covered.set(d.register, (covered.get(d.register) ?? 0) + 1);
|
|
1510
|
+
if (d.component !== 'composite' && !componentIds.has(d.component)) {
|
|
1511
|
+
err('C100', `subject ${s.id}: decomposition ${d.register} names undeclared component "${d.component}" (composition-components-resolve)`);
|
|
1512
|
+
}
|
|
1513
|
+
// C102 — composition-state-rule-closed: the vocabulary is
|
|
1514
|
+
// closed (any_fault_else_analyzer first — a new rule is a
|
|
1515
|
+
// grammar extension, never a free string).
|
|
1516
|
+
if (d.component === 'composite' &&
|
|
1517
|
+
!Composition_1.COMPOSITION_STATE_RULES.includes(d.rule ?? '')) {
|
|
1518
|
+
err('C102', `subject ${s.id}: decomposition ${d.register} declares unknown composite state rule "${d.rule ?? '(none)'}" — the vocabulary is closed (${Composition_1.COMPOSITION_STATE_RULES.join(', ')}) (composition-state-rule-closed)`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
for (const serve of s.has.serves ?? []) {
|
|
1522
|
+
if ((covered.get(serve.aspect) ?? 0) !== 1) {
|
|
1523
|
+
err('C101', `subject ${s.id}: serve "${serve.aspect}" is covered ${covered.get(serve.aspect) ?? 0} times by the decomposition — every served register decomposes exactly once (composition-decomposition-covers)`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
// ── C65–C70: the monitors (TODO.roadmap/34 — doctrine ch. 14 §14.5/§14.12) ──
|
|
1530
|
+
// Continuous compliance: a monitor runs the standard next to the live
|
|
1531
|
+
// twins — triggers (the clock), evaluation refs (applicability-expanded
|
|
1532
|
+
// per twin at runtime), evidence sinks, escalation. The doctrine's
|
|
1533
|
+
// validation rules (§14.12, third/fourth bullets): every monitor's
|
|
1534
|
+
// evaluate refs resolve to requirements/promises applicable to the
|
|
1535
|
+
// monitored subjects; a monitor without an escalation path for `fail`
|
|
1536
|
+
// is a warning. Freshness semantics, the verdict stream, and the
|
|
1537
|
+
// escalation ACTIONS are runtime — the smart app's monitor service owns
|
|
1538
|
+
// them; these rules guarantee the declarations the runtime needs.
|
|
1539
|
+
{
|
|
1540
|
+
const roleIds = new Set((standard.roles ?? []).map(r => r.id));
|
|
1541
|
+
const KNOWN_OUTCOMES = new Set(Monitor_1.MONITOR_OUTCOMES);
|
|
1542
|
+
const KNOWN_ACTIONS = new Set(Monitor_1.MONITOR_ESCALATION_ACTIONS);
|
|
1543
|
+
const KNOWN_STREAMS = new Set(Monitor_1.MONITOR_STREAMS);
|
|
1544
|
+
const KNOWN_REFSET_KINDS = new Set(Monitor_1.MONITOR_REFSET_KINDS);
|
|
1545
|
+
for (const m of standard.monitors ?? []) {
|
|
1546
|
+
const subjects = (standard.subjects ?? []).filter(s => m.over.includes(s.id));
|
|
1547
|
+
// C65 — monitor-subject-resolves: the watched set is non-empty and
|
|
1548
|
+
// every ref names a declared subject (a monitor watching nothing
|
|
1549
|
+
// judges nothing).
|
|
1550
|
+
if (m.over.length === 0) {
|
|
1551
|
+
err('C65', `monitor ${m.id}: declares no subject set — a monitor watches a subject set (over { … }) (monitor-subject-resolves)`);
|
|
1552
|
+
}
|
|
1553
|
+
for (const ref of m.over) {
|
|
1554
|
+
if (!subjectIds.has(ref)) {
|
|
1555
|
+
err('C65', `monitor ${m.id}: over "${ref}" is not a declared subject (monitor-subject-resolves)`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// C66 — monitor-trigger-wellformed: without triggers "continuous"
|
|
1559
|
+
// has no clock (§14.5 step 1) — at least one trigger; a timer's
|
|
1560
|
+
// every window parses (the freshness-window syntax); a signal names
|
|
1561
|
+
// its signal; a change names an aspect that resolves against a
|
|
1562
|
+
// monitored subject (the serve aspect vocabulary — ONE resolver).
|
|
1563
|
+
if (m.triggers.length === 0) {
|
|
1564
|
+
err('C66', `monitor ${m.id}: declares no triggers — without triggers, "continuous" has no clock (§14.5 step 1) (monitor-trigger-wellformed)`);
|
|
1565
|
+
}
|
|
1566
|
+
for (const trigger of m.triggers) {
|
|
1567
|
+
if (trigger.kind === 'timer') {
|
|
1568
|
+
if (!trigger.every || (0, time_1.parseFreshnessWindow)(trigger.every) === null) {
|
|
1569
|
+
err('C66', `monitor ${m.id}: timer trigger every "${trigger.every || '(none)'}" is not a parseable window (shorthand 500ms/5s/1min/1h/1d or ISO 8601 with fixed-length components, e.g. PT1H) (monitor-trigger-wellformed)`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
else if (trigger.kind === 'signal') {
|
|
1573
|
+
if (!trigger.signal) {
|
|
1574
|
+
err('C66', `monitor ${m.id}: signal trigger names no signal (on signal <name>) (monitor-trigger-wellformed)`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
else if (trigger.kind === 'change') {
|
|
1578
|
+
if (!trigger.aspect) {
|
|
1579
|
+
err('C66', `monitor ${m.id}: change trigger names no aspect (on change <aspect>) (monitor-trigger-wellformed)`);
|
|
1580
|
+
}
|
|
1581
|
+
else if (subjects.length > 0 &&
|
|
1582
|
+
!subjects.some(s => resolveServeAspect(trigger.aspect, s))) {
|
|
1583
|
+
err('C66', `monitor ${m.id}: change trigger aspect "${trigger.aspect}" does not resolve against any monitored subject (the serve aspect vocabulary: [level.]{parameters|classification|test_context}.<key>, a bare attribute/characteristic/dimension name, state, environmental_context) (monitor-trigger-wellformed)`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
err('C66', `monitor ${m.id}: trigger kind "${trigger.kind || '(none)'}" is not one of timer (every) | signal (on signal) | change (on change) (monitor-trigger-wellformed)`);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
// C67 — monitor-evaluate-resolves (§14.12): the evaluate refs
|
|
1591
|
+
// resolve to requirements/promises applicable to the monitored
|
|
1592
|
+
// subjects. `all` and `applicable_to(…)` expand per twin at runtime
|
|
1593
|
+
// (applicability-expanded — always admissible); explicit refs must
|
|
1594
|
+
// resolve: requirement ids against the package's requirements,
|
|
1595
|
+
// promise ids against the monitored subjects' promises.
|
|
1596
|
+
const checkRefSet = (label, set, resolveRef, refKind) => {
|
|
1597
|
+
if (!set.kind) {
|
|
1598
|
+
err('C67', `monitor ${m.id}: evaluate names no ${label} selector (all | applicable_to(…) | { refs… }) — a monitor judges something (monitor-evaluate-resolves)`);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (!KNOWN_REFSET_KINDS.has(set.kind)) {
|
|
1602
|
+
err('C67', `monitor ${m.id}: ${label} selector "${set.kind}" is not one of all | applicable_to(…) | { refs… } (monitor-evaluate-resolves)`);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (set.kind === 'applicable_to' && !set.expression) {
|
|
1606
|
+
err('C67', `monitor ${m.id}: ${label} applicable_to() carries no applicability expression (monitor-evaluate-resolves)`);
|
|
1607
|
+
}
|
|
1608
|
+
if (set.kind === 'refs') {
|
|
1609
|
+
if (set.refs.length === 0) {
|
|
1610
|
+
err('C67', `monitor ${m.id}: ${label} selector { } names no refs (monitor-evaluate-resolves)`);
|
|
1611
|
+
}
|
|
1612
|
+
for (const ref of set.refs) {
|
|
1613
|
+
if (!resolveRef(ref)) {
|
|
1614
|
+
err('C67', `monitor ${m.id}: evaluate ${label} ref "${ref}" is not a declared ${refKind} — evaluate refs resolve to requirements/promises applicable to the monitored subjects (§14.12) (monitor-evaluate-resolves)`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
checkRefSet('requirements', m.evaluate.requirements, r => reqIds.has(r), 'requirement');
|
|
1620
|
+
const promiseIds = new Set(subjects
|
|
1621
|
+
.flatMap(s => (s.is.promises ?? []).map(p => p.id))
|
|
1622
|
+
.filter(id => id !== ''));
|
|
1623
|
+
checkRefSet('promises', m.evaluate.promises, p => promiseIds.has(p), 'promise of the monitored subjects');
|
|
1624
|
+
// C68 — monitor-fail-escalation (§14.12, verbatim): a monitor
|
|
1625
|
+
// without an escalation path for `fail` is a WARNING. Pass accrues
|
|
1626
|
+
// history; fail/invalid must act (notify, flag the certificate,
|
|
1627
|
+
// open a case).
|
|
1628
|
+
if (!m.escalate.some(r => r.outcome === 'fail')) {
|
|
1629
|
+
warn('C68', `monitor ${m.id}: declares no escalation path for fail — §14.12: a monitor without an escalation path for fail is a warning (monitor-fail-escalation)`);
|
|
1630
|
+
}
|
|
1631
|
+
// C69 — monitor-escalation-resolves: escalation outcomes are
|
|
1632
|
+
// verdict outcomes; actions are notify | flag_certificate |
|
|
1633
|
+
// open_service_case; a notify names a declared role.
|
|
1634
|
+
for (const rule of m.escalate) {
|
|
1635
|
+
if (!KNOWN_OUTCOMES.has(rule.outcome)) {
|
|
1636
|
+
err('C69', `monitor ${m.id}: escalate on "${rule.outcome || '(none)'}" — the outcome is not one of ${Monitor_1.MONITOR_OUTCOMES.join(' | ')} (monitor-escalation-resolves)`);
|
|
1637
|
+
}
|
|
1638
|
+
if (rule.actions.length === 0) {
|
|
1639
|
+
err('C69', `monitor ${m.id}: escalate on ${rule.outcome || '(none)'} names no actions — an escalation path acts (notify / flag_certificate / open_service_case) (monitor-escalation-resolves)`);
|
|
1640
|
+
}
|
|
1641
|
+
for (const a of rule.actions) {
|
|
1642
|
+
if (!KNOWN_ACTIONS.has(a.action)) {
|
|
1643
|
+
err('C69', `monitor ${m.id}: escalate on ${rule.outcome} action "${a.action}" is not one of ${Monitor_1.MONITOR_ESCALATION_ACTIONS.join(' | ')} (monitor-escalation-resolves)`);
|
|
1644
|
+
}
|
|
1645
|
+
else if (a.action === 'notify' && !a.role) {
|
|
1646
|
+
err('C69', `monitor ${m.id}: escalate on ${rule.outcome} notify names no role (notify <role>) (monitor-escalation-resolves)`);
|
|
1647
|
+
}
|
|
1648
|
+
else if (a.action === 'notify' &&
|
|
1649
|
+
roleIds.size > 0 &&
|
|
1650
|
+
!roleIds.has(a.role)) {
|
|
1651
|
+
err('C69', `monitor ${m.id}: escalate on ${rule.outcome} notify "${a.role}" is not a declared role (monitor-escalation-resolves)`);
|
|
1652
|
+
}
|
|
1653
|
+
else if (a.action !== 'notify' && a.role) {
|
|
1654
|
+
err('C69', `monitor ${m.id}: escalate on ${rule.outcome} ${a.action} carries a role ("${a.role}") — only notify names a role (monitor-escalation-resolves)`);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
// C70 — monitor-emit-sinks (§14.5 step 6): evidence is appended to
|
|
1659
|
+
// the workspace and the verdict log — both streams emitted, each
|
|
1660
|
+
// exactly once, with a named sink. An unknown stream kind binds
|
|
1661
|
+
// nothing.
|
|
1662
|
+
const emitted = new Map();
|
|
1663
|
+
for (const sink of m.emit) {
|
|
1664
|
+
if (!KNOWN_STREAMS.has(sink.stream)) {
|
|
1665
|
+
err('C70', `monitor ${m.id}: emit stream "${sink.stream}" is not one of ${Monitor_1.MONITOR_STREAMS.join(' | ')} (monitor-emit-sinks)`);
|
|
1666
|
+
}
|
|
1667
|
+
if (!sink.target) {
|
|
1668
|
+
err('C70', `monitor ${m.id}: emit ${sink.stream} names no sink (${sink.stream} -> <sink>) (monitor-emit-sinks)`);
|
|
1669
|
+
}
|
|
1670
|
+
emitted.set(sink.stream, (emitted.get(sink.stream) ?? 0) + 1);
|
|
1671
|
+
}
|
|
1672
|
+
for (const stream of Monitor_1.MONITOR_STREAMS) {
|
|
1673
|
+
const n = emitted.get(stream) ?? 0;
|
|
1674
|
+
if (n === 0) {
|
|
1675
|
+
err('C70', `monitor ${m.id}: emit declares no ${stream} sink — §14.5 step 6: values seen, rule results, verdicts, timestamps are appended to the workspace (monitor-emit-sinks)`);
|
|
1676
|
+
}
|
|
1677
|
+
else if (n > 1) {
|
|
1678
|
+
err('C70', `monitor ${m.id}: emit declares the ${stream} stream ${n} times — one sink per stream (monitor-emit-sinks)`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// ── C10–C16: executable process bodies (TODO.roadmap/02) ──
|
|
1684
|
+
// These rules apply only to processes with a `does` body (the
|
|
1685
|
+
// executable form). Abstract processes (signature/invariants, no
|
|
1686
|
+
// `does`) are always valid; v2 canvas processes are untouched.
|
|
1687
|
+
for (const p of standard.processes ?? []) {
|
|
1688
|
+
const flow = p.does;
|
|
1689
|
+
if (!flow) {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const steps = flow.steps ?? [];
|
|
1693
|
+
const edges = flow.edges ?? [];
|
|
1694
|
+
// C16 — step ids are unique within one does body
|
|
1695
|
+
// (process-step-ids-unique). Declared BEFORE the stepById map below
|
|
1696
|
+
// silently keeps the last duplicate.
|
|
1697
|
+
const seenStepIds = new Set();
|
|
1698
|
+
for (const s of steps) {
|
|
1699
|
+
if (seenStepIds.has(s.id)) {
|
|
1700
|
+
err('C16', `process ${p.id}: duplicate step id "${s.id}" in the does body (process-step-ids-unique)`);
|
|
1701
|
+
}
|
|
1702
|
+
seenStepIds.add(s.id);
|
|
1703
|
+
}
|
|
1704
|
+
const stepById = new Map(steps.map(s => [s.id, s]));
|
|
1705
|
+
// The declared name vocabulary for step I/O and edge conditions:
|
|
1706
|
+
// registers + signature IN/OUT parameters + instance-parameter keys +
|
|
1707
|
+
// the process's own `state` (a declared HAS).
|
|
1708
|
+
const declared = new Set(['state']);
|
|
1709
|
+
for (const r of p.registers ?? []) {
|
|
1710
|
+
declared.add(r.name);
|
|
1711
|
+
}
|
|
1712
|
+
for (const prm of p.signature?.inputs ?? []) {
|
|
1713
|
+
declared.add(prm.name);
|
|
1714
|
+
}
|
|
1715
|
+
for (const prm of p.signature?.outputs ?? []) {
|
|
1716
|
+
declared.add(prm.name);
|
|
1717
|
+
}
|
|
1718
|
+
for (const v of Object.values(p.instances?.values ?? {})) {
|
|
1719
|
+
for (const k of Object.keys(v)) {
|
|
1720
|
+
declared.add(k);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
// C10 — exactly one start event per executable process.
|
|
1724
|
+
const starts = steps.filter(s => s.kind === 'start_event');
|
|
1725
|
+
if (starts.length !== 1) {
|
|
1726
|
+
err('C10', `process ${p.id}: executable process has ${starts.length} start events — exactly one required (process-one-start)`);
|
|
1727
|
+
}
|
|
1728
|
+
// C11 — end events on every terminal path (mandatory on empty
|
|
1729
|
+
// gateway branches); flow edges reference declared steps.
|
|
1730
|
+
const outgoing = new Map();
|
|
1731
|
+
for (const e of edges) {
|
|
1732
|
+
if (!stepById.has(e.from)) {
|
|
1733
|
+
err('C11', `process ${p.id}: flow edge starts at undeclared step "${e.from}" (process-terminal-end)`);
|
|
1734
|
+
}
|
|
1735
|
+
if (!stepById.has(e.to)) {
|
|
1736
|
+
err('C11', `process ${p.id}: flow edge ends at undeclared step "${e.to}" (process-terminal-end)`);
|
|
1737
|
+
}
|
|
1738
|
+
outgoing.set(e.from, [...(outgoing.get(e.from) ?? []), e]);
|
|
1739
|
+
}
|
|
1740
|
+
for (const s of steps) {
|
|
1741
|
+
if ((outgoing.get(s.id) ?? []).length === 0 && s.kind !== 'end_event') {
|
|
1742
|
+
err('C11', `process ${p.id}: step "${s.id}" (${s.kind}) is a terminal path with no end event — every terminal path must close at an end_event, mandatory on empty gateway branches (process-terminal-end)`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// C12 — gateway edge conditions and step I/O name declared registers.
|
|
1746
|
+
// The caller-side names of a call's `with` bindings are step I/O too
|
|
1747
|
+
// (TODO.roadmap/38): an in-binding READS the caller name, an
|
|
1748
|
+
// out-mapping WRITES it. The callee-side parameter names are checked
|
|
1749
|
+
// against the callee's signature by C76, not here.
|
|
1750
|
+
for (const s of steps) {
|
|
1751
|
+
const ioNames = [...s.reads, ...s.writes];
|
|
1752
|
+
if (s.wait) {
|
|
1753
|
+
ioNames.push(s.wait);
|
|
1754
|
+
}
|
|
1755
|
+
for (const b of s.callIn) {
|
|
1756
|
+
ioNames.push(b.bind);
|
|
1757
|
+
}
|
|
1758
|
+
for (const b of s.callOut) {
|
|
1759
|
+
ioNames.push(b.bind);
|
|
1760
|
+
}
|
|
1761
|
+
for (const n of ioNames) {
|
|
1762
|
+
if (!declared.has(n)) {
|
|
1763
|
+
err('C12', `process ${p.id}: step "${s.id}" names undeclared register "${n}" (process-flow-names-resolve)`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (s.kind === 'gateway') {
|
|
1767
|
+
for (const e of outgoing.get(s.id) ?? []) {
|
|
1768
|
+
const re = /\bself\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1769
|
+
let m;
|
|
1770
|
+
while ((m = re.exec(e.condition)) !== null) {
|
|
1771
|
+
if (!declared.has(m[1])) {
|
|
1772
|
+
err('C12', `process ${p.id}: gateway "${s.id}" edge condition references undeclared register "${m[1]}" (process-flow-names-resolve)`);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
// C13 — the executable steps realize the signature: every OUT
|
|
1779
|
+
// parameter is written (error), every IN parameter is read (warning).
|
|
1780
|
+
// Call bindings count: an in-binding reads the caller name, an
|
|
1781
|
+
// out-mapping writes it (TODO.roadmap/38).
|
|
1782
|
+
if (p.signature) {
|
|
1783
|
+
const written = new Set();
|
|
1784
|
+
const read = new Set();
|
|
1785
|
+
for (const s of steps) {
|
|
1786
|
+
s.writes.forEach(n => written.add(n));
|
|
1787
|
+
s.reads.forEach(n => read.add(n));
|
|
1788
|
+
if (s.wait) {
|
|
1789
|
+
read.add(s.wait);
|
|
1790
|
+
}
|
|
1791
|
+
s.callOut.forEach(b => written.add(b.bind));
|
|
1792
|
+
s.callIn.forEach(b => read.add(b.bind));
|
|
1793
|
+
}
|
|
1794
|
+
for (const o of p.signature.outputs) {
|
|
1795
|
+
if (!written.has(o.name)) {
|
|
1796
|
+
err('C13', `process ${p.id}: OUT parameter "${o.name}" is never written by any step (process-signature-realized)`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
for (const inp of p.signature.inputs) {
|
|
1800
|
+
if (!read.has(inp.name)) {
|
|
1801
|
+
warn('C13', `process ${p.id}: IN parameter "${inp.name}" is never read by any step (process-signature-realized)`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// C14 — a recurrence (a cycle in the flow graph) must pass through a
|
|
1806
|
+
// timer event WITH a declared period: remove guarded timer nodes and
|
|
1807
|
+
// require the rest acyclic. A period-less timer_event is NOT a guard
|
|
1808
|
+
// (C15) and does not break the cycle here.
|
|
1809
|
+
const timerIds = new Set(steps.filter(s => s.kind === 'timer_event' && s.period).map(s => s.id));
|
|
1810
|
+
const adj = new Map();
|
|
1811
|
+
for (const e of edges) {
|
|
1812
|
+
if (timerIds.has(e.from) || timerIds.has(e.to)) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
adj.set(e.from, [...(adj.get(e.from) ?? []), e.to]);
|
|
1816
|
+
}
|
|
1817
|
+
const cycle = findCycle(adj);
|
|
1818
|
+
if (cycle) {
|
|
1819
|
+
err('C14', `process ${p.id}: recurrence ${cycle.join(' → ')} contains no timer event — an unguarded infinite self-loop (process-timer-recurrence)`);
|
|
1820
|
+
}
|
|
1821
|
+
// C15 — a timer_event must declare its recurrence `period`: without
|
|
1822
|
+
// one it cannot guard a loop (C14) and is almost certainly a
|
|
1823
|
+
// modelling mistake (process-timer-period). The period FORMAT is
|
|
1824
|
+
// validated by C35 (time-format, TODO.roadmap/06); this rule checks
|
|
1825
|
+
// presence only.
|
|
1826
|
+
for (const s of steps) {
|
|
1827
|
+
if (s.kind === 'timer_event' && !s.period) {
|
|
1828
|
+
err('C15', `process ${p.id}: timer_event "${s.id}" declares no period — a timer event without a recurrence period cannot guard a loop (process-timer-period)`);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
// ── C17–C20: instantiation and delegation (TODO.roadmap/03) ──
|
|
1833
|
+
// The instance plane: scope discipline on stated values (C17), INV-8
|
|
1834
|
+
// version pins (C18), subject-chain integrity (C19), and `of` reference
|
|
1835
|
+
// resolution (C20).
|
|
1836
|
+
const CHAIN_LEVEL_ORDER = {
|
|
1837
|
+
family: 0,
|
|
1838
|
+
group: 1,
|
|
1839
|
+
model: 2,
|
|
1840
|
+
sample: 3,
|
|
1841
|
+
};
|
|
1842
|
+
const instrumentIds = new Set((standard.instruments ?? []).map(i => i.id));
|
|
1843
|
+
const instanceIds = new Set((standard.instances ?? []).map(i => i.id));
|
|
1844
|
+
const attrScopes = new Map((standard.attributeDefinitions ?? []).map(a => [a.id, a.scope]));
|
|
1845
|
+
// Classification-dimension scopes, by every spelling a classification
|
|
1846
|
+
// map may use: the instrument dimension id, the is_dimension attribute
|
|
1847
|
+
// mirror id, and the enum-name aliases (DIM_ALIASES).
|
|
1848
|
+
const dimScopes = new Map();
|
|
1849
|
+
for (const inst of standard.instruments ?? []) {
|
|
1850
|
+
for (const d of inst.dimensions ?? []) {
|
|
1851
|
+
if (d.scope) {
|
|
1852
|
+
dimScopes.set(d.id, d.scope);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
for (const a of standard.attributeDefinitions ?? []) {
|
|
1857
|
+
if (a.isDimension && a.scope && !dimScopes.has(a.id)) {
|
|
1858
|
+
dimScopes.set(a.id, a.scope);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
for (const [alias, canonical] of Object.entries(DIM_ALIASES)) {
|
|
1862
|
+
const s = dimScopes.get(canonical) ?? attrScopes.get(canonical);
|
|
1863
|
+
if (s && !dimScopes.has(alias)) {
|
|
1864
|
+
dimScopes.set(alias, s);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
for (const inst of standard.instances ?? []) {
|
|
1868
|
+
const levelIdx = CHAIN_LEVEL_ORDER[inst.level];
|
|
1869
|
+
// C17 — instance-scope: every stated value at its declared scope or
|
|
1870
|
+
// lower; sample-scope values only in a sample's test_context.
|
|
1871
|
+
// NOTE (sample-level override gap): the language PERMITS a
|
|
1872
|
+
// non-sample-scope attribute in a sample's has.attributes (the
|
|
1873
|
+
// lower-override law — a sample may restate a wider-scope value), and
|
|
1874
|
+
// C17 accepts it. But the current app plane has no sample
|
|
1875
|
+
// `parameters` field (only `test_context`), so such a value has no
|
|
1876
|
+
// app-side home: recs SHOULD NOT author sample-level attribute
|
|
1877
|
+
// overrides until the app plane grows one (tracked by TODO.roadmap
|
|
1878
|
+
// task 29). A future strict mode may make this an error.
|
|
1879
|
+
if (levelIdx === undefined) {
|
|
1880
|
+
err('C17', `instance ${inst.id}: level "${inst.level}" is not a chain level (family | group | model | sample) (instance-scope)`);
|
|
1881
|
+
}
|
|
1882
|
+
for (const key of Object.keys(inst.has?.attributes ?? {})) {
|
|
1883
|
+
const scope = attrScopes.get(key);
|
|
1884
|
+
if (scope === undefined) {
|
|
1885
|
+
err('C17', `instance ${inst.id}: attribute "${key}" is not a declared attribute_definition — scope cannot be checked (instance-scope)`);
|
|
1886
|
+
}
|
|
1887
|
+
else if (scope === 'sample') {
|
|
1888
|
+
err('C17', `instance ${inst.id}: sample-scope attribute "${key}" stated in has.attributes — sample-scope values live in has.test_context and are never inherited (instance-scope)`);
|
|
1889
|
+
}
|
|
1890
|
+
else if (scope &&
|
|
1891
|
+
levelIdx !== undefined &&
|
|
1892
|
+
CHAIN_LEVEL_ORDER[scope] !== undefined &&
|
|
1893
|
+
levelIdx < CHAIN_LEVEL_ORDER[scope]) {
|
|
1894
|
+
err('C17', `instance ${inst.id}: attribute "${key}" (scope ${scope}) stated at ${inst.level} level — values are stated at their declared scope or lower (instance-scope)`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
for (const key of Object.keys(inst.has?.testContext ?? {})) {
|
|
1898
|
+
if (inst.level !== 'sample') {
|
|
1899
|
+
err('C17', `instance ${inst.id}: test_context value "${key}" on a ${inst.level}-level instance — test_context exists only at sample level (instance-scope)`);
|
|
1900
|
+
}
|
|
1901
|
+
const scope = attrScopes.get(key);
|
|
1902
|
+
if (scope === undefined) {
|
|
1903
|
+
err('C17', `instance ${inst.id}: attribute "${key}" is not a declared attribute_definition — scope cannot be checked (instance-scope)`);
|
|
1904
|
+
}
|
|
1905
|
+
else if (scope !== 'sample') {
|
|
1906
|
+
err('C17', `instance ${inst.id}: attribute "${key}" (scope ${scope}) stated in test_context — test_context holds sample-scope attributes only (instance-scope)`);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
for (const key of Object.keys(inst.has?.dimensions ?? {})) {
|
|
1910
|
+
if (inst.level === 'sample') {
|
|
1911
|
+
err('C17', `instance ${inst.id}: classification "${key}" on a sample-level instance — samples carry no classification (instance-scope)`);
|
|
1912
|
+
}
|
|
1913
|
+
const scope = dimScopes.get(key);
|
|
1914
|
+
if (scope === undefined) {
|
|
1915
|
+
err('C17', `instance ${inst.id}: classification dimension "${key}" is not declared (instance-scope)`);
|
|
1916
|
+
}
|
|
1917
|
+
else if (levelIdx !== undefined &&
|
|
1918
|
+
CHAIN_LEVEL_ORDER[scope] !== undefined &&
|
|
1919
|
+
levelIdx < CHAIN_LEVEL_ORDER[scope]) {
|
|
1920
|
+
err('C17', `instance ${inst.id}: classification dimension "${key}" (scope ${scope}) stated at ${inst.level} level — values are stated at their declared scope or lower (instance-scope)`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
// C18 — instance-version-pin: every instance is version-pinned to its
|
|
1924
|
+
// definitions (INV-8); re-execution requires the pin.
|
|
1925
|
+
if (Object.keys(inst.definitionVersions ?? {}).length === 0) {
|
|
1926
|
+
err('C18', `instance ${inst.id}: no definition_versions — every instance is version-pinned to its definitions (INV-8) (instance-version-pin)`);
|
|
1927
|
+
}
|
|
1928
|
+
// C19 — chain-acyclic (integrity part 1): upward chain links resolve
|
|
1929
|
+
// to declared instances.
|
|
1930
|
+
for (const kind of ['model', 'group', 'family']) {
|
|
1931
|
+
const target = inst[kind];
|
|
1932
|
+
if (target && !instanceIds.has(target)) {
|
|
1933
|
+
err('C19', `instance ${inst.id}: chain link ${kind} "${target}" is not a declared instance (chain-acyclic)`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
// C20 — instance-of-resolves: the `of` reference names a declared
|
|
1937
|
+
// subject (v3) or instrument (v2) definition.
|
|
1938
|
+
if (!subjectIds.has(inst.of) && !instrumentIds.has(inst.of)) {
|
|
1939
|
+
err('C20', `instance ${inst.id}: of "${inst.of}" is not a declared subject or instrument (instance-of-resolves)`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
// C19 — chain-acyclic (integrity part 2): the resolved link graph is
|
|
1943
|
+
// acyclic (a cycle would make delegation resolution non-terminating).
|
|
1944
|
+
const instAdj = new Map();
|
|
1945
|
+
for (const inst of standard.instances ?? []) {
|
|
1946
|
+
const targets = [inst.model, inst.group, inst.family].filter(t => t && instanceIds.has(t));
|
|
1947
|
+
if (targets.length > 0) {
|
|
1948
|
+
instAdj.set(inst.id, targets);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
const instCycle = findCycle(instAdj);
|
|
1952
|
+
if (instCycle) {
|
|
1953
|
+
err('C19', `instance subject chain cycle: ${instCycle.join(' → ')} (chain-acyclic)`);
|
|
1954
|
+
}
|
|
1955
|
+
// ── C21–C26: the mapping primitive + coverage calculus ──
|
|
1956
|
+
// (TODO.roadmap/04; concept doc §5.2, §5.6, §5.8). Mappings are the ONLY
|
|
1957
|
+
// compliance relation between an implementation model and a reference
|
|
1958
|
+
// model: both ends resolve (C21), the direction is fixed (C22), coverage
|
|
1959
|
+
// is computed not authored (C23), import ≠ mapping (C24), descriptions
|
|
1960
|
+
// are demanded at audit strictness (C25), and views are read-only (C26).
|
|
1961
|
+
const strictness = options.strictness ?? 'normal';
|
|
1962
|
+
const localNs = standard.packageManifest?.id ?? '';
|
|
1963
|
+
const compIds = (0, mapping_coverage_1.componentIds)(standard);
|
|
1964
|
+
// In-model map profiles + standalone .prm files in the package root —
|
|
1965
|
+
// both serializations of the same primitive, linted uniformly.
|
|
1966
|
+
const allProfiles = [...(standard.mapProfiles ?? [])];
|
|
1967
|
+
if ((0, fs_1.existsSync)(dir)) {
|
|
1968
|
+
for (const entry of (0, fs_1.readdirSync)(dir).sort()) {
|
|
1969
|
+
if (!entry.endsWith('.prm')) {
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
const path = (0, path_1.join)(dir, entry);
|
|
1973
|
+
try {
|
|
1974
|
+
allProfiles.push(...(0, prm_1.prmToMapProfiles)((0, prm_1.loadPrm)((0, fs_1.readFileSync)(path, 'utf8'))));
|
|
1975
|
+
}
|
|
1976
|
+
catch (e) {
|
|
1977
|
+
err('C21', `${entry}: ${e.message} (mapping-resolves)`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
// Dependency-manifest resolution for the supply-chain rules (C24's
|
|
1982
|
+
// product_reference exemption, C81, C83): the composition locator when
|
|
1983
|
+
// given, else the SIBLING-package scan (the checkManifestResolution
|
|
1984
|
+
// stopgap pattern) — manifest-only linting (no --with) must still see
|
|
1985
|
+
// package kinds and edition registers. Memoized per package id.
|
|
1986
|
+
const resolveDepManifest = (() => {
|
|
1987
|
+
const cache = new Map();
|
|
1988
|
+
const siblings = new Map();
|
|
1989
|
+
try {
|
|
1990
|
+
const parent = (0, path_1.dirname)((0, path_1.resolve)(dir));
|
|
1991
|
+
for (const entry of (0, fs_1.readdirSync)(parent).sort()) {
|
|
1992
|
+
const full = (0, path_1.join)(parent, entry);
|
|
1993
|
+
try {
|
|
1994
|
+
if (!(0, fs_1.statSync)(full).isDirectory()) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const m = (0, package_1.readPackageManifest)(full);
|
|
1998
|
+
siblings.set(m.id, m);
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
// Not a package dir (no/invalid manifest) — not a candidate.
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
// No readable parent — no sibling fallback.
|
|
2007
|
+
}
|
|
2008
|
+
return (id) => {
|
|
2009
|
+
const cached = cache.get(id);
|
|
2010
|
+
if (cached !== undefined) {
|
|
2011
|
+
return cached;
|
|
2012
|
+
}
|
|
2013
|
+
let m = null;
|
|
2014
|
+
const located = options.resolvePackage?.(id);
|
|
2015
|
+
if (located) {
|
|
2016
|
+
try {
|
|
2017
|
+
m = (0, package_1.readPackageManifest)(located);
|
|
2018
|
+
}
|
|
2019
|
+
catch {
|
|
2020
|
+
m = null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
m ??= siblings.get(id) ?? null;
|
|
2024
|
+
cache.set(id, m);
|
|
2025
|
+
return m;
|
|
2026
|
+
};
|
|
2027
|
+
})();
|
|
2028
|
+
for (const mp of allProfiles) {
|
|
2029
|
+
for (const [source, pairs] of Object.entries(mp.mappings)) {
|
|
2030
|
+
// C22 — direction: the source is a LOCAL component. A namespaced
|
|
2031
|
+
// source (NS#id) maps a reference element — the wrong way round.
|
|
2032
|
+
if (source.includes('#')) {
|
|
2033
|
+
err('C22', `map_profile ${mp.namespace}: mapping source "${source}" is a namespaced reference element — the mapping direction is implementation → reference (mapping-direction)`);
|
|
2034
|
+
}
|
|
2035
|
+
else if (!compIds.has(source)) {
|
|
2036
|
+
err('C21', `map_profile ${mp.namespace}: mapping source "${source}" is not a declared component (mapping-resolves)`);
|
|
2037
|
+
}
|
|
2038
|
+
for (const pair of pairs) {
|
|
2039
|
+
const ref = (0, mapping_coverage_1.parseTargetRef)(pair.target, mp.namespace);
|
|
2040
|
+
if (ref.namespace !== mp.namespace) {
|
|
2041
|
+
err('C21', `map_profile ${mp.namespace}: target "${pair.target}" carries namespace "${ref.namespace}" ≠ the profile namespace "${mp.namespace}" (Namespace#ElementID aliasing) (mapping-resolves)`);
|
|
2042
|
+
}
|
|
2043
|
+
if (localNs && ref.namespace === localNs) {
|
|
2044
|
+
err('C22', `map_profile ${mp.namespace}: target "${pair.target}" is in the model's own namespace — mapping to one's own components inverts the direction (implementation → reference) (mapping-direction)`);
|
|
2045
|
+
}
|
|
2046
|
+
if (!compIds.has(ref.qualified)) {
|
|
2047
|
+
err('C21', `map_profile ${mp.namespace}: target "${ref.qualified}" has no declared alias element — the implementation model declares local copies of the reference elements it maps to (mapping-resolves)`);
|
|
2048
|
+
}
|
|
2049
|
+
// C25 — a mapping without description is a warning at audit
|
|
2050
|
+
// strictness (silent in normal mode).
|
|
2051
|
+
if (strictness === 'audit' && !pair.description) {
|
|
2052
|
+
warn('C25', `map_profile ${mp.namespace}: mapping ${source} -> ${ref.qualified} has no description — how the fulfilment works is demanded at audit strictness (mapping-description)`);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
// C23 — mapping-calculus-consistency: coverage is COMPUTED, not
|
|
2058
|
+
// authored. An authored assertion that disagrees with the calculus is
|
|
2059
|
+
// an error — both the per-pair `coverage` on a mapping (its target is
|
|
2060
|
+
// directly mapped, hence fully covered) and the profile-level
|
|
2061
|
+
// `coverage { … }` tripwires over arbitrary reference components. The
|
|
2062
|
+
// reference tree comes from options.references when supplied, else
|
|
2063
|
+
// from the package's own Namespace#… alias forest.
|
|
2064
|
+
//
|
|
2065
|
+
// Usability notes when options.references is ABSENT: the alias forest
|
|
2066
|
+
// is built from the package's own `Namespace#ElementID` process
|
|
2067
|
+
// declarations, which are FRESH declarations — an alias of a gateway
|
|
2068
|
+
// parent must itself re-declare `child_composition gateway`, or the
|
|
2069
|
+
// alias aggregates with the default `all` and assertions compute
|
|
2070
|
+
// against the wrong composition. And closure proposals fire only at
|
|
2071
|
+
// FULL aggregation (every child full) — a gateway parent aggregated to
|
|
2072
|
+
// minimal/partial proposes nothing, so absence of a proposal is not
|
|
2073
|
+
// absence of cover.
|
|
2074
|
+
for (const mp of allProfiles) {
|
|
2075
|
+
const pairAssertions = Object.entries(mp.mappings).flatMap(([source, pairs]) => pairs
|
|
2076
|
+
.filter(p => p.coverage)
|
|
2077
|
+
.map(pair => ({
|
|
2078
|
+
source,
|
|
2079
|
+
pair,
|
|
2080
|
+
ref: (0, mapping_coverage_1.parseTargetRef)(pair.target, mp.namespace),
|
|
2081
|
+
})));
|
|
2082
|
+
const profileAssertions = Object.entries(mp.coverage ?? {}).map(([ref, level]) => ({ ref: (0, mapping_coverage_1.parseTargetRef)(ref, mp.namespace), level }));
|
|
2083
|
+
if (pairAssertions.length === 0 && profileAssertions.length === 0) {
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
const reference = options.references?.[mp.namespace];
|
|
2087
|
+
const forest = reference
|
|
2088
|
+
? (0, mapping_coverage_1.buildProcessTree)(reference)
|
|
2089
|
+
: (0, mapping_coverage_1.buildProcessTree)(standard, { idPrefix: mp.namespace + '#' });
|
|
2090
|
+
const report = (0, mapping_coverage_1.computeCoverage)(standard, forest, (0, mapping_coverage_1.mappingsFromProfile)(mp, localNs), mp.namespace, { implementationId: localNs, referenceId: mp.namespace });
|
|
2091
|
+
const computed = new Map(report.components.map(c => [c.id, c.coverage]));
|
|
2092
|
+
for (const a of pairAssertions) {
|
|
2093
|
+
const level = computed.get(a.ref.id);
|
|
2094
|
+
if (level === undefined) {
|
|
2095
|
+
continue; // C21 already flags the unresolvable target
|
|
2096
|
+
}
|
|
2097
|
+
if (level !== a.pair.coverage) {
|
|
2098
|
+
err('C23', `map_profile ${mp.namespace}: ${a.source} -> ${a.ref.qualified} asserts coverage "${a.pair.coverage}" but the calculus computes "${level}" — coverage is computed, not authored (mapping-calculus-consistency)`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
for (const a of profileAssertions) {
|
|
2102
|
+
const level = computed.get(a.ref.id);
|
|
2103
|
+
if (level === undefined) {
|
|
2104
|
+
err('C23', `map_profile ${mp.namespace}: coverage assertion for "${a.ref.qualified}" names a component outside the reference tree (mapping-calculus-consistency)`);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (level !== a.level) {
|
|
2108
|
+
err('C23', `map_profile ${mp.namespace}: coverage assertion "${a.ref.qualified}: ${a.level}" disagrees with the computed "${level}" — coverage is computed, not authored (mapping-calculus-consistency)`);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
// C24 — import-not-mapping: an import (uses/extends) is structural
|
|
2113
|
+
// inclusion; a mapping is a fulfilment claim. Neither may be expressed
|
|
2114
|
+
// as the other (concept doc §5.6 b) — one rule, both directions: the
|
|
2115
|
+
// imported set and the mapped set must be disjoint.
|
|
2116
|
+
//
|
|
2117
|
+
// Supply-chain exemption (TODO.roadmap/36, doctrine ch. 15 §15.3): an
|
|
2118
|
+
// ABSTRACT IMPORT of a product reference package is reference content
|
|
2119
|
+
// cited at a pinned edition, never structural inclusion — the composer
|
|
2120
|
+
// does not merge it (ser-des/package.ts). Mode-1 consumption IS a
|
|
2121
|
+
// mapping ("the user's model maps its usage to the product's promised
|
|
2122
|
+
// aspects"), so a map_profile whose namespace is an imported
|
|
2123
|
+
// product_reference package is the intended pattern, not a violation.
|
|
2124
|
+
const imported = new Set([
|
|
2125
|
+
standard.packageManifest?.extends,
|
|
2126
|
+
...(standard.packageManifest?.uses ?? []),
|
|
2127
|
+
].filter((x) => !!x));
|
|
2128
|
+
for (const mp of allProfiles) {
|
|
2129
|
+
if (imported.has(mp.namespace)) {
|
|
2130
|
+
if (resolveDepManifest(mp.namespace)?.kind === 'product_reference') {
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
err('C24', `map_profile ${mp.namespace}: namespace "${mp.namespace}" is imported (uses/extends) — an import may not be expressed as a mapping, nor a mapping as an import; inclusion ≠ fulfilment (import-not-mapping)`);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
// C26 — view-read-only: a view is a read-only lens. It may only name
|
|
2137
|
+
// declared elements, and its `against` reference must be a namespace
|
|
2138
|
+
// the model actually maps to — a view never adds, removes, or edits
|
|
2139
|
+
// the mappings of the model it reads (the grammar gives view_profile
|
|
2140
|
+
// no mapping slots, and applyView returns a frozen projection).
|
|
2141
|
+
const mappedNamespaces = new Set(allProfiles.map(p => p.namespace));
|
|
2142
|
+
for (const vp of standard.viewProfiles ?? []) {
|
|
2143
|
+
for (const el of vp.visibleElements ?? []) {
|
|
2144
|
+
if (!compIds.has(el)) {
|
|
2145
|
+
err('C26', `view_profile ${vp.id}: visible element "${el}" is not a declared component — a view reads the model, it does not invent it (view-read-only)`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (vp.against && !mappedNamespaces.has(vp.against)) {
|
|
2149
|
+
err('C26', `view_profile ${vp.id}: against "${vp.against}" names no declared map_profile/mapSet namespace — a view reads coverage, it does not create it (view-read-only)`);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
// ── C81–C83: the model supply chain (TODO.roadmap/36; doctrine ch. 15
|
|
2153
|
+
// §15.9) ──
|
|
2154
|
+
// Three publishers — the standard (reference), the manufacturer
|
|
2155
|
+
// (product reference), the user (implementation) — and mapping is the
|
|
2156
|
+
// only relation between them. C81: a product reference package's
|
|
2157
|
+
// declaration resolves (manufacturer, product designation, and the
|
|
2158
|
+
// standards-reference packages it maps to — its map profiles and its
|
|
2159
|
+
// maps_to manifest register must agree, and every maps_to target must
|
|
2160
|
+
// resolve). C82: an unmapped IS promise is a brochure claim — flagged
|
|
2161
|
+
// at authoring (a warning; the mapping is the conformance claim made
|
|
2162
|
+
// computable). C83: abstract imports pin a version — no unpinned
|
|
2163
|
+
// reference consumption, and the pin resolves against the product
|
|
2164
|
+
// package's edition register.
|
|
2165
|
+
const productManifest = standard.packageManifest;
|
|
2166
|
+
if (productManifest?.kind === 'product_reference') {
|
|
2167
|
+
if (!productManifest.manufacturer) {
|
|
2168
|
+
err('C81', `package "${productManifest.id}": a product reference package declares its manufacturer — the model speaks for them (product-maps-resolves)`);
|
|
2169
|
+
}
|
|
2170
|
+
if (!productManifest.product) {
|
|
2171
|
+
err('C81', `package "${productManifest.id}": a product reference package declares its product designation (product-maps-resolves)`);
|
|
2172
|
+
}
|
|
2173
|
+
const mapsTo = productManifest.mapsTo ?? [];
|
|
2174
|
+
if (mapsTo.length === 0) {
|
|
2175
|
+
err('C81', `package "${productManifest.id}": a product reference package declares the standards-reference packages it maps to (maps_to) — an unmapped product model is a brochure (product-maps-resolves)`);
|
|
2176
|
+
}
|
|
2177
|
+
for (const target of mapsTo) {
|
|
2178
|
+
const dm = resolveDepManifest(target);
|
|
2179
|
+
if (!dm) {
|
|
2180
|
+
err('C81', `package "${productManifest.id}": maps_to "${target}" does not resolve to a known package — the standards reference a product maps to must resolve (product-maps-resolves)`);
|
|
2181
|
+
}
|
|
2182
|
+
else if (dm.kind === 'product_reference') {
|
|
2183
|
+
err('C81', `package "${productManifest.id}": maps_to "${target}" is itself a product reference package — a product maps to the standard, never to another product (product-maps-resolves)`);
|
|
2184
|
+
}
|
|
2185
|
+
if (!mappedNamespaces.has(target)) {
|
|
2186
|
+
err('C81', `package "${productManifest.id}": maps_to "${target}" names a standards reference the model never maps to — declare the map_profile/.prm mapSet or drop the entry (product-maps-resolves)`);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
for (const ns of mappedNamespaces) {
|
|
2190
|
+
if (!mapsTo.includes(ns)) {
|
|
2191
|
+
err('C81', `map_profile ${ns}: the model maps to a standards reference it does not declare in its manifest maps_to register (product-maps-resolves)`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
// C82 — every block-form IS promise is a mapping SOURCE (statement-
|
|
2195
|
+
// only shorthand entries are prose, not mappable claims).
|
|
2196
|
+
const mappedSources = new Set();
|
|
2197
|
+
for (const mp of allProfiles) {
|
|
2198
|
+
for (const source of Object.keys(mp.mappings)) {
|
|
2199
|
+
mappedSources.add(source);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
for (const s of standard.subjects ?? []) {
|
|
2203
|
+
for (const p of s.is?.promises ?? []) {
|
|
2204
|
+
if (!p.id) {
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
if (!mappedSources.has(p.id)) {
|
|
2208
|
+
warn('C82', `subject ${s.id}: promise "${p.id}" is not mapped to any standard — an unmapped promise is a brochure claim; the mapping is the conformance claim made computable (product-unmapped-promises)`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
// C83 — abstract-import-pinned: a uses/extends edge to a product
|
|
2214
|
+
// reference package carries a version pin (uses { <id>@<edition> }),
|
|
2215
|
+
// and the pin resolves against the product package's edition register
|
|
2216
|
+
// (the C80 register discipline applied to the import edge).
|
|
2217
|
+
if (productManifest) {
|
|
2218
|
+
for (const dep of (0, package_1.effectiveUses)(productManifest)) {
|
|
2219
|
+
const dm = resolveDepManifest(dep);
|
|
2220
|
+
if (dm?.kind !== 'product_reference') {
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const pin = productManifest.usePins?.[dep];
|
|
2224
|
+
if (!pin) {
|
|
2225
|
+
err('C83', `package "${productManifest.id}" imports product reference package "${dep}" without a version pin — abstract imports pin a version (uses { ${dep}@<edition> }); no unpinned reference consumption (abstract-import-pinned)`);
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
const register = new Set([
|
|
2229
|
+
...(dm.editions ?? []),
|
|
2230
|
+
...(dm.version ? [dm.version] : []),
|
|
2231
|
+
]);
|
|
2232
|
+
if (register.size > 0 && !register.has(pin)) {
|
|
2233
|
+
err('C83', `package "${productManifest.id}" pins "${dep}" at "${pin}", which does not resolve against the product package's edition register { ${[...register].join(' ')} } — the pin names a declared edition (abstract-import-pinned)`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
// ── C97–C98: the certification program (TODO.v2/01;
|
|
2238
|
+
// analysis/twin-certification-design.md Q4) ──
|
|
2239
|
+
// A fourth publisher with the product_reference shape: the scheme
|
|
2240
|
+
// operator's program is related to recs and product packages by
|
|
2241
|
+
// MAPPING and pinned abstract imports only — composed into nothing.
|
|
2242
|
+
// C97: the program's maps_to register resolves (the C81-class
|
|
2243
|
+
// resolution discipline — the same resolveDepManifest locator/sibling
|
|
2244
|
+
// scan): the program maps to the rec requirement vocabulary it
|
|
2245
|
+
// certifies against, never to another program, and product twin
|
|
2246
|
+
// declarations enter as pinned abstract imports (uses @edition, C83),
|
|
2247
|
+
// never as maps_to entries. C98: a program declaring a no-surveillance
|
|
2248
|
+
// ISO/IEC 17067 scheme shape (type_1a/1b — sample-only; Table 1) while
|
|
2249
|
+
// declaring surveillance machinery (a monitor — the continuous-
|
|
2250
|
+
// evaluation primitive — or a surveillance-classified process) is a
|
|
2251
|
+
// WARNING: the type_1a/1b shape structurally can't say "continuously".
|
|
2252
|
+
if (productManifest?.kind === 'certification_program') {
|
|
2253
|
+
const mapsTo = productManifest.mapsTo ?? [];
|
|
2254
|
+
if (mapsTo.length === 0) {
|
|
2255
|
+
err('C97', `package "${productManifest.id}": a certification program declares the Recommendation vocabulary it certifies against (maps_to) — a program that maps to nothing certifies nothing (program-maps-resolves)`);
|
|
2256
|
+
}
|
|
2257
|
+
for (const target of mapsTo) {
|
|
2258
|
+
const dm = resolveDepManifest(target);
|
|
2259
|
+
if (!dm) {
|
|
2260
|
+
err('C97', `package "${productManifest.id}": maps_to "${target}" does not resolve to a known package — the requirement vocabulary a program certifies against must resolve (program-maps-resolves)`);
|
|
2261
|
+
}
|
|
2262
|
+
else if (dm.kind === 'certification_program') {
|
|
2263
|
+
err('C97', `package "${productManifest.id}": maps_to "${target}" is itself a certification program — a program maps to the standards vocabulary, never to another program (program-maps-resolves)`);
|
|
2264
|
+
}
|
|
2265
|
+
else if (dm.kind === 'product_reference') {
|
|
2266
|
+
err('C97', `package "${productManifest.id}": maps_to "${target}" is a product reference package — the product twin declarations a program certifies are pinned abstract imports (uses { ${target}@<edition> }, C83), never maps_to entries (program-maps-resolves)`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
// C98 — the no-surveillance scheme shapes (ISO/IEC 17067 Table 1:
|
|
2270
|
+
// type 1a/1b carry no surveillance) versus the program's declared
|
|
2271
|
+
// surveillance machinery. A monitor IS a continuous claim ("without
|
|
2272
|
+
// triggers, 'continuous' has no clock" — types/Monitor.ts); an
|
|
2273
|
+
// activity_kind { surveillance } process is a surveillance claim.
|
|
2274
|
+
const SCHEME_TYPES_WITHOUT_SURVEILLANCE = new Set(['type_1a', 'type_1b']);
|
|
2275
|
+
if (productManifest.schemeType &&
|
|
2276
|
+
SCHEME_TYPES_WITHOUT_SURVEILLANCE.has(productManifest.schemeType)) {
|
|
2277
|
+
const claims = [];
|
|
2278
|
+
if ((standard.monitors ?? []).length > 0) {
|
|
2279
|
+
claims.push(`${standard.monitors.length} monitor(s): ${standard.monitors.map(m => m.id).join(', ')}`);
|
|
2280
|
+
}
|
|
2281
|
+
const surveillanceProcesses = (standard.processes ?? []).filter(p => (p.activityKinds ?? []).includes('surveillance'));
|
|
2282
|
+
if (surveillanceProcesses.length > 0) {
|
|
2283
|
+
claims.push(`surveillance-classified process(es): ${surveillanceProcesses.map(p => p.id).join(', ')}`);
|
|
2284
|
+
}
|
|
2285
|
+
if (claims.length > 0) {
|
|
2286
|
+
warn('C98', `package "${productManifest.id}": scheme_type "${productManifest.schemeType}" certifies a type sample with no surveillance, but the program declares ${claims.join(' and ')} — the ${productManifest.schemeType} shape structurally can't say "continuously"; a surveillance-constitutive program self-classifies type_5 (ISO/IEC 17067 Table 1) (program-surveillance-required)`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
// ── C99: the probe-channel provenance facet (smart TODO.v2/01 TCD-2;
|
|
2291
|
+
// analysis/twin-certification-design.md Q2) ──
|
|
2292
|
+
// A measured reference variable declares the physical-side channel its
|
|
2293
|
+
// reading arrives by: reference_instrument (a traceable reference, cited
|
|
2294
|
+
// by equipment-register id) | observer_attestation (a verification
|
|
2295
|
+
// officer reads the physical display into the evidence form — admitted
|
|
2296
|
+
// with the DECLARED traceability limitation, controller decision 3:
|
|
2297
|
+
// "twin ≡ display, not twin ≡ mass" is data, never a comment) |
|
|
2298
|
+
// sim_ground_truth (the acceptance environment only). The kernel checks
|
|
2299
|
+
// shape and vocabulary, register-free; the ref's RESOLUTION against the
|
|
2300
|
+
// equipment/personnel/sim registers is the smart-side linker's R45.
|
|
2301
|
+
{
|
|
2302
|
+
const PROBE_CHANNELS = new Set([
|
|
2303
|
+
'reference_instrument',
|
|
2304
|
+
'observer_attestation',
|
|
2305
|
+
'sim_ground_truth',
|
|
2306
|
+
]);
|
|
2307
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
2308
|
+
const variableNames = new Set((t.variables ?? []).map(v => v.name));
|
|
2309
|
+
for (const v of t.variables ?? []) {
|
|
2310
|
+
const p = v.provenance;
|
|
2311
|
+
if (!p) {
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (!PROBE_CHANNELS.has(p.channel)) {
|
|
2315
|
+
err('C99', `conformance test ${t.id}: variable "${v.name}" declares provenance channel "${p.channel}" — the probe channel is one of reference_instrument | observer_attestation | sim_ground_truth (variable-provenance-channel)`);
|
|
2316
|
+
}
|
|
2317
|
+
if (p.ref === '') {
|
|
2318
|
+
err('C99', `conformance test ${t.id}: variable "${v.name}" declares channel "${p.channel}" but cites no ref — the channel's register entry (equipment-register id, personnel/participant id, or sim deployment id) is the provenance's substance (variable-provenance-channel)`);
|
|
2319
|
+
}
|
|
2320
|
+
if (p.observedAt === '') {
|
|
2321
|
+
err('C99', `conformance test ${t.id}: variable "${v.name}" declares no observed_at binding — a channel reading without its observation timestamp cannot be paired (variable-provenance-channel)`);
|
|
2322
|
+
}
|
|
2323
|
+
else if (!variableNames.has(p.observedAt)) {
|
|
2324
|
+
err('C99', `conformance test ${t.id}: variable "${v.name}" binds observed_at to "${p.observedAt}", which names no declared variable of the test — the observation timestamp lands in a declared variable (variable-provenance-channel)`);
|
|
2325
|
+
}
|
|
2326
|
+
if (p.channel === 'observer_attestation' && p.limitation === '') {
|
|
2327
|
+
err('C99', `conformance test ${t.id}: variable "${v.name}" declares the observer_attestation channel without its traceability limitation — attestation-only evidence proves "twin ≡ display", not "twin ≡ mass"; the limitation is DECLARED data, never a comment (variable-provenance-channel)`);
|
|
2328
|
+
}
|
|
2329
|
+
if (p.channel !== 'observer_attestation' && p.limitation !== '') {
|
|
2330
|
+
warn('C99', `conformance test ${t.id}: variable "${v.name}" carries a traceability limitation on the ${p.channel} channel — the limitation declaration rides the observer_attestation channel (variable-provenance-channel)`);
|
|
2331
|
+
}
|
|
2332
|
+
if (v.source !== '' && v.source !== 'measured') {
|
|
2333
|
+
warn('C99', `conformance test ${t.id}: variable "${v.name}" is source ${v.source} but declares a physical probe channel — the channel provenance belongs to a measured reference reading (variable-provenance-channel)`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
// ── C86–C88: the model-native DPP (TODO.roadmap/35; doctrine ch. 14
|
|
2339
|
+
// §14.6, ch. 15 §15.6/§15.9) ──
|
|
2340
|
+
// The passport is the product model's named, access-classed PROJECTION —
|
|
2341
|
+
// "it cannot drift from the model because it *is* the model" (§14.6).
|
|
2342
|
+
// §15.9's passport rule: "the passport projection contains only aspects
|
|
2343
|
+
// that resolve — public classes contain nothing marked restricted".
|
|
2344
|
+
// C86: every content entry's class is one of the six declared classes,
|
|
2345
|
+
// and a qualified `<class>.<ref>` resolves against the package's
|
|
2346
|
+
// declared aspects/promises (sustainability refs pass unjudged — the
|
|
2347
|
+
// ESPR delegated-act content models do not exist yet, TODO.roadmap/35
|
|
2348
|
+
// "Out"). C87: an entry marked restricted/authority the public class
|
|
2349
|
+
// reaches (the same entry, or a covering bare class) is a leak. C88:
|
|
2350
|
+
// the UPI scheme declares its pattern and its ESPR level
|
|
2351
|
+
// (model | batch | item). Packages without a passport are untouched —
|
|
2352
|
+
// the loop is empty.
|
|
2353
|
+
{
|
|
2354
|
+
const KNOWN_CONTENT = new Set(Passport_1.PASSPORT_CONTENT_CLASSES);
|
|
2355
|
+
const KNOWN_LEVELS = new Set(Passport_1.PASSPORT_UPI_LEVELS);
|
|
2356
|
+
const entryToken = (e) => e.ref === '' ? e.contentClass : `${e.contentClass}.${e.ref}`;
|
|
2357
|
+
// The per-class resolution domains for qualified entries (C86): the
|
|
2358
|
+
// package's declared aspects/promises, one vocabulary per class.
|
|
2359
|
+
const passportPromiseIds = new Set((standard.subjects ?? []).flatMap(s => (s.is.promises ?? []).map(pr => pr.id).filter(id => id !== '')));
|
|
2360
|
+
const compositionIds = new Set((standard.subjects ?? []).flatMap(s => [
|
|
2361
|
+
...(s.is.structure ?? []),
|
|
2362
|
+
...Object.keys(s.is.designParameters ?? {}),
|
|
2363
|
+
...Object.keys(s.has.characteristics ?? {}),
|
|
2364
|
+
...(s.has.serves ?? []).map(sv => sv.aspect),
|
|
2365
|
+
]));
|
|
2366
|
+
const passportArtifactIds = new Set([
|
|
2367
|
+
...(standard.artifactDefinitions ?? []).map(a => a.id),
|
|
2368
|
+
...(standard.subjects ?? []).flatMap(s => s.is.artifacts ?? []),
|
|
2369
|
+
]);
|
|
2370
|
+
const passportMonitorIds = new Set((standard.monitors ?? []).map(m => m.id));
|
|
2371
|
+
// null = the class has no kernel resolution domain (sustainability —
|
|
2372
|
+
// the forward class; its refs pass unjudged).
|
|
2373
|
+
const CONTENT_DOMAINS = {
|
|
2374
|
+
identity: subjectIds,
|
|
2375
|
+
composition: compositionIds,
|
|
2376
|
+
promises_as_verified: passportPromiseIds,
|
|
2377
|
+
live_compliance_status: new Set([
|
|
2378
|
+
...passportMonitorIds,
|
|
2379
|
+
...passportPromiseIds,
|
|
2380
|
+
]),
|
|
2381
|
+
artifacts: passportArtifactIds,
|
|
2382
|
+
sustainability: null,
|
|
2383
|
+
};
|
|
2384
|
+
for (const p of standard.passports ?? []) {
|
|
2385
|
+
// C86 — passport-content-resolves (§15.9).
|
|
2386
|
+
for (const e of p.entries) {
|
|
2387
|
+
if (!KNOWN_CONTENT.has(e.contentClass)) {
|
|
2388
|
+
err('C86', `passport ${p.id}: content entry "${entryToken(e)}" names an unknown content class — one of ${Passport_1.PASSPORT_CONTENT_CLASSES.join(' | ')} (passport-content-resolves)`);
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
if (e.ref !== '') {
|
|
2392
|
+
const domain = CONTENT_DOMAINS[e.contentClass];
|
|
2393
|
+
if (domain !== null && !domain.has(e.ref)) {
|
|
2394
|
+
err('C86', `passport ${p.id}: content entry "${entryToken(e)}" — "${e.ref}" does not resolve against the package's declared aspects/promises (the passport projection contains only aspects that resolve, §15.9) (passport-content-resolves)`);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
// C87 — passport-access-leak (§15.9): an entry marked restricted/
|
|
2399
|
+
// authority is reachable from the public class when the public
|
|
2400
|
+
// block carries the same entry or a covering bare class. (The
|
|
2401
|
+
// reverse narrowing — a bare restricted class with one ref made
|
|
2402
|
+
// public — is an authored exception, not a leak.)
|
|
2403
|
+
const publicEntries = p.entries.filter(e => e.access === 'public');
|
|
2404
|
+
const publicTokens = new Set(publicEntries.map(entryToken));
|
|
2405
|
+
const publicBareClasses = new Set(publicEntries.filter(e => e.ref === '').map(e => e.contentClass));
|
|
2406
|
+
for (const e of p.entries) {
|
|
2407
|
+
if (e.access !== 'restricted' && e.access !== 'authority') {
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (publicTokens.has(entryToken(e)) ||
|
|
2411
|
+
publicBareClasses.has(e.contentClass)) {
|
|
2412
|
+
err('C87', `passport ${p.id}: entry "${entryToken(e)}" is marked ${e.access} but reachable from the public class — public classes contain nothing marked restricted (§15.9) (passport-access-leak)`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
// C88 — passport-upi-scheme (§14.6; ESPR): the unique product
|
|
2416
|
+
// identifier declares its pattern and its level.
|
|
2417
|
+
if (p.upi.pattern === '') {
|
|
2418
|
+
err('C88', `passport ${p.id}: declares no UPI pattern — a passport is reached through its unique product identifier (upi { pattern … level … }) (passport-upi-scheme)`);
|
|
2419
|
+
}
|
|
2420
|
+
if (p.upi.level === '') {
|
|
2421
|
+
err('C88', `passport ${p.id}: the UPI pattern declares no level — the ESPR unique product identifier is model | batch | item level (passport-upi-scheme)`);
|
|
2422
|
+
}
|
|
2423
|
+
else if (!KNOWN_LEVELS.has(p.upi.level)) {
|
|
2424
|
+
err('C88', `passport ${p.id}: UPI level "${p.upi.level}" is not one of ${Passport_1.PASSPORT_UPI_LEVELS.join(' | ')} (passport-upi-scheme)`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
// ── C90–C91: the architecture invariants (smart gap-close E9,
|
|
2429
|
+
// analysis/architecture-gaps-2026-07.md; smart
|
|
2430
|
+
// docs/oiml-core/09-invariants.md) ──
|
|
2431
|
+
// The `invariant` construct is the first-class replacement for the
|
|
2432
|
+
// note-family encoding. C90 invariant-shape: every invariant carries
|
|
2433
|
+
// name, statement, and severity (severity is PRESENCE-judged only —
|
|
2434
|
+
// the smart side owns the severity vocabulary), and enforcement is a
|
|
2435
|
+
// non-empty claim list XOR the literal `aspirational` marker — never
|
|
2436
|
+
// empty, never both. C91 invariant-enforcement-grammar: every claim
|
|
2437
|
+
// matches kernel:C<n> | linker:<kebab-name> | gate:<kebab-name>, and
|
|
2438
|
+
// the aspirational marker never appears inside a claims list. Claim
|
|
2439
|
+
// TARGET resolution (does the linker rule exist, does the gate have
|
|
2440
|
+
// evidence) is the smart-side linker rule R38's crosswalk — the
|
|
2441
|
+
// kernel checks syntax/shape only. Packages without invariants are
|
|
2442
|
+
// untouched — the loop is empty.
|
|
2443
|
+
{
|
|
2444
|
+
const CLAIM_GRAMMAR = [
|
|
2445
|
+
/^kernel:C\d+$/,
|
|
2446
|
+
/^linker:[a-z0-9-]+$/,
|
|
2447
|
+
/^gate:[a-z0-9-]+$/,
|
|
2448
|
+
];
|
|
2449
|
+
for (const inv of standard.invariants ?? []) {
|
|
2450
|
+
// C90 — invariant-shape.
|
|
2451
|
+
if (inv.name === '') {
|
|
2452
|
+
err('C90', `invariant ${inv.id}: declares no name — an invariant carries a short rule name (invariant-shape)`);
|
|
2453
|
+
}
|
|
2454
|
+
if (inv.statement === '') {
|
|
2455
|
+
err('C90', `invariant ${inv.id}: declares no statement — an invariant carries the rule's prose (the default spelling inline; alternates ride text ${inv.id}.statement blocks) (invariant-shape)`);
|
|
2456
|
+
}
|
|
2457
|
+
if (inv.severity === '') {
|
|
2458
|
+
err('C90', `invariant ${inv.id}: declares no severity (invariant-shape)`);
|
|
2459
|
+
}
|
|
2460
|
+
const e = inv.enforcement;
|
|
2461
|
+
if (!e.aspirational && e.claims.length === 0) {
|
|
2462
|
+
err('C90', `invariant ${inv.id}: declares no enforcement — a non-empty claim list (enforcement { kernel:C<n> … }) or the literal marker (enforcement aspirational) (invariant-shape)`);
|
|
2463
|
+
}
|
|
2464
|
+
if (e.aspirational && e.claims.length > 0) {
|
|
2465
|
+
err('C90', `invariant ${inv.id}: enforcement mixes the aspirational marker with ${e.claims.length} claim(s) — one or the other, never both (invariant-shape)`);
|
|
2466
|
+
}
|
|
2467
|
+
// C91 — invariant-enforcement-grammar (syntax only; the smart
|
|
2468
|
+
// side's R38 owns target resolution).
|
|
2469
|
+
for (const claim of e.claims) {
|
|
2470
|
+
if (claim === 'aspirational') {
|
|
2471
|
+
err('C91', `invariant ${inv.id}: the aspirational marker never appears inside a claims list — spell it bare (enforcement aspirational) (invariant-enforcement-grammar)`);
|
|
2472
|
+
}
|
|
2473
|
+
else if (!CLAIM_GRAMMAR.some(re => re.test(claim))) {
|
|
2474
|
+
err('C91', `invariant ${inv.id}: enforcement claim "${claim}" does not match kernel:C<n> | linker:<kebab-name> | gate:<kebab-name> (invariant-enforcement-grammar)`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
// ── C92–C93: the required test orderings (smart gap-close E10,
|
|
2480
|
+
// analysis/architecture-gaps-2026-07.md; the smart contract
|
|
2481
|
+
// data/schemas/test-sequences.yaml) ──
|
|
2482
|
+
// The `test_sequence` construct is the first-class replacement for the
|
|
2483
|
+
// hand-authored supplemental test-sequences.yaml. C92
|
|
2484
|
+
// test-sequence-shape: every sequence carries name, description, and a
|
|
2485
|
+
// non-empty steps list; every step's order is a positive integer
|
|
2486
|
+
// unique in the sequence; every step carries test XOR phase; role
|
|
2487
|
+
// appears only on test steps and only in the baseline | follow_up
|
|
2488
|
+
// vocabulary; depends_on is an integer. C93 test-sequence-integrity:
|
|
2489
|
+
// every depends_on names the order of an EARLIER step of the same
|
|
2490
|
+
// sequence — never itself (self-reference), never a later step
|
|
2491
|
+
// (forward reference), never an order no step declares. With
|
|
2492
|
+
// single-parent earlier-order edges a cycle is impossible by
|
|
2493
|
+
// construction (a cycle needs at least one non-descending edge, which
|
|
2494
|
+
// the per-edge checks flag), so the three explicit per-edge checks
|
|
2495
|
+
// subsume cycle detection. Test-ref RESOLUTION (does /conf/… name a
|
|
2496
|
+
// declared conformance test of the standard) is the smart-side linker
|
|
2497
|
+
// rule R39's job — the kernel checks syntax/shape only, exactly like
|
|
2498
|
+
// E9's C90/C91 vs R38 split. Packages without test sequences are
|
|
2499
|
+
// untouched — the loop is empty.
|
|
2500
|
+
{
|
|
2501
|
+
for (const seq of standard.testSequences ?? []) {
|
|
2502
|
+
// C92 — test-sequence-shape.
|
|
2503
|
+
if (seq.name === '') {
|
|
2504
|
+
err('C92', `test_sequence ${seq.id}: declares no name — a test sequence carries a short name (test-sequence-shape)`);
|
|
2505
|
+
}
|
|
2506
|
+
if (seq.description === '') {
|
|
2507
|
+
err('C92', `test_sequence ${seq.id}: declares no description — a test sequence carries the ordering's prose (the default spelling inline; alternates ride text ${seq.id}.description blocks) (test-sequence-shape)`);
|
|
2508
|
+
}
|
|
2509
|
+
if (seq.steps.length === 0) {
|
|
2510
|
+
err('C92', `test_sequence ${seq.id}: declares no steps — a test sequence is a non-empty ordered step list (test-sequence-shape)`);
|
|
2511
|
+
}
|
|
2512
|
+
const orders = new Set();
|
|
2513
|
+
for (const step of seq.steps) {
|
|
2514
|
+
const at = step.order !== null ? `step ${step.order}` : 'a step';
|
|
2515
|
+
if (step.order === null ||
|
|
2516
|
+
!Number.isInteger(step.order) ||
|
|
2517
|
+
step.order < 1) {
|
|
2518
|
+
err('C92', `test_sequence ${seq.id}: ${at} declares no positive-integer order (got ${step.order === null ? 'none' : step.order}) — the step order is a positive integer (test-sequence-shape)`);
|
|
2519
|
+
}
|
|
2520
|
+
else if (orders.has(step.order)) {
|
|
2521
|
+
err('C92', `test_sequence ${seq.id}: step order ${step.order} is declared twice — the order is unique within the sequence (test-sequence-shape)`);
|
|
2522
|
+
}
|
|
2523
|
+
else {
|
|
2524
|
+
orders.add(step.order);
|
|
2525
|
+
}
|
|
2526
|
+
if ((step.test === '') === (step.phase === '')) {
|
|
2527
|
+
err('C92', `test_sequence ${seq.id}: ${at} carries ${step.test === ''
|
|
2528
|
+
? 'neither test nor phase'
|
|
2529
|
+
: 'both test and phase'} — exactly one per step (a conformance-test reference XOR an environment-program phase) (test-sequence-shape)`);
|
|
2530
|
+
}
|
|
2531
|
+
if (step.role !== '') {
|
|
2532
|
+
if (step.test === '') {
|
|
2533
|
+
err('C92', `test_sequence ${seq.id}: ${at} declares role "${step.role}" on ${step.phase === ''
|
|
2534
|
+
? 'a step carrying neither test nor phase'
|
|
2535
|
+
: 'a phase step'} — the role is meaningful on test steps only (test-sequence-shape)`);
|
|
2536
|
+
}
|
|
2537
|
+
else if (!TestSequence_1.TEST_SEQUENCE_STEP_ROLES.includes(step.role)) {
|
|
2538
|
+
err('C92', `test_sequence ${seq.id}: ${at} declares role "${step.role}" — the vocabulary is ${TestSequence_1.TEST_SEQUENCE_STEP_ROLES.join(' | ')} (test-sequence-shape)`);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (step.dependsOn !== null && !Number.isInteger(step.dependsOn)) {
|
|
2542
|
+
err('C92', `test_sequence ${seq.id}: ${at} declares depends_on ${step.dependsOn} — the dependency is the integer order of an earlier step (test-sequence-shape)`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
// C93 — test-sequence-integrity (per-edge, explicit: no
|
|
2546
|
+
// self-reference, no forward reference, no dangling order).
|
|
2547
|
+
for (const step of seq.steps) {
|
|
2548
|
+
if (step.dependsOn === null || !Number.isInteger(step.dependsOn)) {
|
|
2549
|
+
continue; // absent, or malformed — C92 owns the shape
|
|
2550
|
+
}
|
|
2551
|
+
const at = step.order !== null ? `step ${step.order}` : 'a step';
|
|
2552
|
+
if (step.order !== null && step.dependsOn === step.order) {
|
|
2553
|
+
err('C93', `test_sequence ${seq.id}: ${at} depends on itself — depends_on names an EARLIER step (test-sequence-integrity)`);
|
|
2554
|
+
}
|
|
2555
|
+
else if (!orders.has(step.dependsOn)) {
|
|
2556
|
+
err('C93', `test_sequence ${seq.id}: ${at} depends_on ${step.dependsOn} names no step of the sequence (test-sequence-integrity)`);
|
|
2557
|
+
}
|
|
2558
|
+
else if (step.order !== null && step.dependsOn > step.order) {
|
|
2559
|
+
err('C93', `test_sequence ${seq.id}: ${at} depends_on ${step.dependsOn} names a LATER step — a forward reference; depends_on names an EARLIER step (test-sequence-integrity)`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
// ── C94: the per-test evaluation-formula traces (smart gap-close E11,
|
|
2565
|
+
// analysis/architecture-gaps-2026-07.md; the smart contract
|
|
2566
|
+
// data/schemas/formulas-used.yaml) ──
|
|
2567
|
+
// The `formulas_used` construct is the first-class replacement for the
|
|
2568
|
+
// hand-authored supplemental formulas-used.yaml. C94
|
|
2569
|
+
// formulas-used-shape: every trace carries a non-empty test reference
|
|
2570
|
+
// (the block symbol — reachable empty only through a quoted-empty id
|
|
2571
|
+
// spelling), name, description, and a non-empty formulas list; every
|
|
2572
|
+
// formula identifier is well-formed — the snake_case shape the
|
|
2573
|
+
// calculations registry uses for output names (FORMULA_ID_SHAPE).
|
|
2574
|
+
// Entry UNIQUENESS per test is the parse-time duplicate-id rule's
|
|
2575
|
+
// (surfaced as C96) — the collection key IS the test reference, so a
|
|
2576
|
+
// second trace for the same test is the same-id declaration every
|
|
2577
|
+
// id-keyed collection judges. Formula-id RESOLUTION (does e_l exist in
|
|
2578
|
+
// the calculations ∪ formulas registries) is the smart-side linker rule
|
|
2579
|
+
// R41's crosswalk
|
|
2580
|
+
// — the kernel checks syntax/shape only, exactly like E9's C90/C91 vs
|
|
2581
|
+
// R38 and E10's C92/C93 vs R39 split. Packages without formulas-used
|
|
2582
|
+
// traces are untouched — the loop is empty.
|
|
2583
|
+
{
|
|
2584
|
+
for (const fu of standard.formulasUsed ?? []) {
|
|
2585
|
+
if ((0, tokenize_1.stripWrapping)(fu.id) === '') {
|
|
2586
|
+
err('C94', `formulas_used ${fu.id}: declares no test reference — the block symbol is the conformance-test reference the trace is keyed by (formulas-used-shape)`);
|
|
2587
|
+
}
|
|
2588
|
+
if (fu.name === '') {
|
|
2589
|
+
err('C94', `formulas_used ${fu.id}: declares no name — a formulas-used trace carries a short name (formulas-used-shape)`);
|
|
2590
|
+
}
|
|
2591
|
+
if (fu.description === '') {
|
|
2592
|
+
err('C94', `formulas_used ${fu.id}: declares no description — a formulas-used trace carries what the listed formulas compute, with the clause evidence (the default spelling inline; alternates ride text ${fu.id}.description blocks) (formulas-used-shape)`);
|
|
2593
|
+
}
|
|
2594
|
+
if (fu.formulas.length === 0) {
|
|
2595
|
+
err('C94', `formulas_used ${fu.id}: declares no formulas — a formulas-used trace is a non-empty registry-formula id list (formulas-used-shape)`);
|
|
2596
|
+
}
|
|
2597
|
+
for (const f of fu.formulas) {
|
|
2598
|
+
if (!FormulasUsed_1.FORMULA_ID_SHAPE.test(f)) {
|
|
2599
|
+
err('C94', `formulas_used ${fu.id}: formula id "${f}" is not a well-formed registry identifier — the snake_case shape the calculations registry uses (^[a-z][a-z0-9_]*$; resolution is the smart-side linker R41's, never the kernel's) (formulas-used-shape)`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
// ── C32–C36: quantities, time, and the IS↔HAS duality ──
|
|
2605
|
+
// (TODO.roadmap/06; doctrine ch. 6). Coherence is judged on quantity
|
|
2606
|
+
// KINDS — resolved through the package's quantity_registers — never on
|
|
2607
|
+
// unit strings.
|
|
2608
|
+
const kindDecl = new Map(); // kind id → register id
|
|
2609
|
+
for (const reg of standard.quantityRegisters ?? []) {
|
|
2610
|
+
for (const k of reg.kinds) {
|
|
2611
|
+
if (!kindDecl.has(k.id)) {
|
|
2612
|
+
kindDecl.set(k.id, reg.id);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
const unitToKind = new Map(); // unit id|symbol → kind id
|
|
2617
|
+
const unitHome = new Map(); // unit id|symbol → register id
|
|
2618
|
+
const registersExist = (standard.quantityRegisters ?? []).length > 0;
|
|
2619
|
+
for (const reg of standard.quantityRegisters ?? []) {
|
|
2620
|
+
for (const u of reg.units) {
|
|
2621
|
+
// C33 — the unit's kind must resolve across the package's registers.
|
|
2622
|
+
if (!kindDecl.has(u.kind)) {
|
|
2623
|
+
err('C33', `quantity_register ${reg.id}: unit "${u.id}" declares kind "${u.kind}", which no quantity_register declares (quantity-coherence)`);
|
|
2624
|
+
}
|
|
2625
|
+
// C33 — no cross-register redefinition: a rec EXTENDS the register
|
|
2626
|
+
// with domain units; it never redefines an existing entry.
|
|
2627
|
+
for (const key of new Set([u.id, u.symbol].filter(s => s))) {
|
|
2628
|
+
const prior = unitHome.get(key);
|
|
2629
|
+
if (prior !== undefined) {
|
|
2630
|
+
err('C33', `quantity_register ${reg.id}: unit "${u.id}" ("${key}") redefines a unit already declared by register "${prior}" — a package extends the register, it never redefines entries (quantity-coherence)`);
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
unitHome.set(key, reg.id);
|
|
2634
|
+
unitToKind.set(key, u.kind);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
const kindOfUnit = (unit) => unit === undefined ? undefined : unitToKind.get(unit.trim());
|
|
2640
|
+
// C32 — INV-1: no bare numbers. A value stated for a declared physical
|
|
2641
|
+
// quantity (the attribute definition carries a unit, declares a
|
|
2642
|
+
// quantity_kind, or is typed QuantityValue) must itself carry a unit.
|
|
2643
|
+
// An empty-string unit token counts as bare, not as unit'd.
|
|
2644
|
+
const attrDefs = standard.attributeDefinitions ?? [];
|
|
2645
|
+
const isPhysicalAttr = (def) => def.unit !== '' ||
|
|
2646
|
+
def.quantityKind !== '' ||
|
|
2647
|
+
def.valueType === 'QuantityValue';
|
|
2648
|
+
const physicalFacet = (def) => def.unit !== ''
|
|
2649
|
+
? `unit "${def.unit}"`
|
|
2650
|
+
: def.valueType === 'QuantityValue'
|
|
2651
|
+
? 'value_type QuantityValue'
|
|
2652
|
+
: `quantity_kind ${def.quantityKind}`;
|
|
2653
|
+
for (const inst of standard.instances ?? []) {
|
|
2654
|
+
const planes = [
|
|
2655
|
+
'attributes',
|
|
2656
|
+
'testContext',
|
|
2657
|
+
];
|
|
2658
|
+
for (const plane of planes) {
|
|
2659
|
+
for (const [key, v] of Object.entries(inst.has?.[plane] ?? {})) {
|
|
2660
|
+
const def = attrDefs.find(a => a.id === key);
|
|
2661
|
+
if (!def) {
|
|
2662
|
+
continue; // C17 reports the undeclared attribute
|
|
2663
|
+
}
|
|
2664
|
+
if (isPhysicalAttr(def) && (v.unit ?? '') === '') {
|
|
2665
|
+
err('C32', `instance ${inst.id}: attribute "${key}" is a declared physical quantity (` +
|
|
2666
|
+
physicalFacet(def) +
|
|
2667
|
+
`) but the stated value is a bare number — INV-1: no bare numbers (inv1-no-bare-quantity)`);
|
|
2668
|
+
}
|
|
2669
|
+
// C33 (warning leg) — a stated unit no register declares is
|
|
2670
|
+
// unmapped (doctrine §6.8: unmapped units are warnings). An empty
|
|
2671
|
+
// unit token is bare (C32 above), not unmapped.
|
|
2672
|
+
if (v.unit !== undefined &&
|
|
2673
|
+
v.unit !== '' &&
|
|
2674
|
+
registersExist &&
|
|
2675
|
+
kindOfUnit(v.unit) === undefined) {
|
|
2676
|
+
warn('C33', `instance ${inst.id}: attribute "${key}" carries unit "${v.unit}", which no quantity_register declares (quantity-coherence)`);
|
|
2677
|
+
}
|
|
2678
|
+
// C33 — stated kind vs the definition's declared kind: an explicit
|
|
2679
|
+
// `kind` on the value, or the stated unit's kind, must agree with
|
|
2680
|
+
// the attribute definition's quantity_kind / declared unit's kind.
|
|
2681
|
+
const statedKind = v.quantityKind ?? kindOfUnit(v.unit);
|
|
2682
|
+
if (def.quantityKind && statedKind && statedKind !== def.quantityKind) {
|
|
2683
|
+
err('C33', `instance ${inst.id}: attribute "${key}" (kind ${def.quantityKind}) stated with ` +
|
|
2684
|
+
(v.quantityKind
|
|
2685
|
+
? `kind "${v.quantityKind}"`
|
|
2686
|
+
: `unit "${v.unit}" (kind ${statedKind})`) +
|
|
2687
|
+
` — quantity kinds differ (quantity-coherence)`);
|
|
2688
|
+
}
|
|
2689
|
+
else if (!def.quantityKind && def.unit && v.unit) {
|
|
2690
|
+
const defKind = kindOfUnit(def.unit);
|
|
2691
|
+
const vKind = kindOfUnit(v.unit);
|
|
2692
|
+
if (defKind && vKind && defKind !== vKind) {
|
|
2693
|
+
err('C33', `instance ${inst.id}: attribute "${key}" declared with unit "${def.unit}" (kind ${defKind}) stated with unit "${v.unit}" (kind ${vKind}) — quantity kinds differ (quantity-coherence)`);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
// C35 — time-typed attributes carry ISO 8601 time values.
|
|
2697
|
+
if (['date', 'datetime', 'duration', 'period'].includes(def.valueType) &&
|
|
2698
|
+
!(0, time_1.isValidTimeValue)(def.valueType, String(v.value))) {
|
|
2699
|
+
err('C35', `instance ${inst.id}: attribute "${key}" (value_type ${def.valueType}) value "${String(v.value)}" is not a valid ISO 8601 ${def.valueType} (time-format)`);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
// C32/C33 — condition-set entries. C32: a NUMERIC entry value (with or
|
|
2705
|
+
// without tolerance) stated without a unit is a bare physical quantity —
|
|
2706
|
+
// INV-1 (free-text values like "local ambient" stay legal). C33: the
|
|
2707
|
+
// entry's unit must measure the entry's declared quantity kind.
|
|
2708
|
+
for (const cs of standard.conditionSets ?? []) {
|
|
2709
|
+
for (const e of cs.entries ?? []) {
|
|
2710
|
+
if (!e.unit) {
|
|
2711
|
+
if (NUMERIC_TOKEN.test(e.value)) {
|
|
2712
|
+
err('C32', `condition_set ${cs.id}: entry "${e.quantityKind}" is a bare physical value — a numeric condition value carries a unit — INV-1: no bare numbers (inv1-no-bare-quantity)`);
|
|
2713
|
+
}
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
const eKind = kindOfUnit(e.unit);
|
|
2717
|
+
if (eKind === undefined) {
|
|
2718
|
+
if (registersExist) {
|
|
2719
|
+
warn('C33', `condition_set ${cs.id}: entry "${e.quantityKind}" carries unit "${e.unit}", which no quantity_register declares (quantity-coherence)`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
else if (kindDecl.has(e.quantityKind) && eKind !== e.quantityKind) {
|
|
2723
|
+
err('C33', `condition_set ${cs.id}: entry "${e.quantityKind}" carries unit "${e.unit}" (kind ${eKind}) — the unit does not measure the entry's quantity kind (quantity-coherence)`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
// C33 — symbols and verdict quantities: a declared quantity_kind must
|
|
2728
|
+
// agree with the declared unit's kind.
|
|
2729
|
+
const unitables = [
|
|
2730
|
+
...(standard.symbols ?? []).map(s => ({
|
|
2731
|
+
kind: 'symbol',
|
|
2732
|
+
id: s.id,
|
|
2733
|
+
unit: s.unit,
|
|
2734
|
+
qk: s.quantityKind,
|
|
2735
|
+
})),
|
|
2736
|
+
...(standard.verdicts ?? []).map(v => ({
|
|
2737
|
+
kind: 'verdict',
|
|
2738
|
+
id: v.id,
|
|
2739
|
+
unit: v.unit,
|
|
2740
|
+
qk: v.quantityKind,
|
|
2741
|
+
})),
|
|
2742
|
+
];
|
|
2743
|
+
for (const u of unitables) {
|
|
2744
|
+
if (!u.unit || !u.qk) {
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
const uKind = kindOfUnit(u.unit);
|
|
2748
|
+
if (uKind && kindDecl.has(u.qk) && uKind !== u.qk) {
|
|
2749
|
+
err('C33', `${u.kind} ${u.id}: declared quantity_kind "${u.qk}" but unit "${u.unit}" measures kind ${uKind} — quantity kinds differ (quantity-coherence)`);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
// C34 — duality-coherence: one value structure, two roles.
|
|
2753
|
+
for (const d of standard.duals ?? []) {
|
|
2754
|
+
if (d.attribute && !attrIds.has(d.attribute)) {
|
|
2755
|
+
err('C34', `dual ${d.id}: attribute "${d.attribute}" is not a declared attribute_definition (duality-coherence)`);
|
|
2756
|
+
}
|
|
2757
|
+
if (!d.designed && !d.exhibited) {
|
|
2758
|
+
err('C34', `dual ${d.id}: neither designed nor exhibited stated — a dual pair carries at least one role (duality-coherence)`);
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
// Tolerance marks the designed side, uncertainty the measured side —
|
|
2762
|
+
// the two never merge (doctrine §6.3).
|
|
2763
|
+
if (d.designed?.uncertainty !== undefined) {
|
|
2764
|
+
warn('C34', `dual ${d.id}: the designed role carries an uncertainty — uncertainty belongs to the measured (exhibited) side (duality-coherence)`);
|
|
2765
|
+
}
|
|
2766
|
+
if (d.exhibited?.tolerance !== undefined) {
|
|
2767
|
+
warn('C34', `dual ${d.id}: the exhibited role carries a tolerance — tolerance belongs to the specified (designed) side (duality-coherence)`);
|
|
2768
|
+
}
|
|
2769
|
+
// INV-1: a role stated for a declared physical quantity carries a unit
|
|
2770
|
+
// (or an explicit quantity kind) — never a bare number. An empty-string
|
|
2771
|
+
// unit token counts as bare, not as unit'd.
|
|
2772
|
+
const dualAttr = d.attribute
|
|
2773
|
+
? attrDefs.find(a => a.id === d.attribute)
|
|
2774
|
+
: undefined;
|
|
2775
|
+
if (dualAttr && isPhysicalAttr(dualAttr)) {
|
|
2776
|
+
for (const [role, v] of [
|
|
2777
|
+
['designed', d.designed],
|
|
2778
|
+
['exhibited', d.exhibited],
|
|
2779
|
+
]) {
|
|
2780
|
+
if (v && (v.unit ?? '') === '' && (v.quantityKind ?? '') === '') {
|
|
2781
|
+
err('C34', `dual ${d.id}: role "${role}" of attribute "${d.attribute}" is a bare physical value — INV-1 (duality-coherence)`);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (d.designed && d.exhibited) {
|
|
2786
|
+
const roleKind = (v) => v.quantityKind ?? kindOfUnit(v.unit);
|
|
2787
|
+
const dk = roleKind(d.designed);
|
|
2788
|
+
const ek = roleKind(d.exhibited);
|
|
2789
|
+
for (const [role, v] of [
|
|
2790
|
+
['designed', d.designed],
|
|
2791
|
+
['exhibited', d.exhibited],
|
|
2792
|
+
]) {
|
|
2793
|
+
if (v.unit !== undefined &&
|
|
2794
|
+
v.unit !== '' &&
|
|
2795
|
+
registersExist &&
|
|
2796
|
+
kindOfUnit(v.unit) === undefined) {
|
|
2797
|
+
warn('C34', `dual ${d.id}: the ${role} role carries unit "${v.unit}", which no quantity_register declares (duality-coherence)`);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (dk && ek && dk !== ek) {
|
|
2801
|
+
err('C34', `dual ${d.id}: designed (kind ${dk}) vs exhibited (kind ${ek}) — the two roles of one quantity must share a quantity kind (duality-coherence)`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
// C35 — timer-event recurrence periods are ISO 8601 durations (closes
|
|
2806
|
+
// task 02's deferred format check; C15 checks presence only).
|
|
2807
|
+
for (const p of standard.processes ?? []) {
|
|
2808
|
+
for (const s of p.does?.steps ?? []) {
|
|
2809
|
+
if (s.kind === 'timer_event' && s.period && !(0, time_1.isDuration)(s.period)) {
|
|
2810
|
+
err('C35', `process ${p.id}: timer_event "${s.id}" period "${s.period}" is not an ISO 8601 duration (time-format)`);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
// C36 — map<K, V> field/value types are well-formed (K = string or an
|
|
2815
|
+
// enum id; V a valid type expression).
|
|
2816
|
+
for (const c of standard.dataclasses ?? []) {
|
|
2817
|
+
for (const a of c.attributes ?? []) {
|
|
2818
|
+
if (/^map\s*</.test(a.type) && !(0, type_expr_1.isWellFormedMapType)(a.type)) {
|
|
2819
|
+
err('C36', `class ${c.id}: attribute "${a.id}" type "${a.type}" is not a well-formed map<K, V> type — K is string or an enum id, V a type expression (map-type)`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
for (const a of standard.attributeDefinitions ?? []) {
|
|
2824
|
+
if (/^map\s*</.test(a.valueType) && !(0, type_expr_1.isWellFormedMapType)(a.valueType)) {
|
|
2825
|
+
err('C36', `attribute_definition ${a.id}: value_type "${a.valueType}" is not a well-formed map<K, V> type — K is string or an enum id, V a type expression (map-type)`);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
const resolveParamType = (type) => {
|
|
2829
|
+
const t = type.trim();
|
|
2830
|
+
if (t === '') {
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
if (kindDecl.has(t)) {
|
|
2834
|
+
return { kind: t };
|
|
2835
|
+
}
|
|
2836
|
+
const k = kindOfUnit(t);
|
|
2837
|
+
if (k !== undefined) {
|
|
2838
|
+
return { kind: k };
|
|
2839
|
+
}
|
|
2840
|
+
return { token: t };
|
|
2841
|
+
};
|
|
2842
|
+
const typesCompatible = (a, b) => {
|
|
2843
|
+
if (a === null || b === null) {
|
|
2844
|
+
return true;
|
|
2845
|
+
}
|
|
2846
|
+
if ('kind' in a && 'kind' in b) {
|
|
2847
|
+
return a.kind === b.kind;
|
|
2848
|
+
}
|
|
2849
|
+
if ('token' in a && 'token' in b) {
|
|
2850
|
+
return a.token === b.token;
|
|
2851
|
+
}
|
|
2852
|
+
return false;
|
|
2853
|
+
};
|
|
2854
|
+
const processById = new Map((standard.processes ?? []).map(q => [q.id, q]));
|
|
2855
|
+
for (const p of standard.processes ?? []) {
|
|
2856
|
+
// The declared type sites of one name: signature in/out + registers.
|
|
2857
|
+
// A step's read/write names carry no type of their own — the boundary
|
|
2858
|
+
// type of a handoff is whatever the declarations say, so a name with
|
|
2859
|
+
// TWO conflicting declarations is the kind-incompatible boundary.
|
|
2860
|
+
const sites = new Map();
|
|
2861
|
+
const noteSites = (pos) => (prm) => {
|
|
2862
|
+
sites.set(prm.name, [
|
|
2863
|
+
...(sites.get(prm.name) ?? []),
|
|
2864
|
+
{ pos, type: prm.type },
|
|
2865
|
+
]);
|
|
2866
|
+
};
|
|
2867
|
+
(p.signature?.inputs ?? []).forEach(noteSites('signature in'));
|
|
2868
|
+
(p.signature?.outputs ?? []).forEach(noteSites('signature out'));
|
|
2869
|
+
(p.registers ?? []).forEach(noteSites('registers'));
|
|
2870
|
+
// The first declared site answers the caller-side type of a name
|
|
2871
|
+
// (C74 owns the conflict when sites disagree).
|
|
2872
|
+
const declaredTypeOf = (name) => sites.get(name)?.[0]?.type;
|
|
2873
|
+
// C74 — one name, ONE type (process-io-type-coherence). Runs on the
|
|
2874
|
+
// abstract form too: the signature/registers coherence is a
|
|
2875
|
+
// declaration-level fact, independent of the `does` body.
|
|
2876
|
+
for (const [name, decls] of sites) {
|
|
2877
|
+
let conflict = null;
|
|
2878
|
+
for (let x = 0; x < decls.length && !conflict; x++) {
|
|
2879
|
+
for (let y = x + 1; y < decls.length && !conflict; y++) {
|
|
2880
|
+
if (!typesCompatible(resolveParamType(decls[x].type), resolveParamType(decls[y].type))) {
|
|
2881
|
+
conflict = {
|
|
2882
|
+
a: `${decls[x].pos} "${decls[x].type}"`,
|
|
2883
|
+
b: `${decls[y].pos} "${decls[y].type}"`,
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (conflict) {
|
|
2889
|
+
err('C74', `process ${p.id}: "${name}" declares incompatible types — ${conflict.a} vs ${conflict.b} — one name, one type at a transition boundary (process-io-type-coherence)`);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
const flow = p.does;
|
|
2893
|
+
if (!flow) {
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
const steps = flow.steps ?? [];
|
|
2897
|
+
const edges = flow.edges ?? [];
|
|
2898
|
+
const stepById = new Map(steps.map(s => [s.id, s]));
|
|
2899
|
+
// Effective step I/O: the declared reads/writes plus the wait target
|
|
2900
|
+
// (a read) and the caller-side names of a call's `with` bindings.
|
|
2901
|
+
const readsOf = (s) => new Set([
|
|
2902
|
+
...s.reads,
|
|
2903
|
+
...(s.wait ? [s.wait] : []),
|
|
2904
|
+
...s.callIn.map(b => b.bind),
|
|
2905
|
+
]);
|
|
2906
|
+
const writesOf = (s) => new Set([...s.writes, ...s.callOut.map(b => b.bind)]);
|
|
2907
|
+
// The C12 declared-name vocabulary (registers + signature + instance
|
|
2908
|
+
// keys + the process's own state).
|
|
2909
|
+
const declaredNames = new Set(['state', ...sites.keys()]);
|
|
2910
|
+
for (const v of Object.values(p.instances?.values ?? {})) {
|
|
2911
|
+
for (const k of Object.keys(v)) {
|
|
2912
|
+
declaredNames.add(k);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
// Names the process is PROVIDED with at entry: the IN parameters, the
|
|
2916
|
+
// per-classification instance values, and the operational state —
|
|
2917
|
+
// ambient HAS, available at every step without a writer in the flow.
|
|
2918
|
+
const provided = new Set(['state']);
|
|
2919
|
+
for (const prm of p.signature?.inputs ?? []) {
|
|
2920
|
+
provided.add(prm.name);
|
|
2921
|
+
}
|
|
2922
|
+
for (const v of Object.values(p.instances?.values ?? {})) {
|
|
2923
|
+
for (const k of Object.keys(v)) {
|
|
2924
|
+
provided.add(k);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
// Edge conditions read self.<name> AFTER the source step completes.
|
|
2928
|
+
const condReads = new Map();
|
|
2929
|
+
for (const e of edges) {
|
|
2930
|
+
const re = /\bself\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
2931
|
+
let m;
|
|
2932
|
+
while ((m = re.exec(e.condition)) !== null) {
|
|
2933
|
+
condReads.set(e.from, (condReads.get(e.from) ?? new Set()).add(m[1]));
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
// C75 — the step-chain dataflow covers every read: a must-analysis
|
|
2937
|
+
// (definitely-written on EVERY incoming path) over the flow graph,
|
|
2938
|
+
// computed as a shrink-to-fixpoint from the top (all written names).
|
|
2939
|
+
// Known limitation: the intersection runs over ALL predecessors
|
|
2940
|
+
// uniformly, so a parallel_gateway fork's conjunctive branches are
|
|
2941
|
+
// treated as alternative paths — a value written on one parallel
|
|
2942
|
+
// branch and read after the join (guaranteed bound under the concept
|
|
2943
|
+
// doc §4.2 "unordered conjunction" semantics) is reported uncovered.
|
|
2944
|
+
// The direction is safe (over-reports, never misses a real gap).
|
|
2945
|
+
// Follow-up: distinguish same-fork conjunctive predecessors (union)
|
|
2946
|
+
// from choice predecessors (intersection).
|
|
2947
|
+
const allWritten = new Set();
|
|
2948
|
+
for (const s of steps) {
|
|
2949
|
+
writesOf(s).forEach(n => allWritten.add(n));
|
|
2950
|
+
}
|
|
2951
|
+
const preds = new Map();
|
|
2952
|
+
for (const e of edges) {
|
|
2953
|
+
if (stepById.has(e.from) && stepById.has(e.to)) {
|
|
2954
|
+
preds.set(e.to, [...(preds.get(e.to) ?? []), e.from]);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
const availIn = new Map();
|
|
2958
|
+
for (const s of steps) {
|
|
2959
|
+
availIn.set(s.id, new Set(allWritten));
|
|
2960
|
+
}
|
|
2961
|
+
let changed = true;
|
|
2962
|
+
while (changed) {
|
|
2963
|
+
changed = false;
|
|
2964
|
+
for (const s of steps) {
|
|
2965
|
+
const ps = preds.get(s.id) ?? [];
|
|
2966
|
+
let next = new Set();
|
|
2967
|
+
if (ps.length > 0) {
|
|
2968
|
+
next = new Set(allWritten);
|
|
2969
|
+
for (const pid of ps) {
|
|
2970
|
+
const pOut = new Set([
|
|
2971
|
+
...(availIn.get(pid) ?? []),
|
|
2972
|
+
...writesOf(stepById.get(pid)),
|
|
2973
|
+
]);
|
|
2974
|
+
next = new Set([...next].filter(n => pOut.has(n)));
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
const cur = availIn.get(s.id);
|
|
2978
|
+
if (next.size !== cur.size || [...next].some(n => !cur.has(n))) {
|
|
2979
|
+
availIn.set(s.id, next);
|
|
2980
|
+
changed = true;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const availOut = (s) => new Set([...(availIn.get(s.id) ?? []), ...writesOf(s)]);
|
|
2985
|
+
// C75 (error leg) — a read with no writer on every incoming path and
|
|
2986
|
+
// no provided home reads a never-bound value. Undeclared names are
|
|
2987
|
+
// C12's finding, not repeated here.
|
|
2988
|
+
for (const s of steps) {
|
|
2989
|
+
for (const n of readsOf(s)) {
|
|
2990
|
+
if (!declaredNames.has(n) || provided.has(n)) {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
if (!(availIn.get(s.id) ?? new Set()).has(n)) {
|
|
2994
|
+
err('C75', `process ${p.id}: step "${s.id}" reads "${n}", which is neither an IN parameter/instance value nor written on every incoming flow path (process-flow-io-cover)`);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
for (const n of condReads.get(s.id) ?? new Set()) {
|
|
2998
|
+
if (!declaredNames.has(n) || provided.has(n)) {
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
if (!availOut(s).has(n)) {
|
|
3002
|
+
err('C75', `process ${p.id}: the edge condition at "${s.id}" reads "${n}", which is neither an IN parameter/instance value nor written on every incoming flow path (process-flow-io-cover)`);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
// C75 (warning leg) — a write with no reader is a dead output: no
|
|
3007
|
+
// step reads it, no edge condition references it, and no OUT
|
|
3008
|
+
// parameter names it. A capture-step write lands in evidence through
|
|
3009
|
+
// the capture form (the form is its reader), so it is never dead.
|
|
3010
|
+
const allRead = new Set();
|
|
3011
|
+
for (const s of steps) {
|
|
3012
|
+
readsOf(s).forEach(n => allRead.add(n));
|
|
3013
|
+
}
|
|
3014
|
+
for (const names of condReads.values()) {
|
|
3015
|
+
names.forEach(n => allRead.add(n));
|
|
3016
|
+
}
|
|
3017
|
+
const outNames = new Set((p.signature?.outputs ?? []).map(o => o.name));
|
|
3018
|
+
for (const s of steps) {
|
|
3019
|
+
if (s.capture) {
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
for (const n of writesOf(s)) {
|
|
3023
|
+
if (!declaredNames.has(n)) {
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
if (!allRead.has(n) && !outNames.has(n)) {
|
|
3027
|
+
warn('C75', `process ${p.id}: step "${s.id}" writes "${n}", which no step reads and no OUT parameter names — a dead output (process-flow-io-cover)`);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
// C76 — a `calls` step binds the callee's declared signature
|
|
3032
|
+
// completely and kind-compatibly (subprocess-signature-bound). The
|
|
3033
|
+
// callee lookup runs against the COMPOSED process set, so a call
|
|
3034
|
+
// across a `uses` boundary is checked the same way (post-merge).
|
|
3035
|
+
for (const s of steps) {
|
|
3036
|
+
if (!s.calls) {
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
const callee = processById.get(s.calls);
|
|
3040
|
+
if (!callee) {
|
|
3041
|
+
err('C76', `process ${p.id}: step "${s.id}" calls "${s.calls}", which is not a declared process (subprocess-signature-bound)`);
|
|
3042
|
+
continue;
|
|
3043
|
+
}
|
|
3044
|
+
const sig = callee.signature;
|
|
3045
|
+
if (!sig) {
|
|
3046
|
+
if (s.callIn.length > 0 || s.callOut.length > 0) {
|
|
3047
|
+
err('C76', `process ${p.id}: step "${s.id}" carries a with {…} binding, but "${s.calls}" declares no signature — there is nothing to bind (subprocess-signature-bound)`);
|
|
3048
|
+
}
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
const checkBindings = (direction, params, bindings) => {
|
|
3052
|
+
for (const prm of params) {
|
|
3053
|
+
const bs = bindings.filter(b => b.param === prm.name);
|
|
3054
|
+
if (bs.length === 0) {
|
|
3055
|
+
err('C76', `process ${p.id}: step "${s.id}" calls "${callee.id}": ${direction} parameter "${prm.name}" is not ${direction === 'IN' ? 'bound from a caller register' : 'mapped back to a caller register'} — the signature binds completely (subprocess-signature-bound)`);
|
|
3056
|
+
}
|
|
3057
|
+
else if (bs.length > 1) {
|
|
3058
|
+
err('C76', `process ${p.id}: step "${s.id}" calls "${callee.id}": ${direction} parameter "${prm.name}" is bound ${bs.length} times — exactly once (subprocess-signature-bound)`);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
for (const b of bindings) {
|
|
3062
|
+
const prm = params.find(q => q.name === b.param);
|
|
3063
|
+
if (!prm) {
|
|
3064
|
+
err('C76', `process ${p.id}: step "${s.id}" calls "${callee.id}": "${b.param}" is not a declared ${direction} parameter of the callee (subprocess-signature-bound)`);
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
const callerType = declaredTypeOf(b.bind);
|
|
3068
|
+
if (callerType === undefined) {
|
|
3069
|
+
continue; // undeclared caller-side name — C12's finding
|
|
3070
|
+
}
|
|
3071
|
+
// IN: the caller register feeds the callee's input; OUT: the
|
|
3072
|
+
// callee's output lands in the caller register. Kind equality
|
|
3073
|
+
// both ways (no kind hierarchy — see the section header).
|
|
3074
|
+
if (!typesCompatible(resolveParamType(callerType), resolveParamType(prm.type))) {
|
|
3075
|
+
err('C76', `process ${p.id}: step "${s.id}" calls "${callee.id}": caller "${b.bind}" (${callerType || 'untyped'}) and ${direction} parameter "${b.param}" (${prm.type || 'untyped'}) are not kind-compatible (subprocess-signature-bound)`);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
checkBindings('IN', sig.inputs, s.callIn);
|
|
3080
|
+
checkBindings('OUT', sig.outputs, s.callOut);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
// ── C37–C40: operational state machines (TODO.roadmap/07) ──
|
|
3084
|
+
// The subject's HAS state is a state_machine typed `kind operational`,
|
|
3085
|
+
// bound via has.state (C40), driven by step `fires` (C37), and gated on
|
|
3086
|
+
// by `self.state = #…` preconditions (C39). The operational and
|
|
3087
|
+
// lifecycle families are strictly disjoint (C38).
|
|
3088
|
+
const machineById = new Map((standard.stateMachines ?? []).map(sm => [sm.entityName, sm]));
|
|
3089
|
+
const operationalIds = new Set((standard.stateMachines ?? [])
|
|
3090
|
+
.filter(sm => sm.kind === 'operational')
|
|
3091
|
+
.map(sm => sm.entityName));
|
|
3092
|
+
const lifecycleIds = new Set((standard.stateMachines ?? [])
|
|
3093
|
+
.filter(sm => sm.kind !== 'operational')
|
|
3094
|
+
.map(sm => sm.entityName));
|
|
3095
|
+
// C38 — state-family-separation: the two families never reference each
|
|
3096
|
+
// other. A cascade targets an entity by name; naming the OTHER family's
|
|
3097
|
+
// machine is a cross-family reference, in either direction.
|
|
3098
|
+
for (const sm of standard.stateMachines ?? []) {
|
|
3099
|
+
const operational = sm.kind === 'operational';
|
|
3100
|
+
for (const t of sm.transitions ?? []) {
|
|
3101
|
+
for (const c of t.cascades ?? []) {
|
|
3102
|
+
if (!operational && operationalIds.has(c.targetEntity)) {
|
|
3103
|
+
err('C38', `state_machine ${sm.entityName} (lifecycle): transition ${t.from} -> ${t.to} cascades into operational machine "${c.targetEntity}" — the lifecycle family never targets the operational family (state-family-separation)`);
|
|
3104
|
+
}
|
|
3105
|
+
if (operational && lifecycleIds.has(c.targetEntity)) {
|
|
3106
|
+
err('C38', `state_machine ${sm.entityName} (operational): transition ${t.from} -> ${t.to} cascades into lifecycle machine "${c.targetEntity}" — the operational family never targets a workflow entity's state (state-family-separation)`);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
// ── C95: cascade-transition-resolve (smart gap-close E12,
|
|
3112
|
+
// analysis/cascade-machine-routing-design.md §4–§5) ──
|
|
3113
|
+
// A declared cascade that writes `status` on a machinated target must
|
|
3114
|
+
// ROUTE the write through a declared transition of the target's own
|
|
3115
|
+
// machine — the `via <transition-action>` facet — instead of
|
|
3116
|
+
// raw-writing past the machine (the INV-honesty leak E12 closes). The
|
|
3117
|
+
// machines and the entity classes are visible to the kernel in one
|
|
3118
|
+
// composed package (both live in oiml-smart-core — the design's §5
|
|
3119
|
+
// layer argument), so target resolution is intra-package. The eight
|
|
3120
|
+
// legs (§5, verbatim):
|
|
3121
|
+
// 1. via-present: a status-writing step (a mechanical `set`
|
|
3122
|
+
// containing `status`, or a semantic `submit`/`lock`) on a target
|
|
3123
|
+
// entity that has a declared machine declares `via` — self-steps
|
|
3124
|
+
// (target == the owning machine's entity) excepted;
|
|
3125
|
+
// 2. via-resolves: `via` names a transition of the TARGET machine;
|
|
3126
|
+
// 3. via-matches-status: that transition's `to` equals the written
|
|
3127
|
+
// status (`submit` ⇒ SUBMITTED, `lock` ⇒ LOCKED);
|
|
3128
|
+
// 4. via-unguarded: the named transition declares no `guard`;
|
|
3129
|
+
// 5. via-forbidden-elsewhere: no `via` on self / payload-only /
|
|
3130
|
+
// create / notify / record steps or on targets with no declared
|
|
3131
|
+
// machine;
|
|
3132
|
+
// 6. self-consistency: a self-step writing `status` writes the
|
|
3133
|
+
// owning transition's `to`;
|
|
3134
|
+
// 7. status-is-a-state: every written status is a declared state of
|
|
3135
|
+
// the target machine;
|
|
3136
|
+
// 8. fields-resolve: `where` field paths and `set`/`with` field
|
|
3137
|
+
// names resolve against the target entity's declared fields.
|
|
3138
|
+
// ROLLOUT (closed 2026-07-27): leg 1 WARNS → ERRS. During the rollout
|
|
3139
|
+
// window the shipped corpus carried the via-less cross-entity steps
|
|
3140
|
+
// the design enumerated (§3.2), so leg 1 warned (the C33 de-escalation
|
|
3141
|
+
// precedent). The smart declaration leg (gap E12) landed the 13 via
|
|
3142
|
+
// facets + the test_report_recalled edge and deleted the redundant
|
|
3143
|
+
// step, so leg 1 is now the catalogued error and the corpus pin
|
|
3144
|
+
// asserts zero C95 issues of any severity. Legs 2–8 were errors from
|
|
3145
|
+
// day one (leg 8 by the design's explicit decision — every field the
|
|
3146
|
+
// shipped steps write is declared, so a miss is a genuine typo).
|
|
3147
|
+
{
|
|
3148
|
+
// The semantic side-effect actions' documented status meaning (the
|
|
3149
|
+
// walker's vocabulary, state-walk.ts: submit ⇒ SUBMITTED, lock ⇒
|
|
3150
|
+
// LOCKED); notify/record are record-creates, never status writes.
|
|
3151
|
+
const SEMANTIC_STATUS = {
|
|
3152
|
+
submit: 'SUBMITTED',
|
|
3153
|
+
lock: 'LOCKED',
|
|
3154
|
+
};
|
|
3155
|
+
// Entity references (machine entityName, cascade targets) spell the
|
|
3156
|
+
// bare entity name; class ids may carry a namespace suffix
|
|
3157
|
+
// (`TestAssignment#data`). Index both spellings.
|
|
3158
|
+
const dataclassByEntity = new Map();
|
|
3159
|
+
for (const c of standard.dataclasses ?? []) {
|
|
3160
|
+
if (!dataclassByEntity.has(c.id)) {
|
|
3161
|
+
dataclassByEntity.set(c.id, c);
|
|
3162
|
+
}
|
|
3163
|
+
const bare = c.id.includes('#') ? c.id.slice(0, c.id.indexOf('#')) : c.id;
|
|
3164
|
+
if (!dataclassByEntity.has(bare)) {
|
|
3165
|
+
dataclassByEntity.set(bare, c);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
// The left-hand field path of every comparison clause of a where
|
|
3169
|
+
// conjunction (`a = … AND b != …`). Interpolations (${…}) sit on the
|
|
3170
|
+
// right-hand side by construction; a clause this extractor cannot
|
|
3171
|
+
// read stays silent here — the walker's fail-closed clause parsing
|
|
3172
|
+
// owns the runtime judgment.
|
|
3173
|
+
const whereFields = (where) => {
|
|
3174
|
+
const fields = [];
|
|
3175
|
+
for (const clause of where.split(/\s+[Aa][Nn][Dd]\s+/)) {
|
|
3176
|
+
const m = /^([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=|!=|<=|>=|<|>)/.exec(clause.trim());
|
|
3177
|
+
if (m) {
|
|
3178
|
+
fields.push(m[1]);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return fields;
|
|
3182
|
+
};
|
|
3183
|
+
for (const sm of standard.stateMachines ?? []) {
|
|
3184
|
+
for (const t of sm.transitions ?? []) {
|
|
3185
|
+
const transitionLabel = `${t.from} -> ${t.to}${t.actionName ? ` action ${t.actionName}` : ''}`;
|
|
3186
|
+
t.cascades.forEach((c, k) => {
|
|
3187
|
+
const stepLabel = `state_machine ${sm.entityName}: transition ${transitionLabel}: cascade ${k} (${c.targetEntity})`;
|
|
3188
|
+
const self = c.targetEntity === sm.entityName;
|
|
3189
|
+
const targetMachine = machineById.get(c.targetEntity);
|
|
3190
|
+
const targetClass = dataclassByEntity.get(c.targetEntity);
|
|
3191
|
+
const isCreate = c.create !== null;
|
|
3192
|
+
const semanticStatus = c.action !== null ? SEMANTIC_STATUS[c.action] : undefined;
|
|
3193
|
+
const setStatus = c.set.find(s => s.field === 'status');
|
|
3194
|
+
const writesStatus = !isCreate &&
|
|
3195
|
+
(semanticStatus !== undefined || setStatus !== undefined);
|
|
3196
|
+
const writtenStatus = semanticStatus ?? setStatus?.value ?? '';
|
|
3197
|
+
// Leg 1 — via-present (error severity — the rollout window
|
|
3198
|
+
// closed when the smart declaration leg landed: the corpus is
|
|
3199
|
+
// via-complete).
|
|
3200
|
+
if (writesStatus && targetMachine && !self && c.via === '') {
|
|
3201
|
+
err('C95', `${stepLabel} writes status '${writtenStatus}' on machinated entity '${c.targetEntity}' but declares no via — a status-writing cascade routes through a declared transition of the target's machine (cascade-transition-resolve)`);
|
|
3202
|
+
}
|
|
3203
|
+
// Leg 5 — via-forbidden-elsewhere; legs 2–4 — the routing
|
|
3204
|
+
// contract, judged only where a via is legal (one issue per
|
|
3205
|
+
// defect: a forbidden via reports under leg 5 only).
|
|
3206
|
+
if (c.via !== '') {
|
|
3207
|
+
if (self ||
|
|
3208
|
+
isCreate ||
|
|
3209
|
+
!writesStatus ||
|
|
3210
|
+
!targetMachine ||
|
|
3211
|
+
c.action === 'notify' ||
|
|
3212
|
+
c.action === 'record') {
|
|
3213
|
+
const forbidden = self
|
|
3214
|
+
? "a self-step (the target is the owning machine's own entity)"
|
|
3215
|
+
: isCreate
|
|
3216
|
+
? 'a create step (a fresh record has no current-state hop to route)'
|
|
3217
|
+
: c.action === 'notify' || c.action === 'record'
|
|
3218
|
+
? `an action ${c.action} step (a record-create, never a status hop)`
|
|
3219
|
+
: !writesStatus
|
|
3220
|
+
? 'a step that writes no status (payload-only)'
|
|
3221
|
+
: `a step on entity '${c.targetEntity}', which declares no state machine`;
|
|
3222
|
+
err('C95', `${stepLabel} declares via '${c.via}' on ${forbidden} — via routes a status write through the target's machine and is forbidden everywhere else (cascade-transition-resolve)`);
|
|
3223
|
+
}
|
|
3224
|
+
else {
|
|
3225
|
+
// Leg 2 — via-resolves. An action name is not unique
|
|
3226
|
+
// across a machine (the design's §4.1 evidence), so the
|
|
3227
|
+
// via names a CANDIDATE SET; legs 3–4 qualify it.
|
|
3228
|
+
const candidates = targetMachine.transitions.filter(tr => tr.actionName === c.via);
|
|
3229
|
+
if (candidates.length === 0) {
|
|
3230
|
+
err('C95', `${stepLabel} via '${c.via}' resolves to no transition of machine '${c.targetEntity}' (cascade-transition-resolve)`);
|
|
3231
|
+
}
|
|
3232
|
+
else {
|
|
3233
|
+
// Leg 3 — via-matches-status: the route must land on
|
|
3234
|
+
// the written status.
|
|
3235
|
+
const landing = candidates.filter(tr => tr.to === writtenStatus);
|
|
3236
|
+
if (landing.length === 0) {
|
|
3237
|
+
err('C95', `${stepLabel} via '${c.via}' names no transition of machine '${c.targetEntity}' whose to is '${writtenStatus}' — the route must land on the written status (cascade-transition-resolve)`);
|
|
3238
|
+
}
|
|
3239
|
+
else if (landing.some(tr => tr.guard !== '')) {
|
|
3240
|
+
// Leg 4 — via-unguarded: a cascade has no caller to
|
|
3241
|
+
// supply computed guard values.
|
|
3242
|
+
err('C95', `${stepLabel} via '${c.via}' names a guarded transition of machine '${c.targetEntity}' — a cascade has no caller to supply guard values; the route must be unguarded (cascade-transition-resolve)`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
// Leg 6 — self-consistency: the self-step's status write
|
|
3248
|
+
// restates the owning transition's `to` (the walker already
|
|
3249
|
+
// wrote it; the set block exists to carry the payload).
|
|
3250
|
+
if (self && writesStatus && writtenStatus !== t.to) {
|
|
3251
|
+
err('C95', `${stepLabel} writes status '${writtenStatus}' on its own entity, but the owning transition's to is '${t.to}' — a self-step's status write restates the owning transition (cascade-transition-resolve)`);
|
|
3252
|
+
}
|
|
3253
|
+
// Leg 7 — status-is-a-state (the typo net: "SUMBITTED" never
|
|
3254
|
+
// reaches runtime).
|
|
3255
|
+
if (writesStatus && targetMachine && writtenStatus !== '') {
|
|
3256
|
+
const stateNames = new Set(targetMachine.states.map(s => s.name));
|
|
3257
|
+
if (!stateNames.has(writtenStatus)) {
|
|
3258
|
+
err('C95', `${stepLabel} writes status '${writtenStatus}', which machine '${c.targetEntity}' does not declare as a state (cascade-transition-resolve)`);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
// Leg 8 — fields-resolve (the design's explicit error leg):
|
|
3262
|
+
// where paths and set/with field names against the target
|
|
3263
|
+
// entity's declared fields. A dotted path resolves its head
|
|
3264
|
+
// (the reference field); the deeper segments ride the
|
|
3265
|
+
// referenced entity and are the runtime's judgment.
|
|
3266
|
+
if (targetClass) {
|
|
3267
|
+
const fieldIds = new Set(targetClass.attributes.map(a => a.id));
|
|
3268
|
+
for (const path of whereFields(c.where)) {
|
|
3269
|
+
const head = path.includes('.')
|
|
3270
|
+
? path.slice(0, path.indexOf('.'))
|
|
3271
|
+
: path;
|
|
3272
|
+
if (!fieldIds.has(head)) {
|
|
3273
|
+
err('C95', `${stepLabel} where path '${path}' resolves to no declared field of entity '${c.targetEntity}' (cascade-transition-resolve)`);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
for (const s of c.set) {
|
|
3277
|
+
if (!fieldIds.has(s.field)) {
|
|
3278
|
+
err('C95', `${stepLabel} set field '${s.field}' resolves to no declared field of entity '${c.targetEntity}' (cascade-transition-resolve)`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
for (const key of Object.keys(c.with)) {
|
|
3282
|
+
if (!fieldIds.has(key)) {
|
|
3283
|
+
err('C95', `${stepLabel} with parameter '${key}' resolves to no declared field of entity '${c.targetEntity}' (cascade-transition-resolve)`);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
// C37 — state-fires-resolve: a step's `fires` names a transition action
|
|
3292
|
+
// of the process's bound machine (process `state <machineRef>`).
|
|
3293
|
+
// C39 — state-machine-states-referenced: a `self.state = #…` gate in a
|
|
3294
|
+
// precondition names a declared state of the bound machine. Both rules
|
|
3295
|
+
// need the binding, so a missing/dangling binding is reported once per
|
|
3296
|
+
// rule that actually uses it.
|
|
3297
|
+
for (const p of standard.processes ?? []) {
|
|
3298
|
+
const firedSteps = (p.does?.steps ?? []).filter(s => s.fires);
|
|
3299
|
+
const gates = [];
|
|
3300
|
+
for (const pc of p.preconditions ?? []) {
|
|
3301
|
+
for (const g of (0, operational_state_1.extractStateGates)(pc.check)) {
|
|
3302
|
+
gates.push({ preconditionId: pc.id, state: g.state });
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
const bound = p.state ? machineById.get(p.state) : undefined;
|
|
3306
|
+
if (firedSteps.length > 0) {
|
|
3307
|
+
if (!p.state) {
|
|
3308
|
+
for (const s of firedSteps) {
|
|
3309
|
+
err('C37', `process ${p.id}: step "${s.id}" fires "${s.fires}" but the process binds no state machine — declare state <machineRef> (state-fires-resolve)`);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
else if (!bound) {
|
|
3313
|
+
err('C37', `process ${p.id}: state machine "${p.state}" is not declared — step fires cannot resolve (state-fires-resolve)`);
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
const actions = new Set(bound.transitions.map(t => t.actionName).filter(a => a));
|
|
3317
|
+
for (const s of firedSteps) {
|
|
3318
|
+
if (!actions.has(s.fires)) {
|
|
3319
|
+
err('C37', `process ${p.id}: step "${s.id}" fires "${s.fires}", which state machine "${p.state}" does not declare as a transition action (state-fires-resolve)`);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (gates.length > 0) {
|
|
3325
|
+
if (!p.state) {
|
|
3326
|
+
err('C39', `process ${p.id}: precondition "${gates[0].preconditionId}" gates on self.state but the process binds no state machine — declare state <machineRef> (state-machine-states-referenced)`);
|
|
3327
|
+
}
|
|
3328
|
+
else if (!bound) {
|
|
3329
|
+
err('C39', `process ${p.id}: state machine "${p.state}" is not declared — state gates cannot resolve (state-machine-states-referenced)`);
|
|
3330
|
+
}
|
|
3331
|
+
else {
|
|
3332
|
+
const states = new Set(bound.states.map(s => s.name));
|
|
3333
|
+
for (const g of gates) {
|
|
3334
|
+
if (!states.has(g.state)) {
|
|
3335
|
+
err('C39', `process ${p.id}: precondition "${g.preconditionId}" gates on #${g.state}, which is not a declared state of machine "${p.state}" (state-machine-states-referenced)`);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
// C40 — anatomy-state-resolves: a subject's has.state names a declared
|
|
3342
|
+
// state machine. A DECLARED but lifecycle machine is a cross-family
|
|
3343
|
+
// reference and reports under C38 instead (family separation).
|
|
3344
|
+
for (const s of standard.subjects ?? []) {
|
|
3345
|
+
if (!s.has?.state) {
|
|
3346
|
+
continue;
|
|
3347
|
+
}
|
|
3348
|
+
const machine = machineById.get(s.has.state);
|
|
3349
|
+
if (!machine) {
|
|
3350
|
+
err('C40', `subject ${s.id}: has.state "${s.has.state}" is not a declared state machine — a subject's HAS state binds a declared operational machine (anatomy-state-resolves)`);
|
|
3351
|
+
}
|
|
3352
|
+
else if (machine.kind !== 'operational') {
|
|
3353
|
+
err('C38', `subject ${s.id}: has.state "${s.has.state}" is a lifecycle state machine — a subject's HAS state is its OPERATIONAL machine; workflow lifecycle state lives on entities (state-family-separation)`);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
// C41 — precondition-on-violation-known: on_violation parses as a free
|
|
3357
|
+
// string (ser-des/config/process.ts, conformanceTest.ts), and the only
|
|
3358
|
+
// known outcome is `invalid` — a violated run-validity precondition
|
|
3359
|
+
// VOIDS the run; it never fails the instrument (doctrine ch. 04 §4.5).
|
|
3360
|
+
// Any other value warns. A STATE GATE declared `on_violation fail` is
|
|
3361
|
+
// always a warning naming the doctrine: the warm-up pattern voids the
|
|
3362
|
+
// run, and recording `fail` would state a conformity verdict the
|
|
3363
|
+
// instrument never earned.
|
|
3364
|
+
const checkOnViolation = (ownerKind, ownerId, pc) => {
|
|
3365
|
+
if (pc.onViolation === 'invalid') {
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
if (pc.onViolation === 'fail' && (0, operational_state_1.extractStateGates)(pc.check).length > 0) {
|
|
3369
|
+
warn('C41', `${ownerKind} ${ownerId}: precondition "${pc.id}" declares on_violation fail — a violated run-validity precondition VOIDS the run (invalid); it never fails the instrument (doctrine ch. 04 §4.5) (precondition-on-violation-known)`);
|
|
3370
|
+
}
|
|
3371
|
+
else {
|
|
3372
|
+
warn('C41', `${ownerKind} ${ownerId}: precondition "${pc.id}" declares on_violation "${pc.onViolation}" — the only known outcome is "invalid" (precondition-on-violation-known)`);
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
for (const p of standard.processes ?? []) {
|
|
3376
|
+
for (const pc of p.preconditions ?? []) {
|
|
3377
|
+
checkOnViolation('process', p.id, pc);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
3381
|
+
for (const pc of t.preconditions ?? []) {
|
|
3382
|
+
checkOnViolation('conformance_test', t.id, pc);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
// ── C51–C54: the coverage audits (TODO.roadmap/17, concept doc ──
|
|
3386
|
+
// §11.5). The aspect↔requirement↔test↔form↔verdict closure: C5 holds
|
|
3387
|
+
// the requirement→test link; C51/C52 hold the test→form→verdict links
|
|
3388
|
+
// at audit strictness; C53/C54 anchor the requirement's inputs
|
|
3389
|
+
// (uses bound, lookup tables) at the normal level.
|
|
3390
|
+
// C53 — coverage-uses-bound: every requirement limit.uses input is
|
|
3391
|
+
// bound. Unprefixed entries resolve against the package's quantitative
|
|
3392
|
+
// vocabulary (attributes, symbols, requirements, classification
|
|
3393
|
+
// dimensions, behaviors — bare ids or full paths, compared on the
|
|
3394
|
+
// leaf); `observable:`/`formula:` prefixes resolve against the symbol
|
|
3395
|
+
// and calculation registries. `load` is the declared per-load-point
|
|
3396
|
+
// free variable of the MPE lookups (deep-audit-r60 F6 — indeterminate
|
|
3397
|
+
// at verdict time by design). table:/profile: prefixes are C54's.
|
|
3398
|
+
// (Moves C2's former warning leg here and hardens it to an error: a
|
|
3399
|
+
// use input that binds nothing computes nothing.)
|
|
3400
|
+
const calcNames = new Set();
|
|
3401
|
+
for (const c of standard.calculations ?? []) {
|
|
3402
|
+
if (c.id) {
|
|
3403
|
+
calcNames.add(c.id);
|
|
3404
|
+
}
|
|
3405
|
+
if (c.name) {
|
|
3406
|
+
calcNames.add(c.name);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
for (const r of standard.requirements ?? []) {
|
|
3410
|
+
for (const u of r.limit?.uses ?? []) {
|
|
3411
|
+
if (/^(table|profile):/.test(u)) {
|
|
3412
|
+
continue; // C54's lookup-table leg
|
|
3413
|
+
}
|
|
3414
|
+
if (u.startsWith('observable:')) {
|
|
3415
|
+
const id = u.slice('observable:'.length);
|
|
3416
|
+
if (!symbolIds.has(id)) {
|
|
3417
|
+
err('C53', `requirement ${r.id}: limit.uses "${u}" — observable "${id}" is not a declared symbol (coverage-uses-bound)`);
|
|
3418
|
+
}
|
|
3419
|
+
continue;
|
|
3420
|
+
}
|
|
3421
|
+
if (u.startsWith('formula:')) {
|
|
3422
|
+
const id = u.slice('formula:'.length);
|
|
3423
|
+
if (!calcNames.has(id)) {
|
|
3424
|
+
err('C53', `requirement ${r.id}: limit.uses "${u}" — formula "${id}" is not a declared calculation (coverage-uses-bound)`);
|
|
3425
|
+
}
|
|
3426
|
+
continue;
|
|
3427
|
+
}
|
|
3428
|
+
// `uses` may carry bare ids or full paths — compare the last segment.
|
|
3429
|
+
const leaf = u.split('.').pop() ?? u;
|
|
3430
|
+
if (!attrIds.has(leaf) &&
|
|
3431
|
+
!attrIds.has(u) &&
|
|
3432
|
+
!symbolIds.has(leaf) &&
|
|
3433
|
+
!symbolIds.has(u) &&
|
|
3434
|
+
!reqIds.has(u) &&
|
|
3435
|
+
!dimIds.has(leaf) &&
|
|
3436
|
+
!behaviorIds.has(leaf) &&
|
|
3437
|
+
u !== 'load') {
|
|
3438
|
+
err('C53', `requirement ${r.id}: limit.uses "${u}" binds nothing — it is not a declared attribute, symbol, requirement, dimension, or behavior (coverage-uses-bound)`);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
// C54 — coverage-lookup-table-exists: lookup tables exist. The
|
|
3443
|
+
// table:/profile: uses prefixes and the calculations' lookup profiles
|
|
3444
|
+
// (lookupMPE/lookupProfile targets) resolve to declared tables and
|
|
3445
|
+
// their profile definitions — a dangling lookup breaks evaluation at
|
|
3446
|
+
// runtime. A profile reference resolves as a declared table id, as
|
|
3447
|
+
// `<table>.<name>` with <name> in the table's profileDefs, or as a
|
|
3448
|
+
// bare <name> in the `profiles` table's profileDefs.
|
|
3449
|
+
const tablesById = new Map((standard.tables ?? []).map(t => [t.id, t]));
|
|
3450
|
+
const profileRefResolves = (ref) => {
|
|
3451
|
+
if (tablesById.has(ref)) {
|
|
3452
|
+
return true;
|
|
3453
|
+
}
|
|
3454
|
+
const dot = ref.indexOf('.');
|
|
3455
|
+
if (dot > 0) {
|
|
3456
|
+
const tbl = tablesById.get(ref.slice(0, dot));
|
|
3457
|
+
const name = ref.slice(dot + 1);
|
|
3458
|
+
if (tbl && (tbl.profileDefs ?? []).some(p => p.name === name)) {
|
|
3459
|
+
return true;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
const profiles = tablesById.get('profiles');
|
|
3463
|
+
return (profiles !== undefined &&
|
|
3464
|
+
(profiles.profileDefs ?? []).some(p => p.name === ref));
|
|
3465
|
+
};
|
|
3466
|
+
for (const r of standard.requirements ?? []) {
|
|
3467
|
+
for (const u of r.limit?.uses ?? []) {
|
|
3468
|
+
if (u.startsWith('table:')) {
|
|
3469
|
+
const id = u.slice('table:'.length);
|
|
3470
|
+
if (!tablesById.has(id)) {
|
|
3471
|
+
err('C54', `requirement ${r.id}: limit.uses "${u}" — table "${id}" is not declared (coverage-lookup-table-exists)`);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
else if (u.startsWith('profile:')) {
|
|
3475
|
+
const ref = u.slice('profile:'.length);
|
|
3476
|
+
if (!profileRefResolves(ref)) {
|
|
3477
|
+
err('C54', `requirement ${r.id}: limit.uses "${u}" — profile "${ref}" is not a declared table or profile definition (coverage-lookup-table-exists)`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
for (const c of standard.calculations ?? []) {
|
|
3483
|
+
if (c.profile && !profileRefResolves(c.profile)) {
|
|
3484
|
+
err('C54', `calculation ${c.name || c.id}: lookup profile "${c.profile}" is not a declared table or profile definition (coverage-lookup-table-exists)`);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
// C51 — coverage-test-evidence (AUDIT): a conformance test whose runs
|
|
3488
|
+
// leave no declared evidence — no form declares it via
|
|
3489
|
+
// conformance_process, it names no result_forms / report_rows, and no
|
|
3490
|
+
// form covers a requirement it targets. inherits_from chains inherit
|
|
3491
|
+
// the parent's evidence links (R 60's class-driven instances).
|
|
3492
|
+
if (strictness === 'audit') {
|
|
3493
|
+
const testById = new Map((standard.conformanceTests ?? []).map(t => [t.id, t]));
|
|
3494
|
+
const formDeclaredTests = new Set();
|
|
3495
|
+
for (const f of standard.forms ?? []) {
|
|
3496
|
+
for (const pid of f.conformanceProcessIds ??
|
|
3497
|
+
(f.conformanceProcessId ? [f.conformanceProcessId] : [])) {
|
|
3498
|
+
formDeclaredTests.add(pid);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
const formRequirementSets = (standard.forms ?? []).map(f => new Set(f.requirements ?? []));
|
|
3502
|
+
const hasEvidence = (t, seen) => {
|
|
3503
|
+
if (seen.has(t.id)) {
|
|
3504
|
+
return false;
|
|
3505
|
+
}
|
|
3506
|
+
seen.add(t.id);
|
|
3507
|
+
if (formDeclaredTests.has(t.id)) {
|
|
3508
|
+
return true;
|
|
3509
|
+
}
|
|
3510
|
+
if ((t.resultForms ?? []).length > 0 || (t.reportRows ?? []).length > 0) {
|
|
3511
|
+
return true;
|
|
3512
|
+
}
|
|
3513
|
+
if (formRequirementSets.some(reqs => (t.targets ?? []).some(target => reqs.has(target)))) {
|
|
3514
|
+
return true;
|
|
3515
|
+
}
|
|
3516
|
+
const parent = t.inheritsFrom ? testById.get(t.inheritsFrom) : undefined;
|
|
3517
|
+
return parent !== undefined && hasEvidence(parent, seen);
|
|
3518
|
+
};
|
|
3519
|
+
for (const t of standard.conformanceTests ?? []) {
|
|
3520
|
+
if (!hasEvidence(t, new Set())) {
|
|
3521
|
+
warn('C51', `conformance test ${t.id}: no form records its evidence — the run happens and nothing permanent records it (coverage-test-evidence)`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
// C52 — coverage-form-judgment (AUDIT): a test-evidence form (one
|
|
3525
|
+
// binding a conformance process) carrying no judgment — no
|
|
3526
|
+
// pass_fail, no field evaluation/verdict, no requirements binding.
|
|
3527
|
+
// Documentary forms (report sections, headers) are not the closure's
|
|
3528
|
+
// "form" slot and stay silent.
|
|
3529
|
+
const formHasJudgment = (f) => {
|
|
3530
|
+
if (f.passFail) {
|
|
3531
|
+
return true;
|
|
3532
|
+
}
|
|
3533
|
+
const walk = (fields) => fields.some(x => x.evaluation || x.verdict || walk(x.fields ?? []));
|
|
3534
|
+
return walk(f.fields ?? []);
|
|
3535
|
+
};
|
|
3536
|
+
for (const f of standard.forms ?? []) {
|
|
3537
|
+
const bindsTest = !!f.conformanceProcessId || (f.conformanceProcessIds ?? []).length > 0;
|
|
3538
|
+
if (!bindsTest) {
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
if (!formHasJudgment(f) && (f.requirements ?? []).length === 0) {
|
|
3542
|
+
warn('C52', `form ${f.id}: binds a conformance process but computes no judgment — evidence gathered and never weighed (coverage-form-judgment)`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
// ── C71–C73: the normative-text coverage metric (TODO.roadmap/26, ──
|
|
3547
|
+
// concept doc §11.6) — layer 5 of the validation stack. The sentence
|
|
3548
|
+
// decomposition ships inside the package (sources-prd/*.sentences.json
|
|
3549
|
+
// + coverage.json); without them the metric is silent. C73 (config
|
|
3550
|
+
// hygiene — malformed payloads, stale allowances/adjudications) runs
|
|
3551
|
+
// at every level; C71 (uncovered normative sentences) is audit-level
|
|
3552
|
+
// and budgeted by the package's text_coverage_budget (C72, applied
|
|
3553
|
+
// with the allowlist below). Duplicate pairs are REPORTED by
|
|
3554
|
+
// `primmel check --coverage`, never auto-failed here.
|
|
3555
|
+
const textCoverageData = (0, text_coverage_1.loadTextCoverageData)(dir);
|
|
3556
|
+
issues.push(...textCoverageData.issues);
|
|
3557
|
+
if (textCoverageData.manifests.length > 0) {
|
|
3558
|
+
const textCoverage = (0, text_coverage_1.computeTextCoverage)(standard, textCoverageData.manifests, textCoverageData.config);
|
|
3559
|
+
issues.push(...textCoverage.configIssues);
|
|
3560
|
+
if (strictness === 'audit') {
|
|
3561
|
+
for (const d of textCoverage.documents) {
|
|
3562
|
+
for (const s of d.uncoveredCounted) {
|
|
3563
|
+
warn('C71', (0, text_coverage_1.uncoveredSentenceMessage)(d.urn, s));
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
// ── Finalize: the package allowlist + level severities ──
|
|
3569
|
+
// The allowlist (KNOWN/STALE/budget) applies to every level; --strict
|
|
3570
|
+
// then promotes the surviving warnings — except KNOWN issues and
|
|
3571
|
+
// budget-covered C51/C52 warnings (the budget is their allowance).
|
|
3572
|
+
const level = strictness === 'audit' ? 'audit' : 'normal';
|
|
3573
|
+
const { allowlist, issues: allowlistParseIssues } = (0, check_allowlist_1.loadAllowlist)(dir);
|
|
3574
|
+
issues.push(...allowlistParseIssues);
|
|
3575
|
+
const finalized = (0, check_allowlist_1.applyAllowlist)(issues, allowlist, (0, check_rules_1.activeRuleIds)(level)).issues;
|
|
3576
|
+
if (options.strict) {
|
|
3577
|
+
for (const i of finalized) {
|
|
3578
|
+
if (i.severity !== 'warning' || i.known) {
|
|
3579
|
+
continue;
|
|
3580
|
+
}
|
|
3581
|
+
if (check_allowlist_1.BUDGETED_RULES.has(i.check) && allowlist.coverageBudget !== null) {
|
|
3582
|
+
continue; // the budget governs these — C55 reports the excess
|
|
3583
|
+
}
|
|
3584
|
+
if (check_allowlist_1.TEXT_BUDGETED_RULES.has(i.check) &&
|
|
3585
|
+
allowlist.textCoverageBudget !== null) {
|
|
3586
|
+
continue; // the text budget governs these — C72 reports the excess
|
|
3587
|
+
}
|
|
3588
|
+
i.severity = 'error';
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
return finalized;
|
|
3592
|
+
}
|
|
3593
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
3594
|
+
// Manifest-only resolution stopgap (TODO.roadmap/05).
|
|
3595
|
+
//
|
|
3596
|
+
// The shipped package.primmel declarations (uses/requires/provides) are
|
|
3597
|
+
// enforced metadata ONLY at this level until task 13 moves content from
|
|
3598
|
+
// textual includes to real composition: composing the real packages
|
|
3599
|
+
// fails by design today (overlays redeclare layer content), so the
|
|
3600
|
+
// composition rules C27–C31 cannot run on them. This lint parses the
|
|
3601
|
+
// manifest of the package at `dir` plus the manifests of its SIBLING
|
|
3602
|
+
// package directories (a package id resolves when a sibling manifest
|
|
3603
|
+
// declares the same id) and verifies — WITHOUT composing content:
|
|
3604
|
+
// - every uses/extends entry resolves (reported under C27);
|
|
3605
|
+
// - the uses graph is acyclic (C29);
|
|
3606
|
+
// - every requires entry names a package in the uses closure or one of
|
|
3607
|
+
// its provides entries (C31).
|
|
3608
|
+
// All messages carry a "(manifest-only …)" marker so CI output cannot be
|
|
3609
|
+
// mistaken for a full composition verdict.
|
|
3610
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
3611
|
+
function checkManifestResolution(dir) {
|
|
3612
|
+
const issues = [];
|
|
3613
|
+
const err = (check, message) => issues.push({ check, severity: 'error', message });
|
|
3614
|
+
const MARKER = '(manifest-only sibling resolution — no content composed)';
|
|
3615
|
+
let root;
|
|
3616
|
+
try {
|
|
3617
|
+
root = (0, package_1.readPackageManifest)(dir);
|
|
3618
|
+
}
|
|
3619
|
+
catch (e) {
|
|
3620
|
+
err('C27', `${e.message} ${MARKER}`);
|
|
3621
|
+
return issues;
|
|
3622
|
+
}
|
|
3623
|
+
// The locator: sibling directories of `dir` carrying a parseable
|
|
3624
|
+
// package.primmel, keyed by their declared id (last wins — the sorted
|
|
3625
|
+
// scan keeps the choice deterministic).
|
|
3626
|
+
const siblings = new Map();
|
|
3627
|
+
const parent = (0, path_1.dirname)((0, path_1.resolve)(dir));
|
|
3628
|
+
for (const entry of (0, fs_1.readdirSync)(parent).sort()) {
|
|
3629
|
+
const full = (0, path_1.join)(parent, entry);
|
|
3630
|
+
try {
|
|
3631
|
+
if (!(0, fs_1.statSync)(full).isDirectory()) {
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
const m = (0, package_1.readPackageManifest)(full);
|
|
3635
|
+
siblings.set(m.id, m);
|
|
3636
|
+
}
|
|
3637
|
+
catch {
|
|
3638
|
+
// Not a package dir (no/invalid manifest) — not a resolution candidate.
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
// Walk the uses closure over manifests only (cycle-safe), collecting
|
|
3642
|
+
// the packages the root transitively imports.
|
|
3643
|
+
const closure = new Map();
|
|
3644
|
+
const state = new Map(); // 1 = in stack, 2 = done
|
|
3645
|
+
const stack = [];
|
|
3646
|
+
const visit = (m) => {
|
|
3647
|
+
state.set(m.id, 1);
|
|
3648
|
+
stack.push(m.id);
|
|
3649
|
+
for (const dep of (0, package_1.effectiveUses)(m)) {
|
|
3650
|
+
const c = state.get(dep) ?? 0;
|
|
3651
|
+
if (c === 1) {
|
|
3652
|
+
err('C29', `uses cycle: ${[...stack.slice(stack.indexOf(dep)), dep].join(' → ')} — package composition must be acyclic (uses-cycle) ${MARKER}`);
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
if (c === 2) {
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
const dm = siblings.get(dep);
|
|
3659
|
+
if (!dm) {
|
|
3660
|
+
err('C27', `package "${m.id}" uses "${dep}", which no sibling package directory declares (uses-resolves) ${MARKER}`);
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
closure.set(dep, dm);
|
|
3664
|
+
visit(dm);
|
|
3665
|
+
}
|
|
3666
|
+
stack.pop();
|
|
3667
|
+
state.set(m.id, 2);
|
|
3668
|
+
};
|
|
3669
|
+
visit(root);
|
|
3670
|
+
// Every requires entry (of the root and of every package in its
|
|
3671
|
+
// closure) names a composed package id or one of its provides entries
|
|
3672
|
+
// — satisfied by another package, never by the requiring one itself.
|
|
3673
|
+
for (const m of [root, ...closure.values()]) {
|
|
3674
|
+
for (const req of m.requires ?? []) {
|
|
3675
|
+
const satisfied = [...closure.values()].some(other => other.id !== m.id &&
|
|
3676
|
+
(other.id === req || (other.provides ?? []).includes(req)));
|
|
3677
|
+
if (!satisfied) {
|
|
3678
|
+
err('C31', `package "${m.id}" requires "${req}", which no used package provides (requires-satisfied) ${MARKER}`);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
return issues;
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* A list item's address key in a nested text path (C89): the declared
|
|
3686
|
+
* `name` (a form/subform field, a requirement parameter) or the declared
|
|
3687
|
+
* numeric position (a test-sequence step's `order`, a requirement
|
|
3688
|
+
* subject's `slot`) as its bare token. Keys are declared content, not
|
|
3689
|
+
* positional indexes — a reorder never silently re-points an address; a
|
|
3690
|
+
* rename breaks it loudly, which is the point (the smart-side hygiene
|
|
3691
|
+
* finding that motivated path addressing).
|
|
3692
|
+
*/
|
|
3693
|
+
function textAddressKey(item, segment) {
|
|
3694
|
+
if (typeof item !== 'object' || item === null) {
|
|
3695
|
+
return false;
|
|
3696
|
+
}
|
|
3697
|
+
const rec = item;
|
|
3698
|
+
if (rec.name === segment) {
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
if (typeof rec.order === 'number' && String(rec.order) === segment) {
|
|
3702
|
+
return true;
|
|
3703
|
+
}
|
|
3704
|
+
if (typeof rec.slot === 'number' && String(rec.slot) === segment) {
|
|
3705
|
+
return true;
|
|
3706
|
+
}
|
|
3707
|
+
return false;
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* C89 — spelling-code-wellformed (TODO.roadmap/25; doctrine ch. 10 §10.7).
|
|
3711
|
+
*
|
|
3712
|
+
* Every human-readable string is spelling-coded per ISO 24229; BCP 47 is
|
|
3713
|
+
* not used. The kernel checks the SYNTAX layer (src/spelling.ts) —
|
|
3714
|
+
* register resolution (does this language/script/country/conversion
|
|
3715
|
+
* system exist) is the consumer's vendored-snapshot discipline:
|
|
3716
|
+
*
|
|
3717
|
+
* - the manifest's default_spelling and declared spellings parse as
|
|
3718
|
+
* spelling codes (language-script[-country][-extension]; the script
|
|
3719
|
+
* is MANDATORY — a bare language code is an error), and the default
|
|
3720
|
+
* belongs to the declared set;
|
|
3721
|
+
* - every `text` block addresses an existing element's prose field.
|
|
3722
|
+
* The address is `<element-id>.<field>` for a top-level field, or
|
|
3723
|
+
* `<element-id>.<path…>.<field>` for prose NESTED inside the element
|
|
3724
|
+
* (smart gap-close E13): the element is the longest dot-boundary
|
|
3725
|
+
* prefix registered in the package (element ids may carry dots), an
|
|
3726
|
+
* intermediate segment names a nested structure, a list item is
|
|
3727
|
+
* keyed by its declared key (name/order/slot — never a positional
|
|
3728
|
+
* index), and the terminal segment is a prose field. A dangling,
|
|
3729
|
+
* scalar, or ambiguous segment is an error. The terminal segment's
|
|
3730
|
+
* snake_case → camelCase fallback is DELIBERATE and terminal-only
|
|
3731
|
+
* (TODO.v2/13 item 3b, pinned — the walk is NOT extended): the
|
|
3732
|
+
* terminal draws from the FIXED prose-field vocabulary authored in
|
|
3733
|
+
* snake_case (the PROSE_FIELDS set — `true_label`, `scope_note`, …)
|
|
3734
|
+
* while the parsed structure holds the property camelCase
|
|
3735
|
+
* (`trueLabel`, …), so the vocabulary lookup translates exactly
|
|
3736
|
+
* there. Intermediate segments are not vocabulary lookups — they
|
|
3737
|
+
* name nested structures by the parsed model's own property names
|
|
3738
|
+
* or key list items by author-declared content (names/orders/slots)
|
|
3739
|
+
* — so a case translation there would bless a second spelling of
|
|
3740
|
+
* machine structure (and an ambiguity where `foo_bar` and `fooBar`
|
|
3741
|
+
* collide); no shipped nested structure name needs it (the corpus
|
|
3742
|
+
* legs);
|
|
3743
|
+
* - every spell entry's code parses; a duplicate code within one
|
|
3744
|
+
* content set is an error (use an extension to distinguish);
|
|
3745
|
+
* - an entry repeating the package's default_spelling is an error —
|
|
3746
|
+
* the default's value belongs inline on the element (`name "…"`);
|
|
3747
|
+
* - every `via` conversion system code parses
|
|
3748
|
+
* (titular:source:target:identifying); a `zz-` user-assigned code
|
|
3749
|
+
* validates with a warning (user-assigned codes are not portable).
|
|
3750
|
+
*/
|
|
3751
|
+
function checkSpellingCodes(standard) {
|
|
3752
|
+
const issues = [];
|
|
3753
|
+
const err = (check, message) => issues.push({ check, severity: 'error', message });
|
|
3754
|
+
const warn = (check, message) => issues.push({ check, severity: 'warning', message });
|
|
3755
|
+
const m = standard.packageManifest;
|
|
3756
|
+
if (m?.defaultSpelling) {
|
|
3757
|
+
const parsed = (0, spelling_1.parseSpellingCode)(m.defaultSpelling);
|
|
3758
|
+
if (typeof parsed === 'string') {
|
|
3759
|
+
err('C89', `package "${m.id}": default_spelling "${m.defaultSpelling}" — ${parsed} (spelling-code-wellformed)`);
|
|
3760
|
+
}
|
|
3761
|
+
if ((m.spellings ?? []).length > 0 &&
|
|
3762
|
+
!m.spellings.includes(m.defaultSpelling)) {
|
|
3763
|
+
err('C89', `package "${m.id}": default_spelling "${m.defaultSpelling}" is not in the declared spellings set (spelling-code-wellformed)`);
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
for (const s of m?.spellings ?? []) {
|
|
3767
|
+
const parsed = (0, spelling_1.parseSpellingCode)(s);
|
|
3768
|
+
if (typeof parsed === 'string') {
|
|
3769
|
+
err('C89', `package "${m.id}": declared spelling "${s}" — ${parsed} (spelling-code-wellformed)`);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
const texts = standard.texts ?? [];
|
|
3773
|
+
if (texts.length === 0) {
|
|
3774
|
+
return issues;
|
|
3775
|
+
}
|
|
3776
|
+
// The element registry a text block may address — every id-keyed
|
|
3777
|
+
// collection (prose lives on requirements, terms, forms, subjects…).
|
|
3778
|
+
// The walk needs the element OBJECTS, not just the ids: a nested
|
|
3779
|
+
// address (<element-id>.<path…>.<field>) resolves segment by segment
|
|
3780
|
+
// into the addressed element's structure.
|
|
3781
|
+
const elements = new Map();
|
|
3782
|
+
const ID_COLLECTIONS = [
|
|
3783
|
+
'requirements',
|
|
3784
|
+
'requirementClasses',
|
|
3785
|
+
'conformanceTests',
|
|
3786
|
+
'conformanceClasses',
|
|
3787
|
+
'terms',
|
|
3788
|
+
'forms',
|
|
3789
|
+
'subforms',
|
|
3790
|
+
'symbols',
|
|
3791
|
+
'tables',
|
|
3792
|
+
'calculations',
|
|
3793
|
+
'notes',
|
|
3794
|
+
'provisions',
|
|
3795
|
+
'processes',
|
|
3796
|
+
'instruments',
|
|
3797
|
+
'attributeDefinitions',
|
|
3798
|
+
'capabilities',
|
|
3799
|
+
'behaviors',
|
|
3800
|
+
'conditionSets',
|
|
3801
|
+
'subjects',
|
|
3802
|
+
'referenceMaterials',
|
|
3803
|
+
'artifactDefinitions',
|
|
3804
|
+
'monitors',
|
|
3805
|
+
'passports',
|
|
3806
|
+
'invariants',
|
|
3807
|
+
'testSequences',
|
|
3808
|
+
'formulasUsed',
|
|
3809
|
+
'stateMachines',
|
|
3810
|
+
'figures',
|
|
3811
|
+
'links',
|
|
3812
|
+
];
|
|
3813
|
+
for (const field of ID_COLLECTIONS) {
|
|
3814
|
+
for (const item of standard[field] ?? []) {
|
|
3815
|
+
if (item?.id) {
|
|
3816
|
+
elements.set(item.id, item);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
// The prose-field vocabulary a text block may address (machine content
|
|
3821
|
+
// — identifiers, bind paths, OCL, snake_case keys — is never
|
|
3822
|
+
// spelling-coded, doctrine §10.7).
|
|
3823
|
+
const PROSE_FIELDS = new Set([
|
|
3824
|
+
'name',
|
|
3825
|
+
'title',
|
|
3826
|
+
'label',
|
|
3827
|
+
'term',
|
|
3828
|
+
'definition',
|
|
3829
|
+
'statement',
|
|
3830
|
+
'description',
|
|
3831
|
+
'guidance',
|
|
3832
|
+
'note',
|
|
3833
|
+
'notes',
|
|
3834
|
+
'scope_note',
|
|
3835
|
+
'subject',
|
|
3836
|
+
'message',
|
|
3837
|
+
'designation',
|
|
3838
|
+
'placeholder',
|
|
3839
|
+
'true_label',
|
|
3840
|
+
'false_label',
|
|
3841
|
+
'examples',
|
|
3842
|
+
]);
|
|
3843
|
+
for (const t of texts) {
|
|
3844
|
+
const dot = t.id.lastIndexOf('.');
|
|
3845
|
+
if (dot <= 0 || dot === t.id.length - 1) {
|
|
3846
|
+
err('C89', `text "${t.id}": the address is <element-id>.<field> (a nested prose field addresses <element-id>.<path…>.<field>) (spelling-code-wellformed)`);
|
|
3847
|
+
}
|
|
3848
|
+
else {
|
|
3849
|
+
const segments = t.id.split('.');
|
|
3850
|
+
if (segments.some(s => s === '')) {
|
|
3851
|
+
err('C89', `text "${t.id}": empty path segment — the address is <element-id>.<path…>.<field> (spelling-code-wellformed)`);
|
|
3852
|
+
}
|
|
3853
|
+
else {
|
|
3854
|
+
// The addressed element is the LONGEST dot-boundary prefix of the
|
|
3855
|
+
// address registered in the package (element ids may themselves
|
|
3856
|
+
// carry dots — r144-3/sec-3.4); the rest is the path into the
|
|
3857
|
+
// element's structure. On a one-segment path this degenerates to
|
|
3858
|
+
// the <element-id>.<field> split.
|
|
3859
|
+
let elementId = '';
|
|
3860
|
+
let path = [];
|
|
3861
|
+
const tried = [];
|
|
3862
|
+
for (let i = segments.length - 1; i >= 1; i--) {
|
|
3863
|
+
const candidate = segments.slice(0, i).join('.');
|
|
3864
|
+
tried.push(candidate);
|
|
3865
|
+
if (elements.has(candidate)) {
|
|
3866
|
+
elementId = candidate;
|
|
3867
|
+
path = segments.slice(i);
|
|
3868
|
+
break;
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (elementId === '') {
|
|
3872
|
+
// Every dot-boundary prefix was tried (longest first — element
|
|
3873
|
+
// ids may carry dots); the one-level "last-dot split" mental
|
|
3874
|
+
// model would name only the longest, so the message lists the
|
|
3875
|
+
// tried set (TODO.v2/13 item 3a).
|
|
3876
|
+
err('C89', `text "${t.id}": no element "${tried[0]}" in the package — every dot-boundary prefix was tried (${tried.map(c => `"${c}"`).join(', ')}); the addressed element is the LONGEST registered prefix (spelling-code-wellformed)`);
|
|
3877
|
+
}
|
|
3878
|
+
else {
|
|
3879
|
+
// Walk the intermediate segments: an object segment names a
|
|
3880
|
+
// nested structure (an own property whose value is an object or
|
|
3881
|
+
// list), a list segment keys ONE item by its declared key. The
|
|
3882
|
+
// terminal segment must be a prose field the addressed
|
|
3883
|
+
// structure CARRIES (own property — a prose slot the parser
|
|
3884
|
+
// fills only when authored is addressable exactly then; there
|
|
3885
|
+
// is nothing to alternate where no default value lives).
|
|
3886
|
+
const field = path[path.length - 1];
|
|
3887
|
+
let node = elements.get(elementId);
|
|
3888
|
+
let soFar = elementId;
|
|
3889
|
+
let resolved = true;
|
|
3890
|
+
for (let s = 0; s < path.length - 1 && resolved; s++) {
|
|
3891
|
+
const seg = path[s];
|
|
3892
|
+
if (Array.isArray(node)) {
|
|
3893
|
+
const matches = node.filter(item => textAddressKey(item, seg));
|
|
3894
|
+
if (matches.length !== 1) {
|
|
3895
|
+
err('C89', matches.length === 0
|
|
3896
|
+
? `text "${t.id}": no item keyed "${seg}" under "${soFar}" — a list item addresses by its declared key (a field or parameter name, a step's order, a subject's slot) (spelling-code-wellformed)`
|
|
3897
|
+
: `text "${t.id}": "${seg}" keys ${matches.length} items under "${soFar}" — the address is ambiguous (spelling-code-wellformed)`);
|
|
3898
|
+
resolved = false;
|
|
3899
|
+
}
|
|
3900
|
+
else {
|
|
3901
|
+
node = matches[0];
|
|
3902
|
+
soFar += `.${seg}`;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
else if (!Object.prototype.hasOwnProperty.call(node, seg) ||
|
|
3906
|
+
typeof node[seg] !== 'object' ||
|
|
3907
|
+
node[seg] === null) {
|
|
3908
|
+
err('C89', `text "${t.id}": "${soFar}" has no nested structure "${seg}" (spelling-code-wellformed)`);
|
|
3909
|
+
resolved = false;
|
|
3910
|
+
}
|
|
3911
|
+
else {
|
|
3912
|
+
node = node[seg];
|
|
3913
|
+
soFar += `.${seg}`;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
if (resolved) {
|
|
3917
|
+
if (Array.isArray(node)) {
|
|
3918
|
+
err('C89', `text "${t.id}": "${soFar}" is a list — address one item's prose field by its key ("${soFar}.<key>.${field}") (spelling-code-wellformed)`);
|
|
3919
|
+
}
|
|
3920
|
+
else if (!PROSE_FIELDS.has(field)) {
|
|
3921
|
+
err('C89', `text "${t.id}": "${field}" is not a prose field (spelling-code-wellformed)`);
|
|
3922
|
+
}
|
|
3923
|
+
else if (
|
|
3924
|
+
// The snake_case → camelCase fallback is terminal-only by
|
|
3925
|
+
// design (the docstring's pin): the terminal segment is a
|
|
3926
|
+
// vocabulary lookup (snake_case prose vocabulary → the
|
|
3927
|
+
// parsed camelCase property), an intermediate segment is a
|
|
3928
|
+
// structure/key name taken verbatim.
|
|
3929
|
+
!Object.prototype.hasOwnProperty.call(node, field) &&
|
|
3930
|
+
!Object.prototype.hasOwnProperty.call(node, field.replace(/_([a-z])/g, (_, c) => c.toUpperCase()))) {
|
|
3931
|
+
err('C89', `text "${t.id}": "${soFar}" carries no prose field "${field}" — the default spelling's value belongs inline on the addressed structure (spelling-code-wellformed)`);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
const seen = new Set();
|
|
3938
|
+
for (const e of t.entries) {
|
|
3939
|
+
const parsed = (0, spelling_1.parseSpellingCode)(e.spelling);
|
|
3940
|
+
if (typeof parsed === 'string') {
|
|
3941
|
+
err('C89', `text "${t.id}": spelling code "${e.spelling}" — ${parsed} (spelling-code-wellformed)`);
|
|
3942
|
+
}
|
|
3943
|
+
if (m?.defaultSpelling && e.spelling === m.defaultSpelling) {
|
|
3944
|
+
err('C89', `text "${t.id}": "${e.spelling}" is the package's default spelling — its value belongs inline on the element, not in a text block (spelling-code-wellformed)`);
|
|
3945
|
+
}
|
|
3946
|
+
if (seen.has(e.spelling)) {
|
|
3947
|
+
err('C89', `text "${t.id}": duplicate "${e.spelling}" value — one value per code per content set; use an extension code to distinguish variants (spelling-code-wellformed)`);
|
|
3948
|
+
}
|
|
3949
|
+
seen.add(e.spelling);
|
|
3950
|
+
if (e.via) {
|
|
3951
|
+
const via = (0, spelling_1.parseConversionCode)(e.via);
|
|
3952
|
+
if (typeof via === 'string') {
|
|
3953
|
+
err('C89', `text "${t.id}": via "${e.via}" — ${via} (spelling-code-wellformed)`);
|
|
3954
|
+
}
|
|
3955
|
+
else if (via.titular.toLowerCase().startsWith('zz-')) {
|
|
3956
|
+
warn('C89', `text "${t.id}": via "${e.via}" is a user-assigned (zz-) conversion code — it validates, but user-assigned codes are not portable (spelling-code-wellformed)`);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return issues;
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* C77–C80 + C85 — edition lifecycle (TODO.roadmap/28; doctrine ch. 13
|
|
3965
|
+
* §13.4/§13.7) and the manifest base URN (TODO.roadmap/27). Editions are
|
|
3966
|
+
* packagings: the relations live on the manifest, and the checks run
|
|
3967
|
+
* WITHOUT composing content (the checkManifestResolution sibling
|
|
3968
|
+
* pattern — supersedes acyclicity is a property of the repo's manifest
|
|
3969
|
+
* set).
|
|
3970
|
+
*
|
|
3971
|
+
* C77 edition-status: a current/preview edition packages the register's
|
|
3972
|
+
* newest entry. (The status ENUM itself is enforced by the manifest
|
|
3973
|
+
* parser, like `kind` — an unknown token is a parse error.)
|
|
3974
|
+
* C78 edition-validity-window: the window is well-formed ISO 8601 and
|
|
3975
|
+
* `to` is not before `from`.
|
|
3976
|
+
* C79 edition-supersedes-resolves: supersedes/replaces targets are
|
|
3977
|
+
* well-formed URNs, never the package itself; a target naming an
|
|
3978
|
+
* earlier edition OF THE SAME document resolves against the edition
|
|
3979
|
+
* register (warning when the register omits it); the supersedes
|
|
3980
|
+
* graph over sibling manifests is acyclic.
|
|
3981
|
+
* C80 edition-pin-resolves (INV-8): every instance's
|
|
3982
|
+
* definition_versions pin resolves against the package's edition
|
|
3983
|
+
* register (editions ∪ {version}) — an unresolvable pin breaks the
|
|
3984
|
+
* re-execution guarantee (§13.5: after an edition change the engine
|
|
3985
|
+
* must know exactly which reports re-judge).
|
|
3986
|
+
* C85 baseurn-wellformed: the base URN is a well-formed IRI — it
|
|
3987
|
+
* grounds every downstream IRI (the RDF projection, provenance
|
|
3988
|
+
* comparisons), and the free-string field was the one manifest
|
|
3989
|
+
* IRI surface nothing validated.
|
|
3990
|
+
*/
|
|
3991
|
+
function checkEditionLifecycle(dir, standard) {
|
|
3992
|
+
const issues = [];
|
|
3993
|
+
const err = (check, message) => issues.push({ check, severity: 'error', message });
|
|
3994
|
+
const warn = (check, message) => issues.push({ check, severity: 'warning', message });
|
|
3995
|
+
const m = standard.packageManifest;
|
|
3996
|
+
if (!m) {
|
|
3997
|
+
return issues;
|
|
3998
|
+
}
|
|
3999
|
+
const register = new Set([
|
|
4000
|
+
...(m.editions ?? []),
|
|
4001
|
+
...(m.version ? [m.version] : []),
|
|
4002
|
+
]);
|
|
4003
|
+
// C77 — a current/preview edition is the register's newest entry.
|
|
4004
|
+
if (m.status === 'current' || m.status === 'preview') {
|
|
4005
|
+
const newest = (m.editions ?? [])[0];
|
|
4006
|
+
if (newest !== undefined && m.version !== newest) {
|
|
4007
|
+
err('C77', `package "${m.id}": status ${m.status} but version "${m.version}" is not the edition register's newest entry (${newest}) (edition-status)`);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
// C78 — the validity window is well-formed time (§13.7).
|
|
4011
|
+
if (m.validity) {
|
|
4012
|
+
const { from, to } = m.validity;
|
|
4013
|
+
if (!from || !((0, time_1.isDate)(from) || (0, time_1.isDateTime)(from))) {
|
|
4014
|
+
err('C78', `package "${m.id}": validity window from "${from}" is not an ISO 8601 date/datetime (edition-validity-window)`);
|
|
4015
|
+
}
|
|
4016
|
+
if (to !== undefined) {
|
|
4017
|
+
if (!((0, time_1.isDate)(to) || (0, time_1.isDateTime)(to))) {
|
|
4018
|
+
err('C78', `package "${m.id}": validity window to "${to}" is not an ISO 8601 date/datetime (edition-validity-window)`);
|
|
4019
|
+
}
|
|
4020
|
+
else if (from) {
|
|
4021
|
+
// Compare as INSTANTS, never lexicographically: a mixed
|
|
4022
|
+
// date/datetime pair (from 2021-01-01T00:00:00Z, to 2021-01-01)
|
|
4023
|
+
// is the same moment — string order false-positives on it.
|
|
4024
|
+
const fromMs = (0, time_1.timeInstantMs)(from);
|
|
4025
|
+
const toMs = (0, time_1.timeInstantMs)(to);
|
|
4026
|
+
if (fromMs !== null && toMs !== null && toMs < fromMs) {
|
|
4027
|
+
err('C78', `package "${m.id}": validity window to ${to} is before from ${from} (edition-validity-window)`);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
// C85 — the base URN is a well-formed IRI (TODO.roadmap/27, task-27c
|
|
4033
|
+
// review Important 1): a scheme, then no whitespace or IRI delimiters.
|
|
4034
|
+
// baseUrn grounds every downstream IRI (the RDF projection's document
|
|
4035
|
+
// node and instance IRIs, edition-normalized provenance comparisons);
|
|
4036
|
+
// the field is a free string, and a malformed value (`urn:bad urn`)
|
|
4037
|
+
// passed check while exports emitted spec-malformed IRIREFs silently.
|
|
4038
|
+
// The same shape the RDF export guards on (export/rdf.ts IRI_SHAPED) —
|
|
4039
|
+
// duplicated as one line rather than shared across the kernel/export
|
|
4040
|
+
// boundary.
|
|
4041
|
+
if (m.baseUrn &&
|
|
4042
|
+
!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"{}|^`\\]*$/.test(m.baseUrn)) {
|
|
4043
|
+
err('C85', `package "${m.id}": baseUrn "${m.baseUrn}" is not a well-formed IRI (a scheme followed by no whitespace or IRI delimiters) (baseurn-wellformed)`);
|
|
4044
|
+
}
|
|
4045
|
+
// C79 — supersedes/replaces: well-formed URNs, never self, resolving
|
|
4046
|
+
// against the register (same-document targets) and acyclic across the
|
|
4047
|
+
// repo's manifests (§13.7).
|
|
4048
|
+
const relations = [
|
|
4049
|
+
...(m.supersedes ?? []).map(t => ['supersedes', t]),
|
|
4050
|
+
...(m.replaces ?? []).map(t => ['replaces', t]),
|
|
4051
|
+
];
|
|
4052
|
+
const ownBasis = (0, model_diff_1.normalizeSourceRef)(m.baseUrn ?? '', '').basis;
|
|
4053
|
+
for (const [rel, target] of relations) {
|
|
4054
|
+
if (!target.startsWith('urn:')) {
|
|
4055
|
+
err('C79', `package "${m.id}": ${rel} "${target}" is not a URN — versioning relations name published package versions (edition-supersedes-resolves)`);
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
if (target === m.baseUrn) {
|
|
4059
|
+
err('C79', `package "${m.id}": ${rel} ${target} — a package cannot ${rel} itself (edition-supersedes-resolves)`);
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
const tNorm = (0, model_diff_1.normalizeSourceRef)(target, '');
|
|
4063
|
+
if (tNorm.basis === ownBasis && tNorm.edition) {
|
|
4064
|
+
if (!register.has(tNorm.edition)) {
|
|
4065
|
+
warn('C79', `package "${m.id}": ${rel} ${target}, but the edition register { ${[...register].join(' ')} } does not list ${tNorm.edition} (edition-supersedes-resolves)`);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
if (relations.length > 0) {
|
|
4070
|
+
// The supersedes graph over the repo's manifests (sibling dirs keyed
|
|
4071
|
+
// by baseUrn) must be acyclic — no superseding oneself through a
|
|
4072
|
+
// chain. Edges to URNs no sibling declares are external and cannot
|
|
4073
|
+
// close a cycle here.
|
|
4074
|
+
const nodes = new Map(); // baseUrn → relations
|
|
4075
|
+
const parent = (0, path_1.dirname)((0, path_1.resolve)(dir));
|
|
4076
|
+
for (const entry of (0, fs_1.readdirSync)(parent).sort()) {
|
|
4077
|
+
const full = (0, path_1.join)(parent, entry);
|
|
4078
|
+
try {
|
|
4079
|
+
if (!(0, fs_1.statSync)(full).isDirectory()) {
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
const sib = (0, package_1.readPackageManifest)(full);
|
|
4083
|
+
const key = sib.baseUrn || sib.id;
|
|
4084
|
+
nodes.set(key, [
|
|
4085
|
+
...(sib.supersedes ?? []).map(t => ['supersedes', t]),
|
|
4086
|
+
...(sib.replaces ?? []).map(t => ['replaces', t]),
|
|
4087
|
+
]);
|
|
4088
|
+
}
|
|
4089
|
+
catch {
|
|
4090
|
+
// Not a package dir — not a graph node.
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
const adj = new Map();
|
|
4094
|
+
for (const [baseUrn, rels] of nodes) {
|
|
4095
|
+
adj.set(baseUrn, rels
|
|
4096
|
+
.map(([, t]) => t)
|
|
4097
|
+
// Self-edges are reported by the dedicated self-supersession
|
|
4098
|
+
// check above — the cycle detector covers chains of length ≥ 2.
|
|
4099
|
+
.filter(t => nodes.has(t) && t !== baseUrn));
|
|
4100
|
+
}
|
|
4101
|
+
const cycle = findCycle(adj);
|
|
4102
|
+
if (cycle) {
|
|
4103
|
+
err('C79', `supersedes cycle: ${cycle.join(' → ')} — the supersedes graph must be acyclic; a package cannot supersede itself through a chain (edition-supersedes-resolves)`);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
// C80 — INV-8: definition version pins resolve against the edition
|
|
4107
|
+
// register. Silent on a register-less package (nothing to resolve
|
|
4108
|
+
// against); C18 still requires the pins themselves.
|
|
4109
|
+
if (register.size > 0) {
|
|
4110
|
+
for (const inst of standard.instances ?? []) {
|
|
4111
|
+
for (const [definition, version] of Object.entries(inst.definitionVersions ?? {})) {
|
|
4112
|
+
if (!register.has(version)) {
|
|
4113
|
+
err('C80', `instance ${inst.id}: definition_versions pin ${definition} : "${version}" does not resolve against the edition register { ${[...register].join(' ')} } — every executed definition is version-pinned to a declared edition (INV-8) (edition-pin-resolves)`);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
return issues;
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
4121
|
+
* DFS cycle detection on an adjacency map. Returns one cycle as a node
|
|
4122
|
+
* path (first node repeated at the end), or null when acyclic. Used by
|
|
4123
|
+
* C14 on the flow graph with timer-event nodes removed — any surviving
|
|
4124
|
+
* cycle is an unguarded loop.
|
|
4125
|
+
*/
|
|
4126
|
+
function findCycle(adj) {
|
|
4127
|
+
const GRAY = 1;
|
|
4128
|
+
const BLACK = 2;
|
|
4129
|
+
const color = new Map();
|
|
4130
|
+
const stack = [];
|
|
4131
|
+
let cycle = null;
|
|
4132
|
+
const visit = (n) => {
|
|
4133
|
+
if (cycle) {
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
color.set(n, GRAY);
|
|
4137
|
+
stack.push(n);
|
|
4138
|
+
for (const m of adj.get(n) ?? []) {
|
|
4139
|
+
if (cycle) {
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
const c = color.get(m) ?? 0;
|
|
4143
|
+
if (c === GRAY) {
|
|
4144
|
+
cycle = [...stack.slice(stack.indexOf(m)), m];
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (c !== BLACK) {
|
|
4148
|
+
visit(m);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
stack.pop();
|
|
4152
|
+
color.set(n, BLACK);
|
|
4153
|
+
};
|
|
4154
|
+
for (const n of adj.keys()) {
|
|
4155
|
+
if (cycle) {
|
|
4156
|
+
break;
|
|
4157
|
+
}
|
|
4158
|
+
if (!color.has(n)) {
|
|
4159
|
+
visit(n);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
return cycle;
|
|
4163
|
+
}
|
|
4164
|
+
//# sourceMappingURL=check.js.map
|