diff --git a/changes/unreleased/query-fanout-failures.fixed.md b/changes/unreleased/query-fanout-failures.fixed.md new file mode 100644 index 0000000000..f837350740 --- /dev/null +++ b/changes/unreleased/query-fanout-failures.fixed.md @@ -0,0 +1,2 @@ +- `Project`/`WhereFeature` on a feature several element rows lack report every such element in the QVTS record (one path failure each) and name the others in the message, instead of stopping at the first. +- The `ambiguousName` finding of `Named` names the namespace that owns the duplicated members, as the QVTS reference semantics do, not the duplicated name. diff --git a/docs/internals/design/query-outcomes.md b/docs/internals/design/query-outcomes.md index b43bcb7dee..a1a02423e2 100644 --- a/docs/internals/design/query-outcomes.md +++ b/docs/internals/design/query-outcomes.md @@ -25,7 +25,7 @@ to `∅` is the ambiguity QVTS exists to remove. | Step | Value | Path error | Model error | |:--|:--|:--|:--| -| `Named` | the element | `unknown-element`: the longest prefix that resolves, the segment it lacks, the nearest spellings; an alias that denotes nothing | `ambiguous-element`: the elements, by qualified name | +| `Named` | the element | `unknown-element`: the longest prefix that resolves, the segment it lacks, the nearest spellings; an alias that denotes nothing | `ambiguous-element`: the elements, by qualified name; the finding names the owning namespace | | `WhereType`, `WhereMetadata` | the selected rows | `unknown-classification` | — | | `RelatedElements` | the neighbours, possibly none | `unknown-relationship` | — | | `Project`, `WhereFeature` on a feature the element lacks | an empty cell, with `tolerate = true` | `missing-feature`: the element (`Target`), the feature (`Property`), the features it does have (`Candidates`); `unknown-property` when no row carries it | — | @@ -39,7 +39,7 @@ QVTS record itself (`internal/doc/queryexec/outcome.go`), field for field with `lean/QVTS/Outcome.lean`: a `PathError` is a list of `Failure{Step, Expected, Frontier}`, a `ModelError` a list of `Finding{Step, Element, Constraint, Observed}` plus the path failures of the same fan-out; `Frontier` carries `At`, `Members (name, metaclass)`, `Features (name, type)`, `Kinds` -and `Nearest`. `Named` and the `missing-feature` outcome of `Project`/`WhereFeature` fill it today. The surfaces that show it: `-run-query` prints it under the +and `Nearest`. `Named` and the `missing-feature` outcome of `Project`/`WhereFeature` fill it today, the latter with one failure per element lacking the feature (the fan-out keeps every failing element, as `fanOut_pathError_all` in qvts requires). The surfaces that show it: `-run-query` prints it under the error line and `-json` writes it as `checks[].outcome` in the shape `qvts/sparql/expected/*.json` records (`outcome`, `step`, `failures` | `findings` + `pathFailures`); `%run-query` prints the same lines; the gRPC service attaches it to the failed call as a `QueryOutcome` error detail. The diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index f4bc29816c..49568fe09a 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -230,21 +230,21 @@ disagreement, which `-check` then shows as that row moving out of the column. ## Results (pilot `2026-09`, 924 files) -**Scorecard:** 931 files; 92455 elements matched (18479 ours-only, 37766 pilot-only, 0 of them implied); properties: 1121587 present-equal, 27497 present-different, 1074012 ours-missing, 68257 pilot-missing, 1973372 absent-both, 26286 indeterminate, 514 pilot-nondeterministic. +**Scorecard:** 931 files; 92491 elements matched (18479 ours-only, 37775 pilot-only, 0 of them implied); properties: 1122005 present-equal, 27547 present-different, 1074516 ours-missing, 68275 pilot-missing, 1974074 absent-both, 26286 indeterminate, 514 pilot-nondeterministic. The pilot's export failed on 59 files (`pilotFailed`: its own export throws on an element without a resource, 52 of them specification examples, 4 test fixtures and, since pilot `2026-09`, the three `Metadata Examples/{Requirement,Risk,Verification}MetadataExample.sysml` whose enumeration-valued metadata bindings the pilot now evaluates into elements with no resource; the files are named in the run's text report), ours refused 36 (all of them specification examples the parser could not read whole), left the pilot's 59 failed files out and converted 28 documents alone for declaring an element another document of their root declares (`oursColliding`); no root fell back to per-document conversion. 11728 (metaclass, property) rows carry a comparison; 111949 references compared through the unkeyed sentinel, 159941 of our `sysx:` annotations were left out, and 287/347 elements (ours/pilot) were dropped for ambiguous keys, 17040/32159 for having none. | root | files | matched | ours-only | pilot-only | present-equal | present-different | ours-missing | pilot-missing | |---|---:|---:|---:|---:|---:|---:|---:|---:| -| `examples/sysml-v2-training` | 100 | 7391 | 1150 | 2775 | 90295 | 1842 | 86649 | 5126 | -| `examples/pilot-corpora/sysml-examples` | 99 | 26536 | 6150 | 9580 | 327975 | 7668 | 312072 | 19290 | -| `examples/pilot-corpora/sysml-validation` | 56 | 9377 | 1882 | 3222 | 114006 | 3162 | 108845 | 6811 | -| `examples/pilot-corpora/kerml-examples` | 58 | 6399 | 1166 | 1842 | 73725 | 2504 | 63927 | 5784 | -| `examples/spec-examples` | 511 | 7074 | 990 | 2656 | 76404 | 1567 | 74074 | 5295 | +| `examples/sysml-v2-training` | 100 | 7391 | 1150 | 2775 | 90295 | 1842 | 86610 | 5126 | +| `examples/pilot-corpora/sysml-examples` | 99 | 26536 | 6150 | 9580 | 327975 | 7668 | 311994 | 19290 | +| `examples/pilot-corpora/sysml-validation` | 56 | 9377 | 1882 | 3222 | 114006 | 3162 | 108820 | 6811 | +| `examples/pilot-corpora/kerml-examples` | 58 | 6399 | 1166 | 1842 | 73725 | 2504 | 63918 | 5784 | +| `examples/spec-examples` | 511 | 7074 | 990 | 2656 | 76404 | 1567 | 74068 | 5295 | | `tests/testdata` | 40 | 1460 | 175 | 511 | 16445 | 428 | 15857 | 1042 | -| `examples` | 52 | 26974 | 6046 | 15072 | 333478 | 8320 | 322907 | 20056 | +| `examples` | 52 | 27002 | 6046 | 15079 | 333821 | 8341 | 323194 | 20070 | | `tools/referee/diff/testdata` | 4 | 53 | 0 | 0 | 555 | 1 | 454 | 41 | -| `docs/manual/examples` | 11 | 7191 | 920 | 2108 | 88704 | 2005 | 89511 | 4812 | +| `docs/manual/examples` | 11 | 7199 | 920 | 2110 | 88779 | 2034 | 89601 | 4816 | The scorecard is mostly **ours-missing**: this repository's writer emits a subset of the metamodel's properties, and the referee now measures which, per metaclass. The full ranked scorecard with one concrete example per differing row is `build/pilot-apijson-diff/pilot-apijson-diff.txt` after a run; the baseline keeps every row's seven counts. @@ -254,16 +254,16 @@ The largest **present-different** rows, with the reading the examples support: | metaclass | property | present-equal | present-different | |---|---|---:|---:| -| `Feature` | `ownedRelationship` | 4159 | 1401 | -| `AttributeUsage` | `ownedRelationship` | 1248 | 1197 | -| `PartUsage` | `ownedRelationship` | 874 | 1004 | -| `PartUsage` | `ownedMember` | 368 | 702 | -| `PartUsage` | `ownedMembership` | 722 | 634 | -| `AttributeUsage` | `ownedMembership` | 1184 | 624 | -| `FeatureMembership` | `memberElement` | 9381 | 570 | -| `FeatureMembership` | `ownedMemberElement` | 9381 | 570 | -| `FeatureMembership` | `ownedRelatedElement` | 9381 | 570 | -| `Feature` | `ownedMembership` | 4271 | 569 | +| `Feature` | `ownedRelationship` | 4163 | 1430 | +| `AttributeUsage` | `ownedRelationship` | 1246 | 1245 | +| `PartUsage` | `ownedRelationship` | 835 | 1067 | +| `PartUsage` | `ownedMember` | 337 | 736 | +| `PartUsage` | `ownedMembership` | 677 | 682 | +| `AttributeUsage` | `ownedMembership` | 1183 | 668 | +| `FeatureMembership` | `memberElement` | 9488 | 570 | +| `FeatureMembership` | `ownedMemberElement` | 9488 | 570 | +| `FeatureMembership` | `ownedRelatedElement` | 9488 | 570 | +| `Feature` | `ownedMembership` | 4305 | 569 | ### Where the pilot states nothing and we do @@ -418,17 +418,17 @@ The (metaclass, property) rows ranked by the corpus elements on which the pilot | 5 | `FeatureMembership` | `source` | 10956 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::source`, which Membership redefines as `[membershipOwningNamespace]` (KerML 8.3.2.3.5) | | 6 | `FeatureMembership` | `target` | 10956 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::target`, which Membership redefines as `[memberElement]` (KerML 8.3.2.3.5) | | 7 | `FeatureMembership` | `visibility` | 10892 | 1 | PR #1232 (`feature/api-json-derived-properties-layered`) | owned attribute `Membership::visibility : VisibilityKind`, default `public` (KerML 8.3.2.3.5); not derived | -| 8 | `Feature` | `featureTarget` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::featureTarget`: the last `chainingFeature`, else the feature itself (KerML 8.3.3.3, deriveFeatureFeatureTarget; KERML11-1 crossFeature) | -| 9 | `Feature` | `inheritedFeature` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::inheritedFeature = inheritedMembership.memberElement->selectByKind(Feature)` (KerML 8.3.3.1, deriveTypeInheritedFeature) | -| 10 | `Feature` | `inheritedMembership` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::inheritedMembership = inheritedMemberships(excluded)` over the supertypes (KerML 8.3.3.1, deriveTypeInheritedMembership) | -| 11 | `Feature` | `isConjugated` | 10180 | 0 | none found | `Type::isConjugated = ownedConjugator <> null` (KerML 8.3.3.1 Type; a derived attribute without a named derive constraint); the pilot writes `false` on every type | -| 12 | `Feature` | `isLibraryElement` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | -| 13 | `Feature` | `member` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Namespace::member = membership.memberElement` (KerML 8.3.2.4, deriveNamespaceMember) | -| 14 | `Feature` | `membership` | 10180 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Namespace::membership = ownedMembership->union(importedMembership)`, and for a Type also `inheritedMembership` (KerML 8.3.2.4, 8.3.3.1) | -| 15 | `Feature` | `feature` | 10174 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::feature = featureMembership.ownedMemberFeature` (KerML 8.3.3.1, deriveTypeFeature) | -| 16 | `Feature` | `featureMembership` | 10174 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::featureMembership = ownedFeatureMembership->union(inheritedMembership->selectByKind(FeatureMembership))` (KerML 8.3.3.1; KERML11-191 restricts the inherited part to the supertypes) | -| 17 | `Feature` | `featuringType` | 9878 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::featuringType = typeFeaturing.featuringType`, plus the owning type of an owned feature (KerML 8.3.3.3, deriveFeatureFeaturingType; KERML11-3/4 multiplicity featuring) | -| 18 | `Feature` | `type` | 9647 | 14 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::type = typing.type` with the types inherited through chaining and redefinition (KerML 8.3.3.3, deriveFeatureType) | +| 8 | `Feature` | `featureTarget` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::featureTarget`: the last `chainingFeature`, else the feature itself (KerML 8.3.3.3, deriveFeatureFeatureTarget; KERML11-1 crossFeature) | +| 9 | `Feature` | `inheritedFeature` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::inheritedFeature = inheritedMembership.memberElement->selectByKind(Feature)` (KerML 8.3.3.1, deriveTypeInheritedFeature) | +| 10 | `Feature` | `inheritedMembership` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::inheritedMembership = inheritedMemberships(excluded)` over the supertypes (KerML 8.3.3.1, deriveTypeInheritedMembership) | +| 11 | `Feature` | `isConjugated` | 10189 | 0 | none found | `Type::isConjugated = ownedConjugator <> null` (KerML 8.3.3.1 Type; a derived attribute without a named derive constraint); the pilot writes `false` on every type | +| 12 | `Feature` | `isLibraryElement` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | +| 13 | `Feature` | `member` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Namespace::member = membership.memberElement` (KerML 8.3.2.4, deriveNamespaceMember) | +| 14 | `Feature` | `membership` | 10189 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Namespace::membership = ownedMembership->union(importedMembership)`, and for a Type also `inheritedMembership` (KerML 8.3.2.4, 8.3.3.1) | +| 15 | `Feature` | `feature` | 10183 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::feature = featureMembership.ownedMemberFeature` (KerML 8.3.3.1, deriveTypeFeature) | +| 16 | `Feature` | `featureMembership` | 10183 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Type::featureMembership = ownedFeatureMembership->union(inheritedMembership->selectByKind(FeatureMembership))` (KerML 8.3.3.1; KERML11-191 restricts the inherited part to the supertypes) | +| 17 | `Feature` | `featuringType` | 9887 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::featuringType = typeFeaturing.featuringType`, plus the owning type of an owned feature (KerML 8.3.3.3, deriveFeatureFeaturingType; KERML11-3/4 multiplicity featuring) | +| 18 | `Feature` | `type` | 9656 | 14 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::type = typing.type` with the types inherited through chaining and redefinition (KerML 8.3.3.3, deriveFeatureType) | | 19 | `OwningMembership` | `isLibraryElement` | 9653 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | | 20 | `OwningMembership` | `relatedElement` | 9653 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::relatedElement = source->union(target)`, ordered (KerML 8.3.2.5.2, deriveRelationshipRelatedElement) | | 21 | `OwningMembership` | `source` | 9653 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::source`, which Membership redefines as `[membershipOwningNamespace]` (KerML 8.3.2.3.5) | @@ -436,23 +436,23 @@ The (metaclass, property) rows ranked by the corpus elements on which the pilot | 23 | `OwningMembership` | `ownedMemberElementId` | 9650 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `OwningMembership::ownedMemberElementId = ownedMemberElement.elementId` (KerML 8.3.2.3.6) | | 24 | `OwningMembership` | `target` | 9650 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::target`, which Membership redefines as `[memberElement]` (KerML 8.3.2.3.5) | | 25 | `OwningMembership` | `visibility` | 9628 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | owned attribute `Membership::visibility : VisibilityKind`, default `public` (KerML 8.3.2.3.5); not derived | -| 26 | `Feature` | `name` | 9570 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Element::name = effectiveName()`: the declared name, or for a feature the name of the feature it redefines (KerML 8.3.2.1, deriveElementName) | -| 27 | `Feature` | `owningFeatureMembership` | 9529 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningFeatureMembership = owningMembership->selectByKind(FeatureMembership)` (KerML 8.3.3.3) | -| 28 | `Feature` | `owningType` | 9529 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningType = owningFeatureMembership.owningType` (KerML 8.3.3.3, deriveFeatureOwningType) | +| 26 | `Feature` | `name` | 9579 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Element::name = effectiveName()`: the declared name, or for a feature the name of the feature it redefines (KerML 8.3.2.1, deriveElementName) | +| 27 | `Feature` | `owningFeatureMembership` | 9538 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningFeatureMembership = owningMembership->selectByKind(FeatureMembership)` (KerML 8.3.3.3) | +| 28 | `Feature` | `owningType` | 9538 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningType = owningFeatureMembership.owningType` (KerML 8.3.3.3, deriveFeatureOwningType) | | 29 | `FeatureMembership` | `memberName` | 9405 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Membership::memberName = memberElement.name` (KerML 8.3.2.3.5, deriveMembershipMemberName) | | 30 | `FeatureMembership` | `ownedMemberName` | 9405 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `OwningMembership::ownedMemberName = ownedMemberElement.name` (KerML 8.3.2.3.6) | -| 31 | `Feature` | `owningNamespace` | 8789 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Element::owningNamespace = owningMembership.membershipOwningNamespace` (KerML 8.3.2.1, deriveElementOwningNamespace) | -| 32 | `FeatureValue` | `isLibraryElement` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | -| 33 | `FeatureValue` | `memberElementId` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Membership::memberElementId = memberElement.elementId` (KerML 8.3.2.3.5, deriveMembershipMemberElementId) | -| 34 | `FeatureValue` | `ownedMemberElementId` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `OwningMembership::ownedMemberElementId = ownedMemberElement.elementId` (KerML 8.3.2.3.6) | -| 35 | `FeatureValue` | `relatedElement` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::relatedElement = source->union(target)`, ordered (KerML 8.3.2.5.2, deriveRelationshipRelatedElement) | -| 36 | `FeatureValue` | `source` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::source`, which Membership redefines as `[membershipOwningNamespace]` (KerML 8.3.2.3.5) | -| 37 | `FeatureValue` | `target` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::target`, which Membership redefines as `[memberElement]` (KerML 8.3.2.3.5) | -| 38 | `FeatureValue` | `visibility` | 7177 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | owned attribute `Membership::visibility : VisibilityKind`, default `public` (KerML 8.3.2.3.5); not derived | +| 31 | `Feature` | `owningNamespace` | 8798 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Element::owningNamespace = owningMembership.membershipOwningNamespace` (KerML 8.3.2.1, deriveElementOwningNamespace) | +| 32 | `FeatureValue` | `isLibraryElement` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | +| 33 | `FeatureValue` | `memberElementId` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Membership::memberElementId = memberElement.elementId` (KerML 8.3.2.3.5, deriveMembershipMemberElementId) | +| 34 | `FeatureValue` | `ownedMemberElementId` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `OwningMembership::ownedMemberElementId = ownedMemberElement.elementId` (KerML 8.3.2.3.6) | +| 35 | `FeatureValue` | `relatedElement` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::relatedElement = source->union(target)`, ordered (KerML 8.3.2.5.2, deriveRelationshipRelatedElement) | +| 36 | `FeatureValue` | `source` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::source`, which Membership redefines as `[membershipOwningNamespace]` (KerML 8.3.2.3.5) | +| 37 | `FeatureValue` | `target` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Relationship::target`, which Membership redefines as `[memberElement]` (KerML 8.3.2.3.5) | +| 38 | `FeatureValue` | `visibility` | 7186 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | owned attribute `Membership::visibility : VisibilityKind`, default `public` (KerML 8.3.2.3.5); not derived | | 39 | `FeatureTyping` | `isLibraryElement` | 6074 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | | 40 | `FeatureTyping` | `owningType` | 6070 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningType = owningFeatureMembership.owningType` (KerML 8.3.3.3, deriveFeatureOwningType) | | 41 | `Redefinition` | `redefinedFeature` | 52 | 20 | closed for this repository's libraries (above) | `Redefinition::redefinedFeature : Feature` (KerML 8.3.3.3.8) is an element reference, and every `Element` carries an `elementId` (KerML 8.3.2.1.2) the API's JSON refers to it by; the 330 redefinitions of a feature of this repository's libraries once spelled as a name, `{"@ref": "caption"}`, now link by the element's fixed id — the same movement for `FeatureTyping::type`, `MetadataUsage::type`, `FeatureReferenceExpression::referent` and `ReferenceSubsetting::referencedFeature` into those libraries — and the 58 left are redefinitions of a feature inherited within the model itself that the writer spells as a name (`vehicle_b_1::providePower`) | -| 42 | `AttributeUsage` | `ownedRelationship` | 36 | 1245 | none found | `Element::ownedRelationship : Relationship [0..*] {ordered}` (KerML 8.3.2.1.2), in the order the notation adds them (KerML 8.2.1 Table 3; `FeatureSpecializationPart`, then `ValuePart`, then the body): a head's typings, specializations and multiplicity as spelled, then its `FeatureValue`, then its members. The writer seated the materialized relationships last, in a fixed kind order; it now owns them as spelled, which moved 4861 properties over 101 rows (67 `ownedRelationship`, 22 `ownedSpecialization`, 12 `ownedSubsetting`) from present-different to present-equal, no other bucket touched (this row 2332 → 1197, `PartDefinition` 215 → 20, `PartUsage` 1554 → 1004, `Feature` 1802 → 1401). What remains (1202) is a different shape: on a usage that writes no multiplicity part (`attribute cargoMass : MassValue;`) the pilot owns an `OwningMembership` of a `Multiplicity`, not implied, after the `FeatureTyping`; ours states none | +| 42 | `AttributeUsage` | `ownedRelationship` | 36 | 1245 | none found | `Element::ownedRelationship : Relationship [0..*] {ordered}` (KerML 8.3.2.1.2), in the order the notation adds them (KerML 8.2.1 Table 3; `FeatureSpecializationPart`, then `ValuePart`, then the body): a head's typings, specializations and multiplicity as spelled, then its `FeatureValue`, then its members. The writer seated the materialized relationships last, in a fixed kind order; it now owns them as spelled, which moved 4861 properties over 101 rows (67 `ownedRelationship`, 22 `ownedSpecialization`, 12 `ownedSubsetting`) from present-different to present-equal, no other bucket touched (at that re-record this row 2332 → 1197, `PartDefinition` 215 → 20, `PartUsage` 1554 → 1004, `Feature` 1802 → 1401; the current baseline, with the re-pinned libraries and the examples that say `tolerate`, reads 1245, 20, 1067 and 1430). What remains (1245 on this row) is a different shape: on a usage that writes no multiplicity part (`attribute cargoMass : MassValue;`) the pilot owns an `OwningMembership` of a `Multiplicity`, not implied, after the `FeatureTyping`; ours states none | | 43 | `FeatureReferenceExpression` | `ownedMembership` | 0 | 7 | the writer owns the `Membership` (`export/rdf_expr.go` `featureReferent`); the 7 left are the `FunctionReferenceMember` shape, a `FeatureMembership` owning an `Expression` typed by the named function (KerML 8.2.5.8.3), which ours writes as a `Membership` of the function | `FeatureReferenceExpression::referent` is the `memberElement` of the first `ownedMembership` that is not a `ParameterMembership` (KerML 8.3.4.8.5, deriveFeatureReferenceExpressionReferent); the notation maps a feature reference to that owned `Membership` (8.2.5.8.3, `FeatureReferenceMember`) | Rows an evaluator rule in PR #1232 (`internal/semantic/metamodel/rules.go`) already names are marked; that PR adds a lazy metamodel evaluator and layers its derived values into the API-JSON writer, so its merge is expected to move those rows from ours-missing to present-equal or present-different, and this referee is how that movement is measured. diff --git a/internal/doc/queryexec/errors.go b/internal/doc/queryexec/errors.go index 57c12a8921..aaca6878fc 100644 --- a/internal/doc/queryexec/errors.go +++ b/internal/doc/queryexec/errors.go @@ -101,6 +101,8 @@ type Error struct { // Outcome is the QVTS record behind a path or model error (outcome.go); nil for // the failures that are neither, the flat fields above being its rendering. Outcome *Outcome + // Also names the other elements the same fan-out failed on (missing-feature). + Also []string // Cause is the evaluator's reason an unevaluable feature could not be read. Cause error } @@ -153,20 +155,22 @@ func (e *Error) missingFeatureMessage() string { if e.Actual != "" { msg = fmt.Sprintf("query %s reads feature %s of %s, but the path stops at %s", e.Query, e.Property, e.Target, e.Actual) } - if len(e.Candidates) == 0 { - return msg + if len(e.Candidates) > 0 { + msg += ": its features are " + strings.Join(e.Candidates, ", ") + if len(e.Candidates) > 3 { + msg = suggest.With(msg, e.missingSegment(), e.Candidates) + } } - msg += ": its features are " + strings.Join(e.Candidates, ", ") - if len(e.Candidates) <= 3 { - return msg + if len(e.Also) > 0 { + msg += fmt.Sprintf(" (also absent on %s)", strings.Join(e.Also, ", ")) } - return suggest.With(msg, e.missingSegment(), e.Candidates) + return msg } // missingSegment is the segment the record says names nothing: the failed step's // argument, so a path that repeats a name cannot misdirect the suggestion. func (e *Error) missingSegment() string { - if e.Outcome != nil && e.Outcome.Path != nil && len(e.Outcome.Path.Failures) == 1 { + if e.Outcome != nil && e.Outcome.Path != nil && len(e.Outcome.Path.Failures) > 0 { return e.Outcome.Path.Failures[0].Step.Argument } return e.Property diff --git a/internal/doc/queryexec/missing_feature_test.go b/internal/doc/queryexec/missing_feature_test.go index b6688cd61d..35b6052480 100644 --- a/internal/doc/queryexec/missing_feature_test.go +++ b/internal/doc/queryexec/missing_feature_test.go @@ -15,6 +15,7 @@ part def Heavy { attribute mass : ScalarValues::Real; } part root { part a : Heavy { attribute :>> mass = 1.0; } part b { attribute volume = 2.0; attribute mas = 3.0; } + part c { attribute volume = 4.0; } } calc def Masses :> Query { in source : Element; @@ -50,12 +51,15 @@ calc def ToleratedHeavyOnes :> Query { if got := strings.Join(executionError.Candidates, ","); got != "mas,volume" { t.Fatalf("%s: frontier = %q, want the features of root::b", query, got) } - if msg := executionError.Error(); !strings.HasSuffix(msg, "of Observatory::root::b, which has no such feature: its features are mas, volume") { + if msg := executionError.Error(); !strings.HasSuffix(msg, "of Observatory::root::b, which has no such feature: its features are mas, volume (also absent on Observatory::root::c)") { t.Fatalf("%s: message = %q", query, msg) } outcome := executionError.Outcome - if outcome == nil || outcome.Kind() != "pathError" || len(outcome.Path.Failures) != 1 { - t.Fatalf("%s: outcome = %+v, want one path failure", query, outcome) + if outcome == nil || outcome.Kind() != "pathError" || len(outcome.Path.Failures) != 2 { + t.Fatalf("%s: outcome = %+v, want one path failure per element lacking mass", query, outcome) + } + if second := outcome.Path.Failures[1]; second.Frontier.At != "Observatory::root::c" || len(second.Frontier.Features) != 1 || second.Frontier.Features[0].Name != "volume" { + t.Fatalf("%s: second failure = %+v", query, second) } failure := outcome.Path.Failures[0] if failure.Step.Label() != "Feature(mass)" || failure.Expected != "a feature named mass on the element" || failure.Frontier.At != "Observatory::root::b" || failure.Frontier.Nearest != "mas" { @@ -69,8 +73,8 @@ calc def ToleratedHeavyOnes :> Query { if err != nil { t.Fatalf("ToleratedMasses: %v", err) } - if len(tolerated.Rows()) != 2 || len(tolerated.Rows()[0].Cells()[0].Values()) != 1 || len(tolerated.Rows()[1].Cells()[0].Values()) != 0 { - t.Fatalf("ToleratedMasses: rows = %+v, want a's mass and an empty cell for b", tolerated.Rows()) + if len(tolerated.Rows()) != 3 || len(tolerated.Rows()[0].Cells()[0].Values()) != 1 || len(tolerated.Rows()[1].Cells()[0].Values()) != 0 || len(tolerated.Rows()[2].Cells()[0].Values()) != 0 { + t.Fatalf("ToleratedMasses: rows = %+v, want a's mass and empty cells for b and c", tolerated.Rows()) } kept, err := fixture.execute(t, "ToleratedHeavyOnes", bindings, Options{}) if err != nil || len(kept.Rows()) != 1 { diff --git a/internal/doc/queryexec/named.go b/internal/doc/queryexec/named.go index 3803facbd6..9f845335ec 100644 --- a/internal/doc/queryexec/named.go +++ b/internal/doc/queryexec/named.go @@ -19,10 +19,11 @@ func (e *executor) evaluateNamed(expression queryplan.Expression) (sequence, err } var result sequence for _, name := range names { - elements, dangling := e.denoted(name) + found, dangling := e.denoted(name) + elements := elementsOf(found) switch { case len(elements) > 1: - return sequence{}, e.ambiguousElement(expression, name, elements) + return sequence{}, e.ambiguousElement(expression, name, found) case len(elements) == 0: return sequence{}, e.unknownElement(expression, name, dangling) } @@ -33,29 +34,64 @@ func (e *executor) evaluateNamed(expression queryplan.Expression) (sequence, err // denoted resolves name to the distinct elements it denotes, aliases followed // and deduplicated; dangling reports a candidate alias that denotes nothing. -func (e *executor) denoted(name string) (elements []*symbols.Symbol, dangling bool) { +// denotation is a registration the name matched — a declaration or an alias — and +// the element it denotes; every registration is kept, elementsOf dedups the targets. +type denotation struct { + registration *symbols.Symbol + element *symbols.Symbol +} + +func (e *executor) denoted(name string) (found []denotation, dangling bool) { for _, candidate := range e.context.Index.LookupUniqueCandidates(name) { element, ok := e.context.Resolver.ResolveAliasTarget(candidate) if !ok || element == nil { dangling = true continue } - if !slices.ContainsFunc(elements, func(o *symbols.Symbol) bool { return symbols.SameElement(o, element) }) { - elements = append(elements, element) + found = append(found, denotation{registration: candidate, element: element}) + } + return found, dangling +} + +// elementsOf is the distinct elements the registrations denote. +func elementsOf(found []denotation) []*symbols.Symbol { + out := make([]*symbols.Symbol, 0, len(found)) + for _, d := range found { + if !slices.ContainsFunc(out, func(o *symbols.Symbol) bool { return symbols.SameElement(o, d.element) }) { + out = append(out, d.element) } } - return elements, dangling + return out } // ambiguousElement reports a name several elements carry, each by its // fully-qualified name, in name order. -func (e *executor) ambiguousElement(expression queryplan.Expression, name string, candidates []*symbols.Symbol) error { +func (e *executor) ambiguousElement(expression queryplan.Expression, name string, candidates []denotation) error { fqns := make([]string, 0, len(candidates)) - for _, candidate := range candidates { - fqns = append(fqns, symbols.FQNOf(candidate)) + for _, element := range elementsOf(candidates) { + fqns = append(fqns, symbols.FQNOf(element)) } sort.Strings(fqns) + // One finding per namespace declaring the duplicate names (an alias counts where it + // is declared, not where its target is), each listing every element denoted there. + byOwner := map[string][]string{} + for _, candidate := range candidates { + owner := ownerOf(symbols.FQNOf(candidate.registration)) + byOwner[owner] = append(byOwner[owner], symbols.FQNOf(candidate.element)) + } + for owner := range byOwner { + sort.Strings(byOwner[owner]) + } step := Step{Kind: StepNamed, Argument: name} + owners := make([]string, 0, len(byOwner)) + for owner := range byOwner { + owners = append(owners, owner) + } + sort.Strings(owners) + findings := make([]Finding, 0, len(owners)) + for _, owner := range owners { + findings = append(findings, Finding{Step: step, Element: owner, Constraint: ConstraintAmbiguousName, Observed: declaredAs(byOwner[owner])}) + } return &Error{ Kind: ErrorAmbiguousElement, Query: e.definition.Name(), @@ -63,13 +99,16 @@ func (e *executor) ambiguousElement(expression queryplan.Expression, name string Actual: name, Candidates: fqns, Origin: expression.Origin(), - Outcome: &Outcome{Model: &ModelError{Findings: []Finding{{ - Step: step, - Element: name, - Constraint: ConstraintAmbiguousName, - Observed: declaredAs(fqns), - }}}}, + Outcome: &Outcome{Model: &ModelError{Findings: findings}}, + } +} + +// ownerOf is the namespace a qualified name is declared in; a root name owns itself. +func ownerOf(fqn string) string { + if i := strings.LastIndex(fqn, "::"); i >= 0 { + return fqn[:i] } + return fqn } // unknownElement reports a name no element carries with its frontier: the longest @@ -98,7 +137,8 @@ func (e *executor) unknownElement(expression queryplan.Expression, name string, segments := strings.Split(name, "::") for i := len(segments) - 1; i >= 1; i-- { prefix := strings.Join(segments[:i], "::") - owners, dangling := e.denoted(prefix) + found, dangling := e.denoted(prefix) + owners := elementsOf(found) switch len(owners) { case 0: if dangling { @@ -119,7 +159,7 @@ func (e *executor) unknownElement(expression queryplan.Expression, name string, default: // The first segment that does not resolve is one several elements carry: // the frontier is that ambiguity, a finding about the model. - return e.ambiguousElement(expression, prefix, owners) + return e.ambiguousElement(expression, prefix, found) } } err.Expected = segments[0] diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 801d2cbeed..726c0bfebe 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -407,15 +407,17 @@ func (e *executor) evaluateWhereFeature(expression queryplan.Expression) (sequen } result := filtered(source) known := false - var missing error + var missing []*Error columnIndex := projectedColumn(source, property) for i := range source.values { values, present, valueErr := e.featureValues(expression, source, i, property, columnIndex) if valueErr != nil { return sequence{}, valueErr } - if !present && !tolerate && missing == nil { - missing = e.missingFeature(expression, property, source.values[i]) + if !present && !tolerate { + if m := e.missingFeature(expression, property, source.values[i]); m != nil { + missing = append(missing, m) + } } known = known || present for _, actual := range values { @@ -435,8 +437,8 @@ func (e *executor) evaluateWhereFeature(expression queryplan.Expression) (sequen if !known { return sequence{}, e.unknownProperty(expression, property) } - if missing != nil { - return sequence{}, missing + if err := joinMissing(missing); err != nil { + return sequence{}, err } return result, nil } @@ -650,7 +652,7 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e result.columns[len(properties)+i] = Column{name: column.name, origin: column.origin.Origin()} } tracker := newPropertyTracker() - missing := make([]error, len(properties)) + missing := make([][]*Error, len(properties)) for row, value := range source.values { result.cells[row] = make([]Cell, total) for column, property := range properties { @@ -658,8 +660,10 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e if valueErr != nil { return sequence{}, e.unevaluable(expression, property, value, valueErr) } - if !present && !tolerate && missing[column] == nil { - missing[column] = e.missingFeature(expression, property, value) + if !present && !tolerate { + if m := e.missingFeature(expression, property, value); m != nil { + missing[column] = append(missing[column], m) + } } known[column] = known[column] || present result.cells[row][column] = Cell{ @@ -686,8 +690,8 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e } // A property some rows carry and an element row lacks is a path error // about that element, unless tolerated; one no row carries is unknown. - for _, err := range missing { - if err != nil { + for _, column := range missing { + if err := joinMissing(column); err != nil { return sequence{}, err } } @@ -1208,7 +1212,7 @@ func (e *executor) tolerateArgument(expression queryplan.Expression) (bool, erro // missingFeature reports a feature the element row lacks, its own features as // the frontier; nil for a non-element row or a metamodel property every element answers. -func (e *executor) missingFeature(expression queryplan.Expression, property string, row Value) error { +func (e *executor) missingFeature(expression queryplan.Expression, property string, row Value) *Error { sym, isElement := row.Element() if !isElement || sym == nil || isQueryableProperty(property) || strings.Contains(property, "::") { // A metadata path names an annotation the element may simply not carry. @@ -1243,6 +1247,22 @@ func (e *executor) missingFeature(expression queryplan.Expression, property stri } } +// joinMissing is the fan-out of a missing feature: one error whose record carries +// every element's failure, the message naming the first element and counting the rest. +func joinMissing(errs []*Error) error { + if len(errs) == 0 { + return nil + } + head := *errs[0] + failures := append([]Failure{}, head.Outcome.Path.Failures...) + for _, other := range errs[1:] { + failures = append(failures, other.Outcome.Path.Failures...) + head.Also = append(head.Also, other.Target) + } + head.Outcome = &Outcome{Path: &PathError{Failures: failures}} + return &head +} + // features lists, in name order, the features an element has from the model's own // declarations (inherited ones included; library ones are noise) with their declared type. func (e *executor) features(sym *symbols.Symbol) []FeatureRef { diff --git a/internal/doc/queryexec/outcome_test.go b/internal/doc/queryexec/outcome_test.go index 8ab1175159..df0782cfe4 100644 --- a/internal/doc/queryexec/outcome_test.go +++ b/internal/doc/queryexec/outcome_test.go @@ -20,6 +20,14 @@ calc def Dangling :> Query { Named(qualifiedName = "Observatory::Broken::x") } package Kinds { part a; part def a; } calc def TwoKinds :> Query { Named(qualifiedName = "Observatory::Kinds::missing") } calc def RootMiss :> Query { Named(qualifiedName = "Observatry") } +calc def Partial :> Query { Named(qualifiedName = "Duplicates::a") } +calc def Spread :> Query { Named(qualifiedName = "a") } +package Targets { part t1; part t2; } +package Choices { alias Pick for Targets::t1; alias Pick for Targets::t2; } +calc def Aliased :> Query { Named(qualifiedName = "Observatory::Choices::Pick") } +package A { alias Pick for Targets::t1; } +package B { alias Pick for Targets::t1; alias Pick for Targets::t2; } +calc def Shared :> Query { Named(qualifiedName = "Pick") } `) outcome := func(query string) Outcome { t.Helper() @@ -67,9 +75,29 @@ calc def RootMiss :> Query { Named(qualifiedName = "Observatry") } t.Fatalf("Twice: outcome = %+v, want one finding", twice) } finding := twice.Model.Findings[0] - if finding.Constraint != ConstraintAmbiguousName || finding.Element != "Observatory::Duplicates::a" || finding.Observed != "Observatory::Duplicates::a (declared 2 times)" { + if finding.Constraint != ConstraintAmbiguousName || finding.Element != "Observatory::Duplicates" || finding.Observed != "Observatory::Duplicates::a (declared 2 times)" { t.Errorf("Twice: finding = %+v", finding) } + if partial := outcome("Partial"); len(partial.Model.Findings) != 1 || partial.Model.Findings[0].Element != "Observatory::Duplicates" { + t.Errorf("Partial: findings = %+v, want the owner of the resolved candidates, not a slice of the partial name", partial.Model.Findings) + } + spread := outcome("Spread") + if len(spread.Model.Findings) != 2 || spread.Model.Findings[0].Element != "Observatory::Duplicates" || spread.Model.Findings[1].Element != "Observatory::Kinds" || + spread.Model.Findings[0].Observed != "Observatory::Duplicates::a (declared 2 times)" || spread.Model.Findings[1].Observed != "Observatory::Kinds::a (declared 2 times)" { + t.Errorf("Spread: findings = %+v, want one per owning namespace", spread.Model.Findings) + } + if aliased := outcome("Aliased"); len(aliased.Model.Findings) != 1 || aliased.Model.Findings[0].Element != "Observatory::Choices" || + aliased.Model.Findings[0].Observed != "Observatory::Targets::t1, Observatory::Targets::t2" { + t.Errorf("Aliased: findings = %+v, want the namespace declaring both aliases, listing their targets", aliased.Model.Findings) + } + shared := outcome("Shared") + var owners []string + for _, f := range shared.Model.Findings { + owners = append(owners, f.Element+"="+f.Observed) + } + if got := strings.Join(owners, ";"); !strings.Contains(got, "Observatory::A=Observatory::Targets::t1;") || !strings.Contains(got, "Observatory::B=Observatory::Targets::t1, Observatory::Targets::t2") { + t.Errorf("Shared: findings = %s, want B's two aliases both listed", got) + } encoded, _ = json.Marshal(twice) if !strings.Contains(string(encoded), `"outcome":"modelError"`) || !strings.Contains(string(encoded), `"pathFailures":[]`) { t.Errorf("Twice JSON = %s", encoded)