Skip to content

Commit a58f301

Browse files
jman4162claude
andcommitted
Support typed sibling bindings for fidelity ladders
Three sibling MetadataUsages all named verificationBinding cannot get stable ids (their qualified names collide), so a ladder needs the SysML-native form: named usages typed by one 'metadata def verificationBinding'. extract_bindings and rule S2K010 now share an is_binding predicate accepting either form; the writer renders the typing ('metadata analyticBinding : verificationBinding about ...'); the parser keeps a named usage's typing as its definition ref and maps 'metadata def' declarations; builder.metadata gains a definition parameter and builder.metadata_def creates the annotation kind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoLtnms93KQL7Jsp9GrZZo
1 parent d41490b commit a58f301

8 files changed

Lines changed: 97 additions & 8 deletions

File tree

src/sysml2kit/backends/sysmlpy.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from sysml2kit.model.analysis import AnalysisCaseDefinition, AnalysisCaseUsage
2525
from sysml2kit.model.base import Element, Ref
2626
from sysml2kit.model.container import Model
27-
from sysml2kit.model.metadata import MetadataUsage
27+
from sysml2kit.model.metadata import MetadataDefinition, MetadataUsage
2828
from sysml2kit.model.relations import (
2929
AllocateRelationship,
3030
DeriveRelationship,
@@ -67,6 +67,7 @@
6767
"AnalysisCaseDefinition": AnalysisCaseDefinition,
6868
"AnalysisCaseUsage": AnalysisCaseUsage,
6969
"ConnectionUsage": ConnectionUsage,
70+
"MetadataDefinition": MetadataDefinition,
7071
}
7172

7273
#: Keys never descended when extracting declaration-level facts.
@@ -431,6 +432,14 @@ def _walk(self, node: Any, model: Model, owner: Element | None, state: _WalkStat
431432
)
432433
metadata_element = MetadataUsage(declared_name=name, values=values)
433434
model.add(metadata_element, owner=owner)
435+
# A named-and-typed usage (``metadata x : verificationBinding``)
436+
# keeps its typing as a definition ref; when the name itself came
437+
# from the typing slot there is nothing separate to resolve.
438+
ident = node.get("identification")
439+
if isinstance(ident, dict) and ident.get("declaredName"):
440+
typing = _first(node.get("ownedFeatureTyping"), "QualifiedName")
441+
if typing and typing.get("names"):
442+
state.typings.append((metadata_element, list(typing["names"])))
434443
if annotated:
435444
state.annotations.append((metadata_element, annotated))
436445
return

src/sysml2kit/model/builder.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from sysml2kit.model.analysis import AnalysisCaseDefinition, AnalysisCaseUsage
1111
from sysml2kit.model.base import Element, Ref
1212
from sysml2kit.model.container import Model
13-
from sysml2kit.model.metadata import MetadataUsage
13+
from sysml2kit.model.metadata import MetadataDefinition, MetadataUsage
1414
from sysml2kit.model.relations import (
1515
AllocateRelationship,
1616
DeriveRelationship,
@@ -193,19 +193,40 @@ def metadata(
193193
*,
194194
owner: Element | None = None,
195195
name: str | None = None,
196+
definition: Element | None = None,
196197
) -> MetadataUsage:
197198
"""Attach a key-value metadata annotation to an element.
198199
199200
Default ownership is the annotated element's owner (package level): the
200201
``about`` reference carries the attachment, and package-level placement
201202
is what survives the textual notation (metadata inside definition bodies
202-
is dropped by the parser).
203+
is dropped by the parser). Passing ``definition`` types the usage by a
204+
``metadata def``, which lets sibling annotations carry distinct names
205+
(a fidelity ladder) while sharing one annotation kind.
203206
"""
204-
usage = MetadataUsage(declared_name=name, annotated=Ref.to(annotated), values=dict(values))
207+
usage = MetadataUsage(
208+
declared_name=name,
209+
annotated=Ref.to(annotated),
210+
values=dict(values),
211+
definition=Ref.to(definition) if definition is not None else None,
212+
)
205213
default_owner = model.owner_of(annotated) or annotated
206214
return model.add(usage, owner=owner if owner is not None else default_owner) # type: ignore[return-value]
207215

208216

217+
def metadata_def(
218+
model: Model,
219+
name: str,
220+
*,
221+
owner: Element | None = None,
222+
doc: str | None = None,
223+
) -> MetadataDefinition:
224+
"""Add a reusable metadata annotation kind (``metadata def``)."""
225+
return model.add( # type: ignore[return-value]
226+
MetadataDefinition(declared_name=name, doc=doc), owner=owner
227+
)
228+
229+
209230
def _relate(
210231
model: Model,
211232
cls: type[SatisfyRelationship | VerifyRelationship | DeriveRelationship | AllocateRelationship],

src/sysml2kit/text/writer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ def _render_attribute(model: Model, element: AttributeUsage, indent: str) -> str
186186
def _render_metadata(model: Model, element: MetadataUsage, indent: str) -> str:
187187
about = f" about {_name_of(model, element.annotated)}" if element.annotated else ""
188188
name = f" {escape_name(element.declared_name)}" if element.declared_name else ""
189+
if element.definition is not None:
190+
name += f" : {_name_of(model, element.definition)}"
189191
if not element.values:
190192
return f"{indent}metadata{name}{about};"
191193
inner = indent + _INDENT

src/sysml2kit/validation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ def fidelity_ladder_shape(model: Model) -> Iterator[ValidationIssue]:
193193
per_analysis: dict[UUID, list[str | None]] = {}
194194
for el in model.iter_elements(kind=MetadataUsage):
195195
assert isinstance(el, MetadataUsage)
196-
if el.declared_name != "verificationBinding" or el.annotated is None:
196+
from sysml2kit.verify.binding import is_binding
197+
198+
if not is_binding(model, el) or el.annotated is None:
197199
continue
198200
label = el.values.get("fidelity")
199201
per_analysis.setdefault(el.annotated.target, []).append(

src/sysml2kit/verify/binding.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@
3131
_PAYLOAD_PREFIX = "payload."
3232

3333

34+
def is_binding(model: Model, element: MetadataUsage) -> bool:
35+
"""Whether this metadata usage is a verificationBinding.
36+
37+
Either the usage itself is named ``verificationBinding`` or it is a
38+
named usage typed by a ``metadata def verificationBinding`` — the
39+
typed form is what lets several sibling bindings (a fidelity ladder)
40+
coexist with distinct names.
41+
"""
42+
if element.declared_name == BINDING_NAME:
43+
return True
44+
if element.definition is not None and element.definition.target in model.elements:
45+
return model.resolve(element.definition).declared_name == BINDING_NAME
46+
return False
47+
48+
3449
class BindingError(ValueError):
3550
"""A verificationBinding metadata is malformed or unresolvable."""
3651

@@ -59,7 +74,7 @@ def extract_bindings(model: Model) -> list[VerificationBinding]:
5974
bindings: list[VerificationBinding] = []
6075
for element in model.iter_elements(kind=MetadataUsage):
6176
assert isinstance(element, MetadataUsage)
62-
if element.declared_name != BINDING_NAME:
77+
if not is_binding(model, element):
6378
continue
6479
if element.annotated is None:
6580
raise BindingError(f"binding {element.element_id} annotates nothing")

tests/test_backend_fidelity.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
builder,
2323
)
2424
from sysml2kit.text import write_model
25+
from sysml2kit.verify.binding import extract_bindings
2526

2627
pytestmark = pytest.mark.parse
2728

@@ -246,3 +247,40 @@ def test_full_traceability_round_trip():
246247
(binding,) = extract_bindings(reparsed)
247248
assert binding.engine == "fake"
248249
assert binding.config_ref == "c.json"
250+
251+
252+
def test_typed_binding_ladder_round_trip():
253+
"""Named usages typed by 'metadata def verificationBinding' survive text."""
254+
model = Model()
255+
pkg = builder.pkg(model, "Ladder")
256+
binding_def = builder.metadata_def(model, "verificationBinding", owner=pkg)
257+
analysis = builder.analysis(model, "study", owner=pkg)
258+
builder.metadata(
259+
model,
260+
analysis,
261+
{"engine": "a", "fidelity": "analytic", "costSeconds": 0.001},
262+
name="analyticBinding",
263+
definition=binding_def,
264+
)
265+
builder.metadata(
266+
model,
267+
analysis,
268+
{"engine": "b", "fidelity": "pattern", "costSeconds": 1.0},
269+
name="patternBinding",
270+
definition=binding_def,
271+
)
272+
model.assign_stable_ids() # distinct names: no sibling clash
273+
274+
reparsed = backend.parse(write_model(model))
275+
bindings = {b.fidelity: b for b in extract_bindings(reparsed)}
276+
assert set(bindings) == {"analytic", "pattern"}
277+
assert bindings["analytic"].engine == "a"
278+
assert bindings["pattern"].cost_s == 1.0
279+
280+
281+
def test_plain_named_metadata_is_not_a_binding():
282+
model = Model()
283+
pkg = builder.pkg(model, "P")
284+
analysis = builder.analysis(model, "study", owner=pkg)
285+
builder.metadata(model, analysis, {"engine": "a"}, name="somethingElse")
286+
assert extract_bindings(model) == []

tests/test_validation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,5 @@ def test_s2k010_mixed_labeling_warns(vehicle: Model):
100100
)
101101
builder.metadata(vehicle, analysis, {"engine": "b"}, name="verificationBinding")
102102
issues = [i for i in validate(vehicle) if i.rule_id == "S2K010"]
103-
assert issues and issues[0].severity == "warning"
103+
assert issues
104+
assert issues[0].severity == "warning"

tests/test_verify.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ def test_fidelity_ladder_escalate_policy():
258258
run = run_verification(model, registry=registry, policy="escalate", budget_s=10.0)
259259
assert calls == ["cheap", "costly"] # thin margin (401 vs 400) escalated
260260
escalated = [v for v in run.requirements if v.escalated_from]
261-
assert escalated and escalated[0].escalated_from == "analytic"
261+
assert escalated
262+
assert escalated[0].escalated_from == "analytic"
262263
assert escalated[0].fidelity == "pattern"
263264
assert set(run.seconds_by_fidelity) == {"analytic", "pattern"}
264265
assert run.passed

0 commit comments

Comments
 (0)