From 40698c58f8dd18eb03d2fdc831840a250e3c9439 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:19:22 +0000 Subject: [PATCH 1/9] fix(queryexec): the missing-feature fan-out keeps every element lacking the feature in the record, naming the others in the message Co-Authored-By: Nicolas Rouquette --- .../unreleased/query-fanout-failures.fixed.md | 1 + docs/internals/design/query-outcomes.md | 2 +- internal/doc/queryexec/errors.go | 18 ++++---- .../doc/queryexec/missing_feature_test.go | 14 ++++--- internal/doc/queryexec/operations.go | 42 ++++++++++++++----- 5 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 changes/unreleased/query-fanout-failures.fixed.md diff --git a/changes/unreleased/query-fanout-failures.fixed.md b/changes/unreleased/query-fanout-failures.fixed.md new file mode 100644 index 0000000000..78a76921bf --- /dev/null +++ b/changes/unreleased/query-fanout-failures.fixed.md @@ -0,0 +1 @@ +- `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. diff --git a/docs/internals/design/query-outcomes.md b/docs/internals/design/query-outcomes.md index b43bcb7dee..d3e9ffb972 100644 --- a/docs/internals/design/query-outcomes.md +++ b/docs/internals/design/query-outcomes.md @@ -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/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/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 { From 292fd9c5a88b1f9cc7ac4e2c5c4de4fb1581f2eb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:33:38 +0000 Subject: [PATCH 2/9] fix(queryexec): the ambiguousName finding names the namespace owning the duplicates, as the QVTS reference semantics do Co-Authored-By: Nicolas Rouquette --- changes/unreleased/query-fanout-failures.fixed.md | 1 + docs/internals/design/query-outcomes.md | 2 +- internal/doc/queryexec/named.go | 8 +++++++- internal/doc/queryexec/outcome_test.go | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changes/unreleased/query-fanout-failures.fixed.md b/changes/unreleased/query-fanout-failures.fixed.md index 78a76921bf..f837350740 100644 --- a/changes/unreleased/query-fanout-failures.fixed.md +++ b/changes/unreleased/query-fanout-failures.fixed.md @@ -1 +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 d3e9ffb972..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 | — | diff --git a/internal/doc/queryexec/named.go b/internal/doc/queryexec/named.go index 3803facbd6..7721afa9d4 100644 --- a/internal/doc/queryexec/named.go +++ b/internal/doc/queryexec/named.go @@ -56,6 +56,12 @@ func (e *executor) ambiguousElement(expression queryplan.Expression, name string } sort.Strings(fqns) step := Step{Kind: StepNamed, Argument: name} + // The finding names the namespace that owns two members of one name: it, not + // the name, violates the constraint (lean/QVTS/Ops.lean, Model.named). + owner := name + if i := strings.LastIndex(name, "::"); i >= 0 { + owner = name[:i] + } return &Error{ Kind: ErrorAmbiguousElement, Query: e.definition.Name(), @@ -65,7 +71,7 @@ func (e *executor) ambiguousElement(expression queryplan.Expression, name string Origin: expression.Origin(), Outcome: &Outcome{Model: &ModelError{Findings: []Finding{{ Step: step, - Element: name, + Element: owner, Constraint: ConstraintAmbiguousName, Observed: declaredAs(fqns), }}}}, diff --git a/internal/doc/queryexec/outcome_test.go b/internal/doc/queryexec/outcome_test.go index 8ab1175159..66d3d70b7c 100644 --- a/internal/doc/queryexec/outcome_test.go +++ b/internal/doc/queryexec/outcome_test.go @@ -67,7 +67,7 @@ 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) } encoded, _ = json.Marshal(twice) From 089f1616061fc682b005db5a0330e59c0f7eae66 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:37:15 +0000 Subject: [PATCH 3/9] fix(queryexec): the ambiguousName finding's owner comes from the resolved candidates, one finding per owning namespace Co-Authored-By: Nicolas Rouquette --- internal/doc/queryexec/named.go | 35 ++++++++++++++++++-------- internal/doc/queryexec/outcome_test.go | 10 ++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/internal/doc/queryexec/named.go b/internal/doc/queryexec/named.go index 7721afa9d4..ed389380e6 100644 --- a/internal/doc/queryexec/named.go +++ b/internal/doc/queryexec/named.go @@ -56,11 +56,21 @@ func (e *executor) ambiguousElement(expression queryplan.Expression, name string } sort.Strings(fqns) step := Step{Kind: StepNamed, Argument: name} - // The finding names the namespace that owns two members of one name: it, not - // the name, violates the constraint (lean/QVTS/Ops.lean, Model.named). - owner := name - if i := strings.LastIndex(name, "::"); i >= 0 { - owner = name[:i] + // One finding per namespace that owns a candidate: the owner of two members of one + // name violates the constraint (lean/QVTS/Ops.lean, Model.named); duplicates spread + // over several namespaces are one finding each, none of them invented from the name. + byOwner := map[string][]string{} + for _, fqn := range fqns { + byOwner[ownerOf(fqn)] = append(byOwner[ownerOf(fqn)], fqn) + } + 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, @@ -69,13 +79,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: owner, - 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 diff --git a/internal/doc/queryexec/outcome_test.go b/internal/doc/queryexec/outcome_test.go index 66d3d70b7c..2f4ce33b3a 100644 --- a/internal/doc/queryexec/outcome_test.go +++ b/internal/doc/queryexec/outcome_test.go @@ -20,6 +20,8 @@ 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") } `) outcome := func(query string) Outcome { t.Helper() @@ -70,6 +72,14 @@ calc def RootMiss :> Query { Named(qualifiedName = "Observatry") } 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) + } encoded, _ = json.Marshal(twice) if !strings.Contains(string(encoded), `"outcome":"modelError"`) || !strings.Contains(string(encoded), `"pathFailures":[]`) { t.Errorf("Twice JSON = %s", encoded) From f02c081701c1dc12c161376ca9ae9cbfc9af4c1d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:41:16 +0000 Subject: [PATCH 4/9] fix(queryexec): the ambiguousName finding names the namespace declaring the duplicate names, aliases where they are declared Co-Authored-By: Nicolas Rouquette --- internal/doc/queryexec/named.go | 53 +++++++++++++++++--------- internal/doc/queryexec/outcome_test.go | 7 ++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/internal/doc/queryexec/named.go b/internal/doc/queryexec/named.go index ed389380e6..b357321073 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,36 +34,53 @@ 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; several registrations of one element are one denotation. +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) + if !slices.ContainsFunc(found, func(o denotation) bool { return symbols.SameElement(o.element, element) }) { + found = append(found, denotation{registration: candidate, element: element}) } } - return elements, dangling + return found, dangling +} + +func elementsOf(found []denotation) []*symbols.Symbol { + out := make([]*symbols.Symbol, 0, len(found)) + for _, d := range found { + out = append(out, d.element) + } + 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)) + // One finding per namespace declaring the duplicate names (an alias counts where it + // is declared, not where its target is), each listing the elements denoted there. + byOwner := map[string][]string{} for _, candidate := range candidates { - fqns = append(fqns, symbols.FQNOf(candidate)) + fqn := symbols.FQNOf(candidate.element) + fqns = append(fqns, fqn) + owner := ownerOf(symbols.FQNOf(candidate.registration)) + byOwner[owner] = append(byOwner[owner], fqn) } sort.Strings(fqns) - step := Step{Kind: StepNamed, Argument: name} - // One finding per namespace that owns a candidate: the owner of two members of one - // name violates the constraint (lean/QVTS/Ops.lean, Model.named); duplicates spread - // over several namespaces are one finding each, none of them invented from the name. - byOwner := map[string][]string{} - for _, fqn := range fqns { - byOwner[ownerOf(fqn)] = append(byOwner[ownerOf(fqn)], fqn) + 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) @@ -117,7 +135,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 { @@ -138,7 +157,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/outcome_test.go b/internal/doc/queryexec/outcome_test.go index 2f4ce33b3a..b89ad5abe4 100644 --- a/internal/doc/queryexec/outcome_test.go +++ b/internal/doc/queryexec/outcome_test.go @@ -22,6 +22,9 @@ 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") } `) outcome := func(query string) Outcome { t.Helper() @@ -80,6 +83,10 @@ calc def Spread :> Query { Named(qualifiedName = "a") } 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) + } encoded, _ = json.Marshal(twice) if !strings.Contains(string(encoded), `"outcome":"modelError"`) || !strings.Contains(string(encoded), `"pathFailures":[]`) { t.Errorf("Twice JSON = %s", encoded) From 3a0072186d4259527c1c3c9cc98c8ad5bae97678 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:45:01 +0000 Subject: [PATCH 5/9] fix(queryexec): every matched registration reaches the ambiguousName finding; only the targets are deduplicated Co-Authored-By: Nicolas Rouquette --- internal/doc/queryexec/named.go | 22 ++++++++++++---------- internal/doc/queryexec/outcome_test.go | 11 +++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/doc/queryexec/named.go b/internal/doc/queryexec/named.go index b357321073..9f845335ec 100644 --- a/internal/doc/queryexec/named.go +++ b/internal/doc/queryexec/named.go @@ -35,7 +35,7 @@ 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. // denotation is a registration the name matched — a declaration or an alias — and -// the element it denotes; several registrations of one element are one denotation. +// the element it denotes; every registration is kept, elementsOf dedups the targets. type denotation struct { registration *symbols.Symbol element *symbols.Symbol @@ -48,17 +48,18 @@ func (e *executor) denoted(name string) (found []denotation, dangling bool) { dangling = true continue } - if !slices.ContainsFunc(found, func(o denotation) bool { return symbols.SameElement(o.element, element) }) { - found = append(found, denotation{registration: candidate, element: 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 { - out = append(out, d.element) + if !slices.ContainsFunc(out, func(o *symbols.Symbol) bool { return symbols.SameElement(o, d.element) }) { + out = append(out, d.element) + } } return out } @@ -67,16 +68,17 @@ func elementsOf(found []denotation) []*symbols.Symbol { // fully-qualified name, in name order. func (e *executor) ambiguousElement(expression queryplan.Expression, name string, candidates []denotation) error { fqns := make([]string, 0, len(candidates)) + 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 the elements denoted there. + // is declared, not where its target is), each listing every element denoted there. byOwner := map[string][]string{} for _, candidate := range candidates { - fqn := symbols.FQNOf(candidate.element) - fqns = append(fqns, fqn) owner := ownerOf(symbols.FQNOf(candidate.registration)) - byOwner[owner] = append(byOwner[owner], fqn) + byOwner[owner] = append(byOwner[owner], symbols.FQNOf(candidate.element)) } - sort.Strings(fqns) for owner := range byOwner { sort.Strings(byOwner[owner]) } diff --git a/internal/doc/queryexec/outcome_test.go b/internal/doc/queryexec/outcome_test.go index b89ad5abe4..df0782cfe4 100644 --- a/internal/doc/queryexec/outcome_test.go +++ b/internal/doc/queryexec/outcome_test.go @@ -25,6 +25,9 @@ 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() @@ -87,6 +90,14 @@ calc def Aliased :> Query { Named(qualifiedName = "Observatory::Choices::Pick") 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) From 66f39285ba03255f7964f4b5e360be1f19548bd9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:31:29 +0000 Subject: [PATCH 6/9] docs(pilot): restate the API-JSON differential record's figures from the re-recorded baseline The baseline moved with the re-pinned libraries and the tolerate examples (3fed3b3c5) but the record's scorecard headline and launch-ready gap rows were left at the earlier totals, failing TestRecordStatesTheBaselinesFigures in the rest shard. Co-Authored-By: Nicolas Rouquette --- docs/project/api-json-differential.md | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index f4bc29816c..ebad573c79 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -230,7 +230,7 @@ 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. @@ -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,19 +436,19 @@ 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`) | From 2b72ffda1017240dfdaa0ec50ee1c363a503cc38 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:40:45 +0000 Subject: [PATCH 7/9] docs(pilot): restate the API-JSON differential record's per-root breakdown from the re-recorded baseline Co-Authored-By: Nicolas Rouquette --- docs/project/api-json-differential.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index ebad573c79..8ab954f015 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -236,15 +236,15 @@ The pilot's export failed on 59 files (`pilotFailed`: its own export throws on a | 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. From bd300b9e79a07c101c8008b7ba9bb7c9ca48cbb8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:41:23 +0000 Subject: [PATCH 8/9] docs(pilot): restate the API-JSON differential record's present-different rows from the re-recorded baseline Co-Authored-By: Nicolas Rouquette --- docs/project/api-json-differential.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index 8ab954f015..b961ea28e8 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -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 From 5ebe25cb19fdcfa331db4fac12b864b55d66af8d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:16:12 +0000 Subject: [PATCH 9/9] docs(pilot): read the ownedRelationship gap commentary against the current baseline Co-Authored-By: Nicolas Rouquette --- docs/project/api-json-differential.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index b961ea28e8..49568fe09a 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -452,7 +452,7 @@ The (metaclass, property) rows ranked by the corpus elements on which the pilot | 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.