diff --git a/changes/unreleased/query-feature-outcomes.changed.md b/changes/unreleased/query-feature-outcomes.changed.md new file mode 100644 index 0000000000..5e6acd2f2f --- /dev/null +++ b/changes/unreleased/query-feature-outcomes.changed.md @@ -0,0 +1 @@ +- `Project` and `WhereFeature` reading a feature an element row lacks now fail with a `missing-feature` error naming the element and the features it does have (`Target`, `Property`, `Candidates`), instead of leaving the cell empty, and carry the QVTS outcome record (`Feature()` step, the element and its features as frontier); a property no row carries stays `unknown-property`. The new `tolerate = true` argument on both operations keeps the empty cell for sources that mix element kinds (the cookbook's `MassTable` uses it). Metadata paths (`Tag::key`) are unaffected. diff --git a/docs/internals/design/query-outcomes.md b/docs/internals/design/query-outcomes.md index cd9ad1bf50..b43bcb7dee 100644 --- a/docs/internals/design/query-outcomes.md +++ b/docs/internals/design/query-outcomes.md @@ -28,7 +28,8 @@ to `∅` is the ambiguity QVTS exists to remove. | `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 | | `WhereType`, `WhereMetadata` | the selected rows | `unknown-classification` | — | | `RelatedElements` | the neighbours, possibly none | `unknown-relationship` | — | -| `Project`, `WhereFeature` on a feature the element lacks | *today an empty cell* `(none)` | *to do:* a path error per element, with the features reachable on it | — | +| `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 | — | +| `Project`, `WhereFeature` on a metadata path (`Tag::key`) the element lacks | *today an empty cell* | *to do:* decide whether an absent annotation is a path error | — | | `Project` on a `[1]` feature with two values | *today the first value* | — | *to do:* `multiplicity` | `Error` carries the frontier in `Target` (the namespace that resolved), `Expected` (the segment it @@ -38,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` fills 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 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/manual/examples/cookbook.sysml b/docs/manual/examples/cookbook.sysml index fd271af497..7e148a41b6 100644 --- a/docs/manual/examples/cookbook.sysml +++ b/docs/manual/examples/cookbook.sysml @@ -164,7 +164,8 @@ package Cookbook { in root : Element; Project( source = PartsByMass(root = root), - properties = ("name", "mass", "qualifiedName") + properties = ("name", "mass", "qualifiedName"), + tolerate = true // opticalPath and dataPath declare no mass ) } @@ -173,6 +174,7 @@ package Cookbook { Project( source = PartsByMass(root = root), properties = ("name", "mass"), + tolerate = true, columns = ( Column(name = "massLbs", cell = { in row : Subsystem; (row.mass ?? 0.0) * 2.2 }), Column(name = "label", expression = "part: " + Element::name) diff --git a/docs/manual/query-cookbook.md b/docs/manual/query-cookbook.md index a88832567d..8671f338d3 100644 --- a/docs/manual/query-cookbook.md +++ b/docs/manual/query-cookbook.md @@ -101,7 +101,8 @@ calc def MassTable :> Query { in root : Element; // entry parameters, bound by the caller Project( // operations compose inside-out source = PartsByMass(root = root), // ... and queries invoke queries - properties = ("name", "mass", "qualifiedName") + properties = ("name", "mass", "qualifiedName"), + tolerate = true // an element lacking a property gets an empty cell ) } ``` @@ -550,7 +551,8 @@ calc def MassTable :> Query { in root : Element; Project( source = PartsByMass(root = root), - properties = ("name", "mass", "qualifiedName") + properties = ("name", "mass", "qualifiedName"), + tolerate = true // opticalPath and dataPath declare no mass ) } ``` @@ -581,8 +583,22 @@ $ sysml cookbook.sysml -run-query "Cookbook::MassTable root=Cookbook::telescope" qualifiedName = "Cookbook::telescope::dataPath" ``` -A cell for a property the element lacks is empty (`(none)` in the CLI's row -listing, an empty table cell in a document). A feature whose declared value +`opticalPath` and `dataPath` are connections: they declare no `mass`. Reading +a feature of an element that has none so named is by default an error that +names the element and the features it does have — a path error, telling a +query that left the model apart from a feature that is present and empty: + +```console +$ sysml cookbook.sysml -run-query "Cookbook::MassTable root=Cookbook::telescope" # without tolerate = true +error: query Cookbook::MassTable reads feature mass of Cookbook::telescope::opticalPath, which has no such feature +``` + +`tolerate = true` on `Project` (and on `WhereFeature`) accepts the mixed rows +and leaves the cell empty (`(none)` in the CLI's row listing, an empty table +cell in a document). A property no row carries at all is an unknown property +whichever way — that is a misspelling, not a mixed source. A metadata path +(`Properties::key`) is always tolerated: an annotation is optional by nature. +A feature whose declared value names an element of the model — an enumeration literal, a part, a unit — holds that element, printed by name like `general`, whether the value is written on the feature or bound by a redefinition (`attribute :>> beam = Beam::'650mm';`); @@ -785,6 +801,7 @@ calc def MassBudget :> Query { Project( source = PartsByMass(root = root), properties = ("name", "mass"), + tolerate = true, columns = ( Column(name = "massLbs", cell = { in row : Subsystem; (row.mass ?? 0.0) * 2.2 }), Column(name = "label", expression = "part: " + Element::name) diff --git a/docs/project/api-json-differential-baseline.json b/docs/project/api-json-differential-baseline.json index 54a3616203..8c97952981 100644 --- a/docs/project/api-json-differential-baseline.json +++ b/docs/project/api-json-differential-baseline.json @@ -65,7 +65,7 @@ "dir": "examples", "origin": "ours", "files": 52, - "digest": "sha256:cd33a6f84a9998e255f6dc8d1b2cb374dce50e930e015f67f990303c8157558a" + "digest": "sha256:ba2e49098c36879a3a944f37a56cdb39f0dde31f3c75dd8817ce333d6ba226c1" }, { "name": "probes", @@ -79,14 +79,14 @@ "dir": "docs/manual/examples", "origin": "ours", "files": 11, - "digest": "sha256:817bcc10d912772ed88d34c1fb500161d00a1e940adca71b72885b6866e4a5c6" + "digest": "sha256:46abb05dbfef90d395cbccb7f1c278f95a4495d0e16770ec5e20df001bd9be7d" }, { "name": "opensysml-libraries", "dir": "internal/workspace/libs/stdlib/OpenSysML Libraries", "origin": "ours", "files": 16, - "digest": "sha256:82e35230165a3d95d831330f21c3290cca8b9519f5b494d341e0a866a0e373a5" + "digest": "sha256:ac6a1e3b3376227415fa16aec5bf1918312a69cad0d8d63b6adcda799a44c9b3" } ], "recorded": "2026-10-27" @@ -111,26 +111,26 @@ "oursUnconvertible": 0, "oursColliding": 28, "pilotFailed": 59, - "pilotLibraryElements": 35490, + "pilotLibraryElements": 35864, "elements": { - "matched": 92455, + "matched": 92491, "oursOnly": 18479, - "pilotOnly": 37766, + "pilotOnly": 37775, "pilotOnlyImplied": 0, "typeMismatch": 1729 }, "properties": { - "presentEqual": 1121587, - "presentDifferent": 27497, - "oursMissing": 1074012, - "pilotMissing": 68257, - "absentBoth": 1973372, + "presentEqual": 1122005, + "presentDifferent": 27547, + "oursMissing": 1074516, + "pilotMissing": 68275, + "absentBoth": 1974074, "indeterminate": 26286, "pilotNondeterministic": 514 }, "rows": 11728, - "unkeyedReferences": 111949, - "extensionProperties": 159941, + "unkeyedReferences": 111976, + "extensionProperties": 159959, "duplicateElements": { "ours": 527, "pilot": 82631 @@ -371,20 +371,20 @@ "oursRefused": 0, "pilotFailed": 0, "oursColliding": 3, - "pilotLibraryElements": 13888, + "pilotLibraryElements": 14020, "elements": { - "matched": 26974, + "matched": 27002, "oursOnly": 6046, - "pilotOnly": 15072, + "pilotOnly": 15079, "pilotOnlyImplied": 0, "typeMismatch": 186 }, "properties": { - "presentEqual": 333478, - "presentDifferent": 8320, - "oursMissing": 322802, - "pilotMissing": 20056, - "absentBoth": 599839, + "presentEqual": 333821, + "presentDifferent": 8341, + "oursMissing": 323194, + "pilotMissing": 20070, + "absentBoth": 600385, "indeterminate": 9869, "pilotNondeterministic": 162 }, @@ -443,20 +443,20 @@ "files": 11, "oursRefused": 0, "pilotFailed": 0, - "pilotLibraryElements": 16846, + "pilotLibraryElements": 17088, "elements": { - "matched": 7191, + "matched": 7199, "oursOnly": 920, - "pilotOnly": 2108, + "pilotOnly": 2110, "pilotOnlyImplied": 0, "typeMismatch": 0 }, "properties": { - "presentEqual": 88704, - "presentDifferent": 2005, - "oursMissing": 89489, - "pilotMissing": 4812, - "absentBoth": 150077, + "presentEqual": 88779, + "presentDifferent": 2034, + "oursMissing": 89601, + "pilotMissing": 4816, + "absentBoth": 150233, "indeterminate": 2170, "pilotNondeterministic": 36 }, @@ -36442,7 +36442,7 @@ 0, 0, 0, - 10180, + 10189, 0, 0 ], @@ -36451,7 +36451,7 @@ 43, 130, 6, - 9782, + 9791, 24, 0 ], @@ -36487,7 +36487,7 @@ 0, 0, 0, - 9255, + 9264, 0, 0 ], @@ -36496,7 +36496,7 @@ 0, 0, 0, - 10180, + 10189, 0, 0 ], @@ -36514,7 +36514,7 @@ 0, 4, 0, - 10176, + 10185, 0, 0 ], @@ -36523,12 +36523,12 @@ 0, 622, 0, - 9558, + 9567, 0, 0 ], "Feature.direction": [ - 8602, + 8611, 192, 4, 101, @@ -36550,7 +36550,7 @@ 0, 21, 0, - 10159, + 10168, 0, 0 ], @@ -36559,7 +36559,7 @@ 0, 55, 0, - 10125, + 10134, 0, 0 ], @@ -36575,7 +36575,7 @@ "Feature.feature": [ 0, 0, - 10174, + 10183, 0, 6, 0, @@ -36584,7 +36584,7 @@ "Feature.featureMembership": [ 0, 0, - 10174, + 10183, 0, 6, 0, @@ -36593,7 +36593,7 @@ "Feature.featureTarget": [ 0, 0, - 10180, + 10189, 0, 0, 0, @@ -36602,7 +36602,7 @@ "Feature.featuringType": [ 67, 0, - 9878, + 9887, 0, 234, 1, @@ -36613,14 +36613,14 @@ 0, 15, 0, - 10165, + 10174, 0, 0 ], "Feature.inheritedFeature": [ 0, 0, - 10180, + 10189, 0, 0, 0, @@ -36629,7 +36629,7 @@ "Feature.inheritedMembership": [ 0, 0, - 10180, + 10189, 0, 0, 0, @@ -36640,7 +36640,7 @@ 0, 437, 0, - 9743, + 9752, 0, 0 ], @@ -36649,7 +36649,7 @@ 0, 3, 0, - 10177, + 10186, 0, 0 ], @@ -36672,7 +36672,7 @@ 0 ], "Feature.isAbstract": [ - 10180, + 10189, 0, 0, 0, @@ -36681,7 +36681,7 @@ 0 ], "Feature.isComposite": [ - 10146, + 10155, 34, 0, 0, @@ -36692,14 +36692,14 @@ "Feature.isConjugated": [ 0, 0, - 10180, + 10189, 0, 0, 0, 0 ], "Feature.isConstant": [ - 10180, + 10189, 0, 0, 0, @@ -36717,7 +36717,7 @@ 0 ], "Feature.isDerived": [ - 10180, + 10189, 0, 0, 0, @@ -36726,7 +36726,7 @@ 0 ], "Feature.isEnd": [ - 10180, + 10189, 0, 0, 0, @@ -36735,7 +36735,7 @@ 0 ], "Feature.isImpliedIncluded": [ - 10180, + 10189, 0, 0, 0, @@ -36755,14 +36755,14 @@ "Feature.isLibraryElement": [ 0, 0, - 10180, + 10189, 0, 0, 0, 0 ], "Feature.isOrdered": [ - 10180, + 10189, 0, 0, 0, @@ -36771,7 +36771,7 @@ 0 ], "Feature.isPortion": [ - 10180, + 10189, 0, 0, 0, @@ -36789,7 +36789,7 @@ 0 ], "Feature.isSufficient": [ - 10180, + 10189, 0, 0, 0, @@ -36798,7 +36798,7 @@ 0 ], "Feature.isUnique": [ - 10180, + 10189, 0, 0, 0, @@ -36807,7 +36807,7 @@ 0 ], "Feature.isVariable": [ - 9079, + 9088, 0, 75, 0, @@ -36836,7 +36836,7 @@ "Feature.member": [ 0, 0, - 10180, + 10189, 0, 0, 0, @@ -36845,7 +36845,7 @@ "Feature.membership": [ 0, 0, - 10180, + 10189, 0, 0, 0, @@ -36863,7 +36863,7 @@ "Feature.name": [ 190, 0, - 9570, + 9579, 0, 0, 0, @@ -36874,7 +36874,7 @@ 0, 453, 0, - 9727, + 9736, 0, 0 ], @@ -36883,7 +36883,7 @@ 0, 0, 0, - 10180, + 10189, 0, 0 ], @@ -36910,7 +36910,7 @@ 0, 4, 0, - 10176, + 10185, 0, 0 ], @@ -36919,14 +36919,14 @@ 0, 3, 0, - 10177, + 10186, 0, 0 ], "Feature.ownedElement": [ 0, 0, - 4917, + 4926, 0, 5263, 0, @@ -36937,7 +36937,7 @@ 0, 1, 0, - 10179, + 10188, 0, 0 ], @@ -36946,7 +36946,7 @@ 4, 568, 0, - 9481, + 9490, 6, 0 ], @@ -36955,7 +36955,7 @@ 0, 121, 6, - 9782, + 9791, 0, 0 ], @@ -36964,7 +36964,7 @@ 0, 7, 0, - 10173, + 10182, 0, 0 ], @@ -36973,7 +36973,7 @@ 1, 568, 0, - 9481, + 9490, 0, 0 ], @@ -36982,7 +36982,7 @@ 0, 0, 0, - 10165, + 10174, 0, 0 ], @@ -36991,21 +36991,21 @@ 0, 3, 0, - 10177, + 10186, 0, 0 ], "Feature.ownedMember": [ 119, 104, - 4671, + 4680, 0, 5278, 8, 0 ], "Feature.ownedMembership": [ - 4296, + 4305, 569, 37, 94, @@ -37016,7 +37016,7 @@ "Feature.ownedRedefinition": [ 280, 0, - 653, + 662, 0, 9247, 0, @@ -37033,7 +37033,7 @@ ], "Feature.ownedRelationship": [ 4163, - 1421, + 1430, 137, 90, 4369, @@ -37043,7 +37043,7 @@ "Feature.ownedSpecialization": [ 873, 20, - 682, + 691, 34, 8571, 0, @@ -37052,7 +37052,7 @@ "Feature.ownedSubsetting": [ 371, 8, - 665, + 674, 6, 9130, 0, @@ -37063,7 +37063,7 @@ 0, 70, 0, - 10110, + 10119, 0, 0 ], @@ -37072,7 +37072,7 @@ 0, 29, 28, - 9463, + 9472, 0, 0 ], @@ -37081,12 +37081,12 @@ 0, 7, 0, - 10173, + 10182, 0, 0 ], "Feature.owner": [ - 10180, + 10189, 0, 0, 0, @@ -37097,14 +37097,14 @@ "Feature.owningFeatureMembership": [ 0, 0, - 9529, + 9538, 0, 0, 0, 0 ], "Feature.owningMembership": [ - 9560, + 9569, 356, 6, 0, @@ -37115,14 +37115,14 @@ "Feature.owningNamespace": [ 1133, 0, - 8789, + 8798, 0, 0, 0, 0 ], "Feature.owningRelationship": [ - 9817, + 9826, 363, 0, 0, @@ -37133,7 +37133,7 @@ "Feature.owningType": [ 0, 0, - 9529, + 9538, 0, 0, 0, @@ -37189,14 +37189,14 @@ 0, 0, 0, - 10180, + 10189, 0, 0 ], "Feature.type": [ 346, 14, - 9647, + 9656, 0, 0, 173, @@ -37207,7 +37207,7 @@ 0, 7, 0, - 10173, + 10182, 0, 0 ], @@ -39466,7 +39466,7 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], @@ -39475,7 +39475,7 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], @@ -39484,7 +39484,7 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], @@ -39493,12 +39493,12 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], "FeatureValue.featureWithValue": [ - 7177, + 7186, 0, 0, 0, @@ -39507,7 +39507,7 @@ 0 ], "FeatureValue.isDefault": [ - 7177, + 7186, 0, 0, 0, @@ -39516,7 +39516,7 @@ 0 ], "FeatureValue.isImplied": [ - 7177, + 7186, 0, 0, 0, @@ -39525,7 +39525,7 @@ 0 ], "FeatureValue.isImpliedIncluded": [ - 7177, + 7186, 0, 0, 0, @@ -39534,7 +39534,7 @@ 0 ], "FeatureValue.isInitial": [ - 7177, + 7186, 0, 0, 0, @@ -39545,14 +39545,14 @@ "FeatureValue.isLibraryElement": [ 0, 0, - 7177, + 7186, 0, 0, 0, 0 ], "FeatureValue.memberElement": [ - 6870, + 6879, 307, 0, 0, @@ -39563,14 +39563,14 @@ "FeatureValue.memberElementId": [ 0, 0, - 7177, + 7186, 0, 0, 0, 0 ], "FeatureValue.membershipOwningNamespace": [ - 7177, + 7186, 0, 0, 0, @@ -39583,7 +39583,7 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], @@ -39592,12 +39592,12 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], "FeatureValue.ownedMemberElement": [ - 6870, + 6879, 307, 0, 0, @@ -39608,14 +39608,14 @@ "FeatureValue.ownedMemberElementId": [ 0, 0, - 7177, + 7186, 0, 0, 0, 0 ], "FeatureValue.ownedRelatedElement": [ - 6870, + 6879, 307, 0, 0, @@ -39628,7 +39628,7 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], @@ -39636,13 +39636,13 @@ 0, 0, 0, - 7177, + 7186, 0, 0, 0 ], "FeatureValue.owningRelatedElement": [ - 7177, + 7186, 0, 0, 0, @@ -39655,14 +39655,14 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], "FeatureValue.relatedElement": [ 0, 0, - 7177, + 7186, 0, 0, 0, @@ -39671,7 +39671,7 @@ "FeatureValue.source": [ 0, 0, - 7177, + 7186, 0, 0, 0, @@ -39680,7 +39680,7 @@ "FeatureValue.target": [ 0, 0, - 7177, + 7186, 0, 0, 0, @@ -39691,12 +39691,12 @@ 0, 0, 0, - 7177, + 7186, 0, 0 ], "FeatureValue.value": [ - 6870, + 6879, 307, 0, 0, @@ -39707,7 +39707,7 @@ "FeatureValue.visibility": [ 0, 0, - 7177, + 7186, 0, 0, 0, @@ -51729,8 +51729,8 @@ 0 ], "InvocationExpression.argument": [ - 446, - 4, + 439, + 11, 0, 14, 16, @@ -51891,8 +51891,8 @@ 0 ], "InvocationExpression.input": [ - 385, - 79, + 368, + 96, 1, 0, 15, @@ -52314,8 +52314,8 @@ 0 ], "InvocationExpression.parameter": [ - 399, - 81, + 382, + 98, 0, 0, 0, @@ -55108,14 +55108,14 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.behavior": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55126,7 +55126,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55135,7 +55135,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55144,7 +55144,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55153,14 +55153,14 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.directedFeature": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55171,7 +55171,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55180,7 +55180,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55189,14 +55189,14 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.feature": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55205,7 +55205,7 @@ "LiteralBoolean.featureMembership": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55214,7 +55214,7 @@ "LiteralBoolean.featureTarget": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55223,7 +55223,7 @@ "LiteralBoolean.featuringType": [ 0, 0, - 92, + 101, 0, 294, 0, @@ -55232,7 +55232,7 @@ "LiteralBoolean.function": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55243,14 +55243,14 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.inheritedFeature": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55259,7 +55259,7 @@ "LiteralBoolean.inheritedMembership": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55270,7 +55270,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55279,12 +55279,12 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.isAbstract": [ - 386, + 395, 0, 0, 0, @@ -55293,7 +55293,7 @@ 0 ], "LiteralBoolean.isComposite": [ - 386, + 395, 0, 0, 0, @@ -55304,14 +55304,14 @@ "LiteralBoolean.isConjugated": [ 0, 0, - 386, + 395, 0, 0, 0, 0 ], "LiteralBoolean.isConstant": [ - 386, + 395, 0, 0, 0, @@ -55320,7 +55320,7 @@ 0 ], "LiteralBoolean.isDerived": [ - 386, + 395, 0, 0, 0, @@ -55329,7 +55329,7 @@ 0 ], "LiteralBoolean.isEnd": [ - 386, + 395, 0, 0, 0, @@ -55338,7 +55338,7 @@ 0 ], "LiteralBoolean.isImpliedIncluded": [ - 386, + 395, 0, 0, 0, @@ -55349,7 +55349,7 @@ "LiteralBoolean.isLibraryElement": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55358,14 +55358,14 @@ "LiteralBoolean.isModelLevelEvaluable": [ 0, 0, - 386, + 395, 0, 0, 0, 0 ], "LiteralBoolean.isOrdered": [ - 386, + 395, 0, 0, 0, @@ -55374,7 +55374,7 @@ 0 ], "LiteralBoolean.isPortion": [ - 386, + 395, 0, 0, 0, @@ -55383,7 +55383,7 @@ 0 ], "LiteralBoolean.isSufficient": [ - 386, + 395, 0, 0, 0, @@ -55392,7 +55392,7 @@ 0 ], "LiteralBoolean.isUnique": [ - 386, + 395, 0, 0, 0, @@ -55401,7 +55401,7 @@ 0 ], "LiteralBoolean.isVariable": [ - 386, + 395, 0, 0, 0, @@ -55412,7 +55412,7 @@ "LiteralBoolean.member": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55421,7 +55421,7 @@ "LiteralBoolean.membership": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55430,7 +55430,7 @@ "LiteralBoolean.output": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55441,7 +55441,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55450,7 +55450,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55459,7 +55459,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55468,7 +55468,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55477,7 +55477,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55486,7 +55486,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55495,7 +55495,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55504,7 +55504,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55513,7 +55513,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55522,7 +55522,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55531,7 +55531,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55540,7 +55540,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55549,7 +55549,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55558,7 +55558,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55567,7 +55567,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55576,7 +55576,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55585,7 +55585,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55594,7 +55594,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55603,7 +55603,7 @@ 0, 0, 0, - 386, + 395, 0, 0 ], @@ -55612,12 +55612,12 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.owner": [ - 386, + 395, 0, 0, 0, @@ -55626,7 +55626,7 @@ 0 ], "LiteralBoolean.owningMembership": [ - 386, + 395, 0, 0, 0, @@ -55637,14 +55637,14 @@ "LiteralBoolean.owningNamespace": [ 0, 0, - 386, + 395, 0, 0, 0, 0 ], "LiteralBoolean.owningRelationship": [ - 386, + 395, 0, 0, 0, @@ -55655,7 +55655,7 @@ "LiteralBoolean.parameter": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55664,7 +55664,7 @@ "LiteralBoolean.result": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55675,14 +55675,14 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.type": [ 0, 0, - 386, + 395, 0, 0, 0, @@ -55693,12 +55693,12 @@ 0, 0, 0, - 386, + 395, 0, 0 ], "LiteralBoolean.value": [ - 386, + 395, 0, 0, 0, @@ -68644,7 +68644,7 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], @@ -68653,7 +68653,7 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], @@ -68662,7 +68662,7 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], @@ -68671,12 +68671,12 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], "ParameterMembership.isImplied": [ - 4435, + 4444, 0, 0, 0, @@ -68685,7 +68685,7 @@ 0 ], "ParameterMembership.isImpliedIncluded": [ - 4435, + 4444, 0, 0, 0, @@ -68696,14 +68696,14 @@ "ParameterMembership.isLibraryElement": [ 0, 0, - 4435, + 4444, 0, 0, 0, 0 ], "ParameterMembership.memberElement": [ - 4222, + 4231, 213, 0, 0, @@ -68714,7 +68714,7 @@ "ParameterMembership.memberElementId": [ 0, 0, - 4435, + 4444, 0, 0, 0, @@ -68723,14 +68723,14 @@ "ParameterMembership.memberName": [ 0, 0, - 4419, + 4428, 0, 0, 0, 0 ], "ParameterMembership.membershipOwningNamespace": [ - 4435, + 4444, 0, 0, 0, @@ -68743,7 +68743,7 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], @@ -68752,12 +68752,12 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], "ParameterMembership.ownedMemberElement": [ - 4222, + 4231, 213, 0, 0, @@ -68768,14 +68768,14 @@ "ParameterMembership.ownedMemberElementId": [ 0, 0, - 4435, + 4444, 0, 0, 0, 0 ], "ParameterMembership.ownedMemberFeature": [ - 4218, + 4227, 213, 4, 0, @@ -68786,14 +68786,14 @@ "ParameterMembership.ownedMemberName": [ 0, 0, - 4419, + 4428, 0, 0, 0, 0 ], "ParameterMembership.ownedMemberParameter": [ - 4222, + 4231, 213, 0, 0, @@ -68802,7 +68802,7 @@ 0 ], "ParameterMembership.ownedRelatedElement": [ - 4222, + 4231, 213, 0, 0, @@ -68815,7 +68815,7 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], @@ -68823,13 +68823,13 @@ 0, 0, 0, - 4435, + 4444, 0, 0, 0 ], "ParameterMembership.owningRelatedElement": [ - 4435, + 4444, 0, 0, 0, @@ -68842,14 +68842,14 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], "ParameterMembership.owningType": [ 70, 0, - 4365, + 4374, 0, 0, 0, @@ -68858,7 +68858,7 @@ "ParameterMembership.relatedElement": [ 0, 0, - 4435, + 4444, 0, 0, 0, @@ -68867,7 +68867,7 @@ "ParameterMembership.source": [ 0, 0, - 4435, + 4444, 0, 0, 0, @@ -68876,7 +68876,7 @@ "ParameterMembership.target": [ 0, 0, - 4435, + 4444, 0, 0, 0, @@ -68887,14 +68887,14 @@ 0, 0, 0, - 4435, + 4444, 0, 0 ], "ParameterMembership.visibility": [ 0, 0, - 4435, + 4444, 0, 0, 0, diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index f58d4efc99..8f3fbc2184 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -69,7 +69,7 @@ "dir": "examples", "origin": "ours", "files": 52, - "digest": "sha256:cd33a6f84a9998e255f6dc8d1b2cb374dce50e930e015f67f990303c8157558a" + "digest": "sha256:ba2e49098c36879a3a944f37a56cdb39f0dde31f3c75dd8817ce333d6ba226c1" }, { "name": "probes", @@ -83,10 +83,10 @@ "dir": "internal/workspace/libs/stdlib/OpenSysML Libraries", "origin": "ours", "files": 16, - "digest": "sha256:82e35230165a3d95d831330f21c3290cca8b9519f5b494d341e0a866a0e373a5" + "digest": "sha256:ac6a1e3b3376227415fa16aec5bf1918312a69cad0d8d63b6adcda799a44c9b3" } ], - "recorded": "2026-10-26" + "recorded": "2026-10-27" }, "totals": { "files": 920, @@ -17898,7 +17898,7 @@ "openSysMLOnly": [], "pilotOnly": [ { - "line": 542, + "line": 543, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18028,12 +18028,6 @@ "severityMismatch": [], "openSysMLOnly": [], "pilotOnly": [ - { - "line": 187, - "severity": "warning", - "category": "kind-mismatch", - "count": 1 - }, { "line": 193, "severity": "warning", @@ -18041,13 +18035,13 @@ "count": 1 }, { - "line": 208, + "line": 199, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 222, + "line": 214, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18059,19 +18053,19 @@ "count": 1 }, { - "line": 239, + "line": 234, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 246, + "line": 245, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 256, + "line": 252, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18083,13 +18077,13 @@ "count": 1 }, { - "line": 267, + "line": 268, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 277, + "line": 273, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18101,7 +18095,7 @@ "count": 1 }, { - "line": 297, + "line": 289, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18113,19 +18107,19 @@ "count": 1 }, { - "line": 314, + "line": 309, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 324, + "line": 320, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 334, + "line": 330, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18143,7 +18137,7 @@ "count": 1 }, { - "line": 360, + "line": 352, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18155,13 +18149,13 @@ "count": 1 }, { - "line": 381, + "line": 372, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 395, + "line": 387, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18179,13 +18173,13 @@ "count": 1 }, { - "line": 414, + "line": 413, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 452, + "line": 420, "severity": "warning", "category": "kind-mismatch", "count": 1 @@ -18197,25 +18191,31 @@ "count": 1 }, { - "line": 468, + "line": 464, + "severity": "warning", + "category": "kind-mismatch", + "count": 1 + }, + { + "line": 474, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 481, + "line": 487, "severity": "warning", "category": "kind-mismatch", "count": 1 }, { - "line": 481, + "line": 487, "severity": "warning", "category": "unmapped", "count": 18 }, { - "line": 487, + "line": 493, "severity": "warning", "category": "kind-mismatch", "count": 1 diff --git a/examples/analysis-results-demo/README.md b/examples/analysis-results-demo/README.md index 0eca74718d..1645af00a5 100644 --- a/examples/analysis-results-demo/README.md +++ b/examples/analysis-results-demo/README.md @@ -76,7 +76,9 @@ part scoutRun : FuelBudgetRun { Two limitations shape the record. The annotation's attribute values are not projectable — `Project(properties = ("runAt"))` reports `unknown property` — so `command` is also carried as a plain attribute on -`DemoRun` for the provenance table to show; the `@AnalysisRecords::RecordedRun` +`DemoRun` for the provenance table to show (a run `-record-run` writes has no +such attribute, so the table's `Project` says `tolerate = true` to leave its +cell empty rather than fail); the `@AnalysisRecords::RecordedRun` metadata still answers `WhereMetadata` filters and keeps the provenance machine-readable. And the library's untyped `'subject'` ref cannot drive the `liveFuelLeft` formula, so `FuelBudgetRun` keeps a typed `lander` ref and each diff --git a/examples/analysis-results-demo/lander-results.sysml b/examples/analysis-results-demo/lander-results.sysml index 74c4c01054..f66d268269 100644 --- a/examples/analysis-results-demo/lander-results.sysml +++ b/examples/analysis-results-demo/lander-results.sysml @@ -448,7 +448,8 @@ package Reporting { calc def Provenance :> Query { Project( source = WhereMetadata(source = Descendants(source = Named(qualifiedName = "Records"), maxDepth = 1), 'metadata' = "AnalysisRecords::RecordedRun"), - properties = ("name", "kind", "caseName", "command")) + properties = ("name", "kind", "caseName", "command"), + tolerate = true) // a run -record-run writes carries command in its metadata only } // The trade-study record. diff --git a/examples/self-model/document.sysml b/examples/self-model/document.sysml index 7a737d72af..a6cda88de4 100644 --- a/examples/self-model/document.sysml +++ b/examples/self-model/document.sysml @@ -33,6 +33,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" @@ -63,6 +64,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" @@ -81,6 +83,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" @@ -111,6 +114,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" @@ -129,6 +133,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" @@ -146,6 +151,7 @@ package OpenSysMLDocument { in root : Element; Project( source = WhereFeature( + tolerate = true, source = WhereType( source = OwnedElements(source = DefinitionOf(root = root)), type = "PartUsage" diff --git a/internal/doc/queryexec/derived_test.go b/internal/doc/queryexec/derived_test.go index d4d035b2ba..249da27cd5 100644 --- a/internal/doc/queryexec/derived_test.go +++ b/internal/doc/queryexec/derived_test.go @@ -64,7 +64,8 @@ calc def Vehicles :> Query { in root : Element[1]; Project( source = WhereType(source = Descendants(source = root, maxDepth = 1), type = "PartUsage"), - properties = ("name", "liftoffMass", "engineCount", "topMass") + properties = ("name", "liftoffMass", "engineCount", "topMass"), + tolerate = true ) } ` diff --git a/internal/doc/queryexec/errors.go b/internal/doc/queryexec/errors.go index adfce6e1b7..57c12a8921 100644 --- a/internal/doc/queryexec/errors.go +++ b/internal/doc/queryexec/errors.go @@ -29,8 +29,11 @@ const ( ErrorUnknownElement ErrorKind = "unknown-element" // ErrorAmbiguousElement: Named was given a name several elements carry — a // finding about the model, where ErrorUnknownElement is one about the query. - ErrorAmbiguousElement ErrorKind = "ambiguous-element" - ErrorUnknownRelationship ErrorKind = "unknown-relationship" + ErrorAmbiguousElement ErrorKind = "ambiguous-element" + ErrorUnknownRelationship ErrorKind = "unknown-relationship" + // ErrorMissingFeature: Project or WhereFeature read a feature an element row + // lacks — a path error about that element; ErrorUnknownProperty: no row had it. + ErrorMissingFeature ErrorKind = "missing-feature" ErrorUnevaluableFeature ErrorKind = "unevaluable-feature" ErrorUnknownInvocation ErrorKind = "unknown-invocation" ErrorInvocationCycle ErrorKind = "invocation-cycle" @@ -143,6 +146,32 @@ func (e *Error) unknownElementMessage() string { return suggest.With(msg, e.Expected, e.Candidates) } +// missingFeatureMessage names the element lacking the feature and the features +// it does have, so the reader sees where the path left the model. +func (e *Error) missingFeatureMessage() string { + msg := fmt.Sprintf("query %s reads feature %s of %s, which has no such feature", e.Query, e.Property, e.Target) + 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 + } + msg += ": its features are " + strings.Join(e.Candidates, ", ") + if len(e.Candidates) <= 3 { + return msg + } + return suggest.With(msg, e.missingSegment(), e.Candidates) +} + +// 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 { + return e.Outcome.Path.Failures[0].Step.Argument + } + return e.Property +} + // column names the projected column an error arose in, when it did in one. func (e *Error) column() string { if e.Property == "" { @@ -188,6 +217,8 @@ func (e *Error) Error() string { return fmt.Sprintf("query %s references unknown property %s", e.Query, e.Property) case ErrorUnknownClassification: return fmt.Sprintf("query %s references unknown classification %s", e.Query, e.Actual) + case ErrorMissingFeature: + return e.missingFeatureMessage() case ErrorUnknownElement: return e.unknownElementMessage() case ErrorAmbiguousElement: diff --git a/internal/doc/queryexec/memberpath.go b/internal/doc/queryexec/memberpath.go index a3f8252207..e73f6f2500 100644 --- a/internal/doc/queryexec/memberpath.go +++ b/internal/doc/queryexec/memberpath.go @@ -31,3 +31,17 @@ func (e *executor) memberPathValues(sym *symbols.Symbol, segments []string) ([]V member, _ := e.context.Model.LookupMember(current, segments[len(segments)-1]) return values, true, member, nil } + +// memberPathStop walks a member path as memberPathValues does and reports where +// it stopped: the last member reached and the segment that names no member there. +func (e *executor) memberPathStop(sym *symbols.Symbol, segments []string) (*symbols.Symbol, string) { + current := sym + for _, segment := range segments { + member, ok := e.context.Model.LookupMember(current, segment) + if !ok || member == nil { + return current, segment + } + current = member + } + return current, segments[len(segments)-1] +} diff --git a/internal/doc/queryexec/memberpath_test.go b/internal/doc/queryexec/memberpath_test.go index e9e57fc2ea..908f2bddbe 100644 --- a/internal/doc/queryexec/memberpath_test.go +++ b/internal/doc/queryexec/memberpath_test.go @@ -251,7 +251,8 @@ calc def Runs :> Query { in root : Element; Project( source = Descendants(source = root, maxDepth = 1), - properties = ("name", "stat.runs") + properties = ("name", "stat.runs"), + tolerate = true ) }`) result := memberPathRows(t, fixture, "Runs") diff --git a/internal/doc/queryexec/missing_feature_test.go b/internal/doc/queryexec/missing_feature_test.go new file mode 100644 index 0000000000..b6688cd61d --- /dev/null +++ b/internal/doc/queryexec/missing_feature_test.go @@ -0,0 +1,118 @@ +package queryexec + +import ( + "errors" + "strings" + "testing" +) + +// TestExecuteMissingFeatureIsAPathErrorUnlessTolerated: reading a feature an +// element lacks names that element and the features it has; tolerate = true +// keeps the empty cell, and only a property no row carries is then unknown. +func TestExecuteMissingFeatureIsAPathErrorUnlessTolerated(t *testing.T) { + fixture := loadExecutionFixture(t, ` +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; } +} +calc def Masses :> Query { + in source : Element; + Project(source = OwnedElements(source = source), properties = ("mass")) +} +calc def ToleratedMasses :> Query { + in source : Element; + Project(source = OwnedElements(source = source), properties = ("mass"), tolerate = true) +} +calc def ToleratedAbsent :> Query { + in source : Element; + Project(source = OwnedElements(source = source), properties = ("absent"), tolerate = true) +} +calc def HeavyOnes :> Query { + in source : Element; + WhereFeature(source = OwnedElements(source = source), 'feature' = "mass", operator = ">", value = "0.5") +} +calc def ToleratedHeavyOnes :> Query { + in source : Element; + WhereFeature(source = OwnedElements(source = source), 'feature' = "mass", operator = ">", value = "0.5", tolerate = true) +} +`) + bindings := Bindings{"source": {ElementValue(fixture.symbol(t, "root"))}} + var executionError *Error + for _, query := range []string{"Masses", "HeavyOnes"} { + _, err := fixture.execute(t, query, bindings, Options{}) + if !errors.As(err, &executionError) || executionError.Kind != ErrorMissingFeature { + t.Fatalf("%s: error = %v, want missing-feature", query, err) + } + if executionError.Target != "Observatory::root::b" || executionError.Property != "mass" { + t.Fatalf("%s: names %s.%s, want Observatory::root::b.mass", query, executionError.Target, executionError.Property) + } + 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") { + 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) + } + 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" { + t.Fatalf("%s: failure = %+v", query, failure) + } + if len(failure.Frontier.Features) != 2 || failure.Frontier.Features[0].Name != "mas" || failure.Frontier.Features[1].Name != "volume" { + t.Fatalf("%s: frontier features = %+v", query, failure.Frontier.Features) + } + } + tolerated, err := fixture.execute(t, "ToleratedMasses", bindings, Options{}) + 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()) + } + kept, err := fixture.execute(t, "ToleratedHeavyOnes", bindings, Options{}) + if err != nil || len(kept.Rows()) != 1 { + t.Fatalf("ToleratedHeavyOnes: rows = %v, err = %v, want a alone", kept.Rows(), err) + } + _, err = fixture.execute(t, "ToleratedAbsent", bindings, Options{}) + if !errors.As(err, &executionError) || executionError.Kind != ErrorUnknownProperty { + t.Fatalf("ToleratedAbsent: error = %v, want unknown-property", err) + } +} + +// TestExecuteMissingFeatureOnAMemberPathStopsAtTheMemberReached: a dotted +// property whose segment names no member reports the member the path reached, +// with that member's features as the frontier, not the row's. +func TestExecuteMissingFeatureOnAMemberPathStopsAtTheMemberReached(t *testing.T) { + fixture := loadExecutionFixture(t, ` +part root { + part x { part stat { attribute count = 1; attribute runs = 2; } } + part y { part stat { attribute count = 3; attribute run = 4; } } +} +calc def Runs :> Query { + in source : Element; + Project(source = OwnedElements(source = source), properties = ("stat.runs")) +} +`) + bindings := Bindings{"source": {ElementValue(fixture.symbol(t, "root"))}} + var executionError *Error + _, err := fixture.execute(t, "Runs", bindings, Options{}) + if !errors.As(err, &executionError) || executionError.Kind != ErrorMissingFeature { + t.Fatalf("error = %v, want missing-feature", err) + } + if executionError.Target != "Observatory::root::y" || executionError.Actual != "Observatory::root::y::stat" { + t.Fatalf("names %s stopping at %s, want root::y stopping at root::y::stat", executionError.Target, executionError.Actual) + } + if msg := executionError.Error(); !strings.HasSuffix(msg, "reads feature stat.runs of Observatory::root::y, but the path stops at Observatory::root::y::stat: its features are count, run") { + t.Fatalf("message = %q", msg) + } + failure := executionError.Outcome.Path.Failures[0] + if failure.Step.Label() != "Feature(runs)" || failure.Frontier.At != "Observatory::root::y::stat" || failure.Frontier.Nearest != "run" { + t.Fatalf("failure = %+v", failure) + } + if len(failure.Frontier.Features) != 2 || failure.Frontier.Features[0].Name != "count" || failure.Frontier.Features[1].Name != "run" { + t.Fatalf("frontier features = %+v", failure.Frontier.Features) + } +} diff --git a/internal/doc/queryexec/objects_test.go b/internal/doc/queryexec/objects_test.go index 52fa8f38ea..d1cf558de4 100644 --- a/internal/doc/queryexec/objects_test.go +++ b/internal/doc/queryexec/objects_test.go @@ -33,7 +33,8 @@ calc def Held :> Query { in root : Element; Project( source = Descendants(source = root, maxDepth = 3), - properties = ("qualifiedName", "name", "pressure") + properties = ("qualifiedName", "name", "pressure"), + tolerate = true ) } calc def Direct :> Query { diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 97a15b0750..801d2cbeed 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -4,6 +4,7 @@ import ( "errors" "math" "regexp" + "slices" "sort" "strconv" "strings" @@ -11,6 +12,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/ir/queryplan" "github.com/Open-MBEE/OpenSysML/internal/semantic/query" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/suggest" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -396,17 +398,25 @@ func (e *executor) evaluateWhereFeature(expression queryplan.Expression) (sequen } return sequence{}, e.operatorError(expression, operator) } + tolerate, err := e.tolerateArgument(expression) + if err != nil { + return sequence{}, err + } if len(source.values) == 0 { return source, nil } result := filtered(source) known := false + 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]) + } known = known || present for _, actual := range values { match, compareErr := e.compareValue(actual, operator, expected) @@ -425,6 +435,9 @@ func (e *executor) evaluateWhereFeature(expression queryplan.Expression) (sequen if !known { return sequence{}, e.unknownProperty(expression, property) } + if missing != nil { + return sequence{}, missing + } return result, nil } @@ -609,6 +622,10 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e if total == 0 { return sequence{}, e.invalidArgument(expression, "properties", "empty") } + tolerate, err := e.tolerateArgument(expression) + if err != nil { + return sequence{}, err + } seen := make(map[string]bool, total) for _, property := range properties { seen[property] = true @@ -633,6 +650,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)) for row, value := range source.values { result.cells[row] = make([]Cell, total) for column, property := range properties { @@ -640,6 +658,9 @@ 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) + } known[column] = known[column] || present result.cells[row][column] = Cell{ values: values, @@ -663,6 +684,13 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e return sequence{}, e.unknownProperty(expression, property) } } + // 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 { + return sequence{}, err + } + } if property, missing := tracker.missing(); missing { return sequence{}, e.unknownProperty(expression, property) } @@ -1169,6 +1197,74 @@ func (e *executor) operatorError(expression queryplan.Expression, operator strin } } +// tolerateArgument reads Project's and WhereFeature's tolerate: by default an +// element lacking the feature is a path error; true keeps the cell empty. +func (e *executor) tolerateArgument(expression queryplan.Expression) (bool, error) { + if !hasArgument(expression, "tolerate") { + return false, nil + } + return e.booleanArgument(expression, "tolerate") +} + +// 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 { + 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. + return nil + } + reached, segment, nested := sym, property, "" + if segments, ok := parseMemberPath(property); ok { + reached, segment = e.memberPathStop(sym, segments) + if reached != sym { + nested = symbols.FQNOf(reached) + } + } + features := e.features(reached) + names := make([]string, 0, len(features)) + for _, f := range features { + names = append(names, f.Name) + } + return &Error{ + Kind: ErrorMissingFeature, + Query: e.definition.Name(), + Operation: expression.Operation(), + Property: property, + Target: symbols.FQNOf(sym), + Actual: nested, + Candidates: names, + Origin: expression.Origin(), + Outcome: &Outcome{Path: &PathError{Failures: []Failure{{ + Step: Step{Kind: StepFeature, Argument: segment}, + Expected: "a feature named " + segment + " on the element", + Frontier: Frontier{At: symbols.FQNOf(reached), Features: features, Nearest: first(suggest.Nearest(segment, names))}, + }}}}, + } +} + +// 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 { + out := []FeatureRef{} + for _, member := range e.context.Model.MembersOf(sym) { + if member == nil || !member.IsFeature() || e.context.Index.IsLibraryDocument(member.DocName) { + continue + } + name := symbols.LastSegment(member.Name) + if name == "" { + continue + } + ref := FeatureRef{Name: name} + if types, ok := e.reader.Values(member, query.PropertyType); ok && len(types) > 0 { + ref.Type = types[0] + } + out = append(out, ref) + } + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return slices.CompactFunc(out, func(a, b FeatureRef) bool { return a.Name == b.Name }) +} + func (e *executor) unknownProperty(expression queryplan.Expression, property string) error { return &Error{ Kind: ErrorUnknownProperty, diff --git a/internal/frontend/repl/docrender_test.go b/internal/frontend/repl/docrender_test.go index 15ec849842..416272b593 100644 --- a/internal/frontend/repl/docrender_test.go +++ b/internal/frontend/repl/docrender_test.go @@ -69,7 +69,8 @@ const docDiagramModel = `package Imaging { in root : Element; Project( source = WhereType(source = OwnedElements(source = root), type = "PartUsage"), - properties = ("name", "mass") + properties = ("name", "mass"), + tolerate = true // the connection link is a part usage with no mass ) } diff --git a/internal/ir/queryplan/columns_test.go b/internal/ir/queryplan/columns_test.go index 3f1839040c..7341313359 100644 --- a/internal/ir/queryplan/columns_test.go +++ b/internal/ir/queryplan/columns_test.go @@ -395,7 +395,7 @@ func TestCompilePositionalSurplusArgumentsAreTyped(t *testing.T) { fixture := loadQueryFixture(t, computedFixture+` calc def Bad :> Query { in root : Element; - Project(Descendants(source = root, maxDepth = 1), ("name"), null, null) + Project(Descendants(source = root, maxDepth = 1), ("name"), null, false, null) } `) _, err := Compile(fixture.index, fixture.model, fixture.resolver, fixture.symbol(t, "Bad")) diff --git a/internal/workspace/libs/extensions-project/.meta.json b/internal/workspace/libs/extensions-project/.meta.json index 655f6a72d1..0ca5e582e6 100644 --- a/internal/workspace/libs/extensions-project/.meta.json +++ b/internal/workspace/libs/extensions-project/.meta.json @@ -14,7 +14,7 @@ }, "libraries/DocumentQueries.sysml": { "algorithm": "SHA256", - "value": "aa60929736008efde216af31a14482817abc221a67e53c337173f53b59d63322" + "value": "370b6f880bb2dc375b534f388e3c802c5820cc45e13977c38b3d36a9d1710233" }, "libraries/IdentityMetadata.sysml": { "algorithm": "SHA256", @@ -69,7 +69,7 @@ "value": "9e00f049209584092dc8bcb398b736c08e6a39b1db667a8a744de3df72a543bf" } }, - "created": "2026-10-25T17:57:00Z", + "created": "2026-10-27T13:19:03Z", "index": { "AnalysisRecords": "libraries/AnalysisRecords.sysml", "DiagramLayout": "libraries/DiagramLayout.sysml", diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 120d45dafe..ecaa9bb75d 100644 Binary files a/internal/workspace/libs/stdlib.snapshot and b/internal/workspace/libs/stdlib.snapshot differ diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml index 69413e1e19..6f7022adb2 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml @@ -179,6 +179,8 @@ library package DocumentQueries { in 'feature' : String[1]; in operator : String[1]; in value : String[1]; + /* As for Project: an element lacking the feature is a path error unless tolerated. */ + in tolerate : Boolean[1] default false; return result : Element[0..*] ordered; } @@ -235,6 +237,11 @@ library package DocumentQueries { in source : Element[0..*] ordered; in properties : String[0..*] ordered default null; in columns : ColumnSpec[0..*] ordered default null; + /* A property an element row lacks — a feature name, or a dotted member path + * whose segment names no member on that row — is a path error naming the + * element and the features it does have; tolerate = true keeps the empty + * cell. A Column path is a computed cell and stays absent, as documented above. */ + in tolerate : Boolean[1] default false; return result : Element[0..*] ordered; } diff --git a/scripts/extension-libraries-pin.sh b/scripts/extension-libraries-pin.sh index f3ec9c497a..196576ba0d 100755 --- a/scripts/extension-libraries-pin.sh +++ b/scripts/extension-libraries-pin.sh @@ -8,4 +8,4 @@ # verifies, because a ref is mutable. Change them together. EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/Open-MBEE/OpenSysML-Extensions-Library.git}" EXTENSIONS_REF="${EXTENSIONS_REF:-main}" -EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-b36b361f85af4764de8e2817cbae62fb9e74ffdb}" +EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-745963b151f925e4543b03d5bb09b978aa492e12}"