Skip to content

fix(runtime): default an undeclared feature multiplicity to 0..* per KerML §7.4.12 - #1542

Merged
HuiJun merged 4 commits into
developfrom
fix/kerml-default-multiplicity
Oct 26, 2026
Merged

HuiJun merged 4 commits into
developfrom
fix/kerml-default-multiplicity

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A feature that declares no multiplicity and inherits none was held to an assumed 1..1 everywhere, so a two-valued KerML feature was refused:

package K {
  class C { feature x : ScalarValues::Real = (1.0, 2.0); }
  feature c : C;
}

sysml -validate was clean, but sysml -e K::c.x failed with multiplicity violation: 2 value(s) bound to a feature with multiplicity upper bound 1. It now answers [1.0, 2.0].

The default is fixed at its source and every consumer of AssumedRange / EffectiveMultiplicityOf was audited:

  • semantics/multiplicity.go: new Model.DefaultMultiplicityOf — 0..*, or the implicit [1..1] where Model.ImplicitlyDeclaresOne (an eligible SysML usage per ImplicitMultiplicityApplies, or an end feature: every SysML end, and a KerML end of a binary association or connector, whose ends redefine BinaryLink's [1..1] ends; a KerML end of an n-ary association keeps 0..*). EffectiveMultiplicityOf and GoverningMultiplicityOf fall back to it; AssumedRange is now documented as the implicit [1..1] only, and the "exactly one result" uses in the runtime (undetermined.go, collections.go) spell that as CountRange(1). DeclaresEndFeature moves from check/passes into semantics.
  • governingMultiplicity now also inherits the implicit [1..1] through redefinition (attribute grossMass :>> mass where mass is a type-owned attribute), and StatedMultiplicityOf (declared, inherited, or implicit; not ok under the bare default) is the question for consumers that refuse a feature holding several objects: the assignment-chain-step-not-one-object pass and the SMT translator's chain and holder checks (solve/reference.go, solve/nested.go) judge a stated multiplicity only, and a variation point stands for its one selection whatever it redefines.
  • Runtime shape (runtime/shape.go): featureRange reports whether the range is defaulted; a defaulted feature keeps representativeRange() (one value) as the range a representative instance holds it to, and EffectiveFeature.MultiplicityDefaulted makes checkAdmits judge no count against it — the rule the write path already had (writeTarget.countJudged, "never an unstated non-parameter range"), extended to default values. Inheritance in inheritedMultiplicity counts a redefined feature's implicit [1..1] as stated and follows role redefinitions, so a requirement subject inherits RequirementCheck::subj : Anything[1] rather than the old fallback.
  • Query plans (ir/queryplan): a query parameter stating no multiplicity still binds exactly one value — query def is this project's construct, not a clause's, and its documented contract (required parameters, [1..1] bindings) is unchanged.
  • The static value-count check (passes.checkValueCount) already judged declared/redefined bounds only; sizing (valuesGoverning → CollectionValues) keeps one value for a feature stating none, which is what the runtime's representative instance holds.

Runtime policy, not a clause: the specification constrains the count (0..*); it does not say how many objects a representative instance materializes. Taking 0..* literally there would make every undeclared non-§7.6.3 usage hold nothing (feature c : C; and part p : P; at package level would give [], a part def's state/action usages would stop being single occurrences), so a feature the default governs still materializes one object where nothing values it, exactly as before — and a declared [0..*] still materializes none, as on develop.

Behaviour before → after (bin/sysml -e):

Model before after
KerML class C { feature x : Real = (1.0, 2.0); } feature c : C; → K::c.x multiplicity violation (upper bound 1) [1.0, 2.0]
SysML package-level attribute top : Real = (1.0, 2.0) [1.0, 2.0] [1.0, 2.0]
attribute b : Real :> many = (1.0, 2.0) under many : Real[0..*] [1.0, 2.0] [1.0, 2.0]
part def P { attribute a : Real = (1.0, 2.0); } part p : P; → p.a multiplicity violation multiplicity violation
part p0 : P[0..*]; / part p2 : P[2]; / part pr : P; → .k [] / [3.0, 3.0] / 3.0 unchanged

Specification basis

  • KerML 1.1 Beta 2 §7.4.12 Multiplicities and §7.4.5.2 ("the usual default of 0..*"; binary association ends are 1..1 through BinaryLink, n-ary ends must declare it); §8.4.4.12.1 (inherited constraints).
  • SysML v2.1 Beta 2 §7.6.3 (default [0..*]; implicit [1..1] for an attribute/item/part/port usage owned by a definition or usage with no owned subsetting or redefinition), §7.13.1 (end features default to 1..1 regardless).
  • docs/project/spec-compliance.md default-multiplicity row: ⚠️ Approximate → ✅ Faithful, clause cite corrected (KerML §7.4.12 / §7.4.5.2, SysML §7.6.3); docs/project/pilot-execution-referee.md w6d:held-undeclared-multi now attributes the divergence to §7.6.3 rather than to a KerML 1.0 assumption.

How it was verified

Four layers:

  • Conformance: new internal/exec/runtime/testdata/conformance/multiplicity_default_kerml.{kerml,expected.json} (two values held, one held, () held as no value, Real[1] still refused); multiplicity_default_assumed (the §7.6.3 case) unchanged. The harness and its round-trip suites (fixtures.ModelPath) now accept a .kerml case beside the .sysml ones.
  • Semantics: TestDefaultMultiplicityFollowsTheClause (KerML class/package features, binary vs ternary association ends, SysML attribute/part/action/connection/subsetting/package/end usages); TestMultiplicityOfANonUsage now expects the unbounded range for a definition and a nil symbol (neither is a usage the implicit [1..1] is declared for).
  • Robustness: new robustness_default_multiplicity_test.go (TestRuntimeRobustnessDefaultMultiplicity): KerML defaults of 0/1/2 values and an expression producing two are held, a declared [1] and the SysML implicit [1..1] still return ErrMultiplicityViolation.
  • No trace fixture: nothing here is ordering-sensitive.

Oracles (both exit 0, git status shows no baseline moved):

  • go run -C tools ./cmd/pilot-exec-diff — bucket counts unchanged (500 cases; disagree 37, differs-by-design 5, …). The referee corpus has no KerML case, and its undeclared-multiplicity rows (w6d:undeclared-multi, :undeclared-multi-typed, :held-undeclared-multi) are SysML: the package-level ones already answered both values, and the held one is a part-def-owned attribute under §7.6.3, which stays deliberately ours. So no row moved; agreement did not increase here because the gap never reached this corpus.
  • go run -C tools ./cmd/pilot-diff — 920 file(s), 521 fully agreeing; 1098 agreed diagnostic(s), 359 only ours, 2450 only the pilot's, the committed headline.
  • The only re-recorded rows are provenance: docs/project/pilot-differential-baseline.json and docs/project/api-json-differential-baseline.json each move one line, provenance.inputs[examples].digest, because examples/self-model/execution.sysml now counts the effective feature's eleventh field; no diagnostic row or count moved in either (-update of both, diff is the digest line alone).

Gates: go build ./..., go vet ./..., gofmt -l . (empty), go test ./... with the training, pilot and spec-example corpora fetched, python3 scripts/changelog.py check, mkdocs build --strict — results below.

Dry-merge against PR #1529 (chore/pilot-pin-2026-09, head 8a100b317) and origin/develop (ab1a9cf9a) in detached worktrees: both merge clean; on each merged tree go build ./..., the touched runtime and semantics tests, ./internal/exec/solve, ./tools/referee/exec/... and the changelog check pass (results below).

go build ./...                         ok
go vet ./...                           ok
gofmt -l .                             (empty)
make lint                              ✓ Lint passed (staticcheck + gosec)
python3 scripts/changelog.py check     ok
mkdocs build --strict                  ok
go test ./...                          ok — 122 packages, training/pilot/spec-example corpora fetched
                                       (the self-model's EffectiveFeature field count moved 10 → 11)
go run -C tools ./cmd/pilot-exec-diff  500 case(s), buckets unchanged, no baseline moved
go run -C tools ./cmd/pilot-diff       920 file(s), 521 fully agreeing; 1098 / 359 / 2450 (committed headline)

dry-merge #1529 (8a100b317) + this branch: merge clean; build, runtime/semantics subsets, changelog ok
dry-merge develop (ab1a9cf9a) + this branch: merge clean; build, runtime/semantics subsets, changelog ok
(the solve suite in the detached worktrees fails only on the absent downloaded training corpus,
 which the worktrees do not carry; it passes in the checkout and with the corpus linked in)

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/12b6af85f9eb4ebdba2012b9c1dabbac
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/12b6af85f9eb4ebdba2012b9c1dabbac?variant=devin
Requested by: @HuiJun

…KerML §7.4.12

A feature that declares and inherits no multiplicity has the usual default
0..* (KerML 1.1 §7.4.12, §7.4.5.2; SysML v2 §7.6.3); only an eligible SysML
usage owned by a type, and an end feature, has the implicit [1..1].
Model.EffectiveMultiplicityOf and GoverningMultiplicityOf fall back to the new
Model.DefaultMultiplicityOf instead of AssumedRange, which is now only the
implicit [1..1]. The runtime holds a feature the default governs to one
representative value and judges no count against it
(EffectiveFeature.MultiplicityDefaulted), so a two-valued KerML feature is
held rather than refused; the implicit [1..1] is also inherited through
redefinition and by a requirement subject. Consumers that refuse a feature
holding several objects judge a stated multiplicity only.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration Bot and others added 2 commits October 26, 2026 15:57
…y field

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…oved

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review October 26, 2026 17:36
devin-ai-integration[bot]

This comment was marked as resolved.

…y association's [1..1]

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant