sysml2kit 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/CHANGELOG.md +60 -0
  2. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/CLAUDE.md +1 -2
  3. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/PKG-INFO +12 -7
  4. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/README.md +10 -5
  5. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/SPEC.md +43 -17
  6. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/rf-library.md +7 -6
  7. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/verification.md +50 -1
  8. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/pyproject.toml +5 -4
  9. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/_version.py +2 -2
  10. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/api/client.py +59 -6
  11. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/api/models.py +9 -1
  12. sysml2kit-0.4.0/src/sysml2kit/backends/_sysmlpy_patches.py +156 -0
  13. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/backends/sysmlpy.py +186 -7
  14. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/cli.py +87 -4
  15. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interchange/reader.py +24 -0
  16. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/mcp/tools_requirements.py +9 -0
  17. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/builder.py +32 -4
  18. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/text/writer.py +42 -17
  19. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/validation.py +36 -0
  20. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/verify/binding.py +29 -1
  21. sysml2kit-0.4.0/src/sysml2kit/verify/runner.py +427 -0
  22. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_api_client.py +30 -6
  23. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_api_live.py +23 -5
  24. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_backend_fidelity.py +114 -0
  25. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_cli_api.py +37 -0
  26. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_cli_fmt.py +46 -0
  27. sysml2kit-0.4.0/tests/test_cli_verify.py +278 -0
  28. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_text_writer/test_vehicle_golden.sysml +2 -2
  29. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_text_writer.py +2 -2
  30. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_validation.py +23 -1
  31. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_verify.py +140 -0
  32. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/uv.lock +6 -6
  33. sysml2kit-0.3.0/src/sysml2kit/verify/runner.py +0 -251
  34. sysml2kit-0.3.0/tests/test_cli_verify.py +0 -120
  35. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/.gitignore +0 -0
  36. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/.pre-commit-config.yaml +0 -0
  37. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/.python-version +0 -0
  38. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/CITATION.cff +0 -0
  39. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/CONTRIBUTING.md +0 -0
  40. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/LICENSE +0 -0
  41. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/NOTICE +0 -0
  42. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/SECURITY.md +0 -0
  43. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docker/compose.yaml +0 -0
  44. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/api-client.md +0 -0
  45. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/backends.md +0 -0
  46. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/cli.md +0 -0
  47. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/concepts.md +0 -0
  48. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/index.md +0 -0
  49. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/interchange.md +0 -0
  50. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/mcp.md +0 -0
  51. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/quickstart.md +0 -0
  52. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/reference.md +0 -0
  53. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/traceability.md +0 -0
  54. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/docs/views.md +0 -0
  55. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/mkdocs.yml +0 -0
  56. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/scripts/slopcheck.sh +0 -0
  57. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/scripts/wait_for_api.sh +0 -0
  58. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/__init__.py +0 -0
  59. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/api/__init__.py +0 -0
  60. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/api/errors.py +0 -0
  61. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/backends/__init__.py +0 -0
  62. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/backends/protocol.py +0 -0
  63. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/diff.py +0 -0
  64. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/graph.py +0 -0
  65. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interchange/__init__.py +0 -0
  66. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interchange/typemap.py +0 -0
  67. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interchange/writer.py +0 -0
  68. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interop/__init__.py +0 -0
  69. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/interop/requirements.py +0 -0
  70. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/mcp/__init__.py +0 -0
  71. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/mcp/_common.py +0 -0
  72. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/mcp/server.py +0 -0
  73. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/mcp/tools_model.py +0 -0
  74. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/__init__.py +0 -0
  75. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/analysis.py +0 -0
  76. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/base.py +0 -0
  77. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/container.py +0 -0
  78. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/metadata.py +0 -0
  79. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/relations.py +0 -0
  80. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/requirements.py +0 -0
  81. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/structure.py +0 -0
  82. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/model/values.py +0 -0
  83. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/py.typed +0 -0
  84. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/query.py +0 -0
  85. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/text/__init__.py +0 -0
  86. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/text/keywords.py +0 -0
  87. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/units.py +0 -0
  88. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/verify/__init__.py +0 -0
  89. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/verify/engines.py +0 -0
  90. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/views.py +0 -0
  91. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/src/sysml2kit/workspace.py +0 -0
  92. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/conftest.py +0 -0
  93. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/fixtures/RFParts.sysml +0 -0
  94. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/fixtures/RFRequirements.sysml +0 -0
  95. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/fixtures/SatcomTerminal28GHz.sysml +0 -0
  96. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/fixtures/vehicle.sysml +0 -0
  97. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_backend_sysmlpy.py +0 -0
  98. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_cli.py +0 -0
  99. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_diff.py +0 -0
  100. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_interchange.py +0 -0
  101. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_interop.py +0 -0
  102. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_mcp.py +0 -0
  103. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_model.py +0 -0
  104. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_package.py +0 -0
  105. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_query.py +0 -0
  106. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_roundtrip_json.py +0 -0
  107. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_roundtrip_text.py +0 -0
  108. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_values_units.py +0 -0
  109. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_views/test_trace_contains_all_edge_kinds.trace.mmd +0 -0
  110. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_views/test_tree_contains_hierarchy.tree.mmd +0 -0
  111. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_views.py +0 -0
  112. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tests/test_workspace.py +0 -0
  113. {sysml2kit-0.3.0 → sysml2kit-0.4.0}/tools/conformance/run_oracle.py +0 -0
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-08-21
4
+
5
+ The multi-fidelity release: one analysis, several engines, honest error
6
+ bars, and budgeted escalation — from JSON or from the textual notation.
7
+
8
+ - **Fidelity ladders.** An analysis may carry sibling
9
+ `verificationBinding` annotations labeled by two new reserved keys,
10
+ `fidelity` (rung name) and `costSeconds` (declared wall-clock, which
11
+ orders the ladder). Sibling bindings are named metadata usages typed
12
+ by a `metadata def verificationBinding` (`builder.metadata_def` +
13
+ `builder.metadata(..., definition=...)`), since distinct names are
14
+ what stable-id hashing needs; a single binding may still just be named
15
+ `verificationBinding`. Validation rule S2K010 rejects duplicate rung
16
+ labels and warns on mixed labeled/unlabeled siblings.
17
+ - **Runner policies.** `run_verification(..., policy=...)`: `all`
18
+ (default) runs every rung and reports the cross-rung `spread` per
19
+ requirement; `cheapest` runs one; `escalate` runs the cheapest rungs,
20
+ ranks must-requirements by margin thinness, and escalates within
21
+ `budget_s` against declared costs (verdicts carry `escalated_from`).
22
+ Every run records measured `seconds_by_fidelity`; write-back uses the
23
+ highest-fidelity verdict and names the rung in its provenance. CLI:
24
+ `verify --policy/--budget-s/--fidelity`; MCP `requirements_verify`
25
+ gains `policy`/`budget_s` and returns `seconds_by_fidelity`.
26
+ - **Text notation carries the whole loop.** A guarded runtime shim
27
+ (`backends/_sysmlpy_patches.py`, mirroring upstream mycr0ft/sysmlpy
28
+ #6, #7, and #9) fixes three visitor defects, and the writer emits
29
+ named dependencies (`dependency verify_1 from A to B;`), typed
30
+ bindings, and package-level metadata — so satisfy, verify, derive,
31
+ allocate, and bindings all round-trip through `.sysml` text.
32
+ `sysml2kit verify` accepts several text files parsed as one model
33
+ (the file declaring the `metadata def` rides along) and warns instead
34
+ of passing vacuously when no bindings are found.
35
+ - **Ownership survives the pilot server.** `push_model` runs in two
36
+ phases (elements with `aliasIds`, then `OwningMembership` records
37
+ mapped through server-minted ids); the interchange reader folds
38
+ membership records back into the owner map. New `api branches` and
39
+ `api commits` subcommands.
40
+ - Pins: sysmlpy floor 0.36.3; the hatchling `<1.32` cap is lifted
41
+ (Metadata-Version 2.5 passes current twine); the 2026-05 spec pin was
42
+ re-confirmed as newest on 2026-08-21.
43
+
44
+ ## 0.3.1 — 2026-08-21
45
+
46
+ Hotfix release; upgrade recommended.
47
+
48
+ - **`fmt` could silently delete verification bindings and verify links.**
49
+ The loss gate's grammar signature missed MetadataFeature nodes, and
50
+ dependency statements leave no grammar node at all, so a format pass on a
51
+ binding-bearing file removed both without triggering the refusal. The
52
+ signature now covers metadata/annotation/dependency nodes and a textual
53
+ keyword guard catches statements the parser drops entirely; `fmt` on such
54
+ files now refuses unless `--lossy` is passed.
55
+ - Metadata values in textual output were rendered with Python repr
56
+ (single-quoted strings, `True`/`False`); they now render as SysML text
57
+ (double-quoted strings, `true`/`false`).
58
+ - With multiple verificationBinding annotations on one analysis, requirement
59
+ checking used the last binding while write-back provenance named the
60
+ first. Both now use the first binding, extra bindings execute with a
61
+ logged warning, and full multi-binding support is planned.
62
+
3
63
  ## 0.3.0 — 2026-08-21
4
64
 
5
65
  - **Verification execution** (`sysml2kit.verify`): `verificationBinding`
@@ -50,8 +50,7 @@ never edit `_version.py` or add a version literal.
50
50
  - `interop/` — `RequirementSpec` extraction; the `metricKey` attribute
51
51
  convention bridges to phased-array-systems and aedl (adapters live in those
52
52
  repos, not here).
53
- - `cli.py` — typer app. MCP is deferred to v0.2; when added it goes in
54
- `src/sysml2kit/mcp/` following apab's `docs/mcp-conventions.md`.
53
+ - `cli.py` — typer app; `mcp/` follows apab's `docs/mcp-conventions.md`.
55
54
 
56
55
  ## Constraints
57
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysml2kit
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: API-first Python tooling for building, querying, validating, and automating SysML v2 models
5
5
  Project-URL: Homepage, https://github.com/jman4162/sysml2kit
6
6
  Project-URL: Source, https://github.com/jman4162/sysml2kit
@@ -33,7 +33,7 @@ Requires-Dist: networkx>=3.2; extra == 'graph'
33
33
  Provides-Extra: mcp
34
34
  Requires-Dist: mcp<2,>=1.26; extra == 'mcp'
35
35
  Provides-Extra: parse
36
- Requires-Dist: sysmlpy<0.37,>=0.36.2; extra == 'parse'
36
+ Requires-Dist: sysmlpy<0.37,>=0.36.3; extra == 'parse'
37
37
  Provides-Extra: verify
38
38
  Requires-Dist: pyyaml>=6.0; extra == 'verify'
39
39
  Description-Content-Type: text/markdown
@@ -49,11 +49,11 @@ Description-Content-Type: text/markdown
49
49
  API-first Python tooling for building, querying, validating, and automating
50
50
  SysML v2 models.
51
51
 
52
- > **Status: pre-alpha.** The 0.3.x line covers the full loop: model, writer,
52
+ > **Status: pre-alpha.** The 0.4.x line covers the full loop: model, writer,
53
53
  > interchange, queries, validation, diff, mermaid views, parse backend, MCP
54
- > server, API client with a live-server harness, and verification execution.
55
- > The API may still move between minor versions; pin an exact version if you
56
- > depend on it. Changes: [CHANGELOG.md](CHANGELOG.md).
54
+ > server, API client with a live-server harness, and multi-fidelity
55
+ > verification execution. The API may still move between minor versions; pin
56
+ > an exact version if you depend on it. Changes: [CHANGELOG.md](CHANGELOG.md).
57
57
 
58
58
  `sysml2kit` is the requirements/architecture/traceability layer for
59
59
  engineering automation stacks: build a system model in Python, emit standard
@@ -82,7 +82,12 @@ https://jman4162.github.io/sysml2kit/
82
82
  - **Verification execution**: `verificationBinding` metadata binds an
83
83
  analysis case to an engine from the `sysml2kit.engines` entry-point group;
84
84
  `sysml2kit verify` runs it and checks each requirement with margins, and
85
- can write results back into the model with provenance.
85
+ can write results back into the model with provenance. Sibling bindings
86
+ labeled with `fidelity`/`costSeconds` form a fidelity ladder: `--policy
87
+ all` reports the cross-rung spread as an error bar, `--policy escalate
88
+ --budget-s N` spends a compute budget on the thinnest margins first, and
89
+ every run records measured seconds per rung. The whole loop — bindings,
90
+ verify links, policies — works from `.sysml` text as well as JSON.
86
91
  - **Mermaid views**: ownership-tree and requirement-trace diagrams.
87
92
  - **MCP server**: nine tools for agents (`sysml2kit mcp serve`).
88
93
  - **API client**: a thin HTTP client for the OMG Systems Modeling API and
@@ -9,11 +9,11 @@
9
9
  API-first Python tooling for building, querying, validating, and automating
10
10
  SysML v2 models.
11
11
 
12
- > **Status: pre-alpha.** The 0.3.x line covers the full loop: model, writer,
12
+ > **Status: pre-alpha.** The 0.4.x line covers the full loop: model, writer,
13
13
  > interchange, queries, validation, diff, mermaid views, parse backend, MCP
14
- > server, API client with a live-server harness, and verification execution.
15
- > The API may still move between minor versions; pin an exact version if you
16
- > depend on it. Changes: [CHANGELOG.md](CHANGELOG.md).
14
+ > server, API client with a live-server harness, and multi-fidelity
15
+ > verification execution. The API may still move between minor versions; pin
16
+ > an exact version if you depend on it. Changes: [CHANGELOG.md](CHANGELOG.md).
17
17
 
18
18
  `sysml2kit` is the requirements/architecture/traceability layer for
19
19
  engineering automation stacks: build a system model in Python, emit standard
@@ -42,7 +42,12 @@ https://jman4162.github.io/sysml2kit/
42
42
  - **Verification execution**: `verificationBinding` metadata binds an
43
43
  analysis case to an engine from the `sysml2kit.engines` entry-point group;
44
44
  `sysml2kit verify` runs it and checks each requirement with margins, and
45
- can write results back into the model with provenance.
45
+ can write results back into the model with provenance. Sibling bindings
46
+ labeled with `fidelity`/`costSeconds` form a fidelity ladder: `--policy
47
+ all` reports the cross-rung spread as an error bar, `--policy escalate
48
+ --budget-s N` spends a compute budget on the thinnest margins first, and
49
+ every run records measured seconds per rung. The whole loop — bindings,
50
+ verify links, policies — works from `.sysml` text as well as JSON.
46
51
  - **Mermaid views**: ownership-tree and requirement-trace diagrams.
47
52
  - **MCP server**: nine tools for agents (`sysml2kit mcp serve`).
48
53
  - **API client**: a thin HTTP client for the OMG Systems Modeling API and
@@ -3,7 +3,8 @@
3
3
  ## Reference spec release
4
4
 
5
5
  This package targets the OMG SysML v2 standard as published in the
6
- `Systems-Modeling/SysML-v2-Release` repository, tag **`2026-05`**.
6
+ `Systems-Modeling/SysML-v2-Release` repository, tag **`2026-05`** (still the
7
+ newest release tag as of 2026-08-21).
7
8
 
8
9
  Policy: the pin moves at most quarterly, in a minor release, with the change
9
10
  noted in the changelog. `src/sysml2kit/interchange/typemap.py` is the single
@@ -50,22 +51,34 @@ not drop elements the profile lacks classes for.
50
51
 
51
52
  The backend parses via ``sysmlpy.load_grammar_antlr`` and walks the raw
52
53
  ANTLR dict (the wrapper-object loader rebuilds usage bodies lossily and is
53
- not used). What survives a text round trip:
54
+ not used). The backend also applies guarded runtime patches to sysmlpy's visitor
55
+ (``backends/_sysmlpy_patches.py``; upstream: mycr0ft/sysmlpy #6 and #7 plus
56
+ the metadata-body issue filed with them). Patches activate only for sysmlpy
57
+ 0.3x with the expected visitor surface; otherwise the pre-patch losses
58
+ apply and their tests document them.
54
59
 
55
- | Round-trips | Lost upstream (sysmlpy visitor discards it) |
60
+ | Round-trips (with patches active) | Still lost |
56
61
  |---|---|
57
- | names, short names | `dependency A to B;` statements (how the writer emits verify/derive) |
58
- | docs (package/part/requirement scope) | `allocate X to Y;` endpoints |
59
- | feature typing, incl. cross-package | `connect a.pa to b.pb;` endpoints |
60
- | multiplicity | `verification` case usages (dropped entirely) |
62
+ | names, short names | unnamed `dependency A to B;` statements (the writer emits named `verify_N`/`derive_N` dependencies, which reify) |
63
+ | docs (package/part/requirement scope) | `connect a.pa to b.pb;` endpoints |
64
+ | feature typing, incl. cross-package | `verification` case usages (dropped entirely) |
65
+ | multiplicity | metadata inside definition/case bodies (hence the package-level placement convention below) |
61
66
  | attribute values with units | value provenance (`source`/`confidence` have no textual slot) |
62
67
  | requirement subject and text | |
63
- | satisfy (package level and inside part bodies) | |
68
+ | satisfy, verify, derive, allocate | |
69
+ | package-level metadata with scalar values (verificationBinding included) | |
64
70
 
65
- Consequence: **satisfy traceability survives text; verify/derive/allocate
66
- require the JSON interchange.** The losses are pinned by tests in
67
- `tests/test_backend_fidelity.py` so an upstream sysmlpy fix surfaces as a
68
- test failure.
71
+ Verify/derive encode as **named dependencies**: ``dependency verify_1 from
72
+ ana to R1;`` the ``verify_``/``derive_`` name prefix is the round-trip
73
+ convention. Metadata annotations are placed at package level (the ``about``
74
+ reference carries the attachment); ``builder.metadata`` defaults to this.
75
+ Endpoint resolution prefers candidates under the same root package, so
76
+ multi-package files with repeated short names resolve correctly.
77
+
78
+ Consequence: **a `.sysml` file is a complete verification artifact** —
79
+ `sysml2kit verify` on text produces the same verdicts as on interchange
80
+ JSON. Remaining losses are pinned by tests in
81
+ `tests/test_backend_fidelity.py`.
69
82
 
70
83
  ## Identity and ownership
71
84
 
@@ -79,15 +92,28 @@ test failure.
79
92
 
80
93
  ## Verification binding convention
81
94
 
82
- A ``MetadataUsage`` named ``verificationBinding`` annotating an
83
- ``AnalysisCaseUsage`` binds that analysis to an executable engine. Values
84
- (flat scalars, per the metadata model):
95
+ A ``MetadataUsage`` annotating an ``AnalysisCaseUsage`` binds that
96
+ analysis to an executable engine when it is recognizable as a
97
+ ``verificationBinding`` in either of two forms:
98
+
99
+ - the usage itself is named ``verificationBinding``
100
+ (``metadata verificationBinding about study {...}``), or
101
+ - the usage is a *named* usage typed by a
102
+ ``metadata def verificationBinding``
103
+ (``metadata analyticBinding : verificationBinding about study {...}``).
104
+
105
+ The typed form is required for a fidelity ladder: sibling usages must
106
+ carry distinct names for stable-id hashing, so they share the annotation
107
+ kind through the definition instead of the name. Values (flat scalars,
108
+ per the metadata model):
85
109
 
86
110
  | key | type | meaning |
87
111
  |---|---|---|
88
112
  | ``engine`` | str, required | registry name, e.g. ``"phased-array-systems"`` |
89
113
  | ``configRef`` | str, optional | ``.yaml``/``.yml``/``.json`` payload file next to the model file (resolved relative to its directory, containment-checked; no ``..``) |
90
114
  | ``payload.<dotted>`` | scalar, optional | override deep-merged over the loaded config; dotted keys expand to nested dicts |
115
+ | ``fidelity`` | str, optional | rung label; sibling bindings on one analysis form a fidelity ladder (labels must be distinct, rule S2K010) |
116
+ | ``costSeconds`` | number, optional | declared wall-clock estimate; orders the ladder and gates the escalate policy's budget |
91
117
 
92
118
  Engines resolve **by name** against a registry populated from the
93
119
  ``sysml2kit.engines`` entry-point group and explicit caller registration.
@@ -101,8 +127,8 @@ the analysis (``source`` starts with ``sysml2kit.verify``) and a
101
127
  ``verificationVerdict`` metadata on each requirement; both replace prior
102
128
  same-named results, so reruns do not accumulate.
103
129
 
104
- Bindings are an interchange-JSON feature: metadata values round-trip through
105
- JSON, while textual-notation fidelity for metadata is not yet pinned.
130
+ Bindings round-trip through interchange JSON and, at package level, through
131
+ the textual notation (see the fidelity table above).
106
132
 
107
133
  ## Provenance on values
108
134
 
@@ -21,12 +21,13 @@ for spec in extract_requirements(model):
21
21
  It ships four library packages (RFVocabulary quantity kinds with units,
22
22
  RFParts part/port definitions, RFRequirements requirement definitions using
23
23
  the metricKey convention, RFAnalyses analysis case definitions) plus
24
- **SatcomTerminal28GHz** a worked example mirroring the aedl `t3-001`
25
- benchmark: a 28 GHz LEO uplink phased-array terminal with eight
26
- machine-checkable requirements (worst-case link margin, sidelobe level,
27
- independent link crosscheck, clear-sky and gain agreement, prime-power and
28
- unit-cost ceilings, grating-lobe margin), each satisfied by a part and
29
- verified by an analysis.
24
+ two worked examples. **SatcomTerminal28GHz** mirrors the aedl `t3-001`
25
+ benchmark: eight machine-checkable requirements, each satisfied by a part
26
+ and verified by an analysis. **SatcomTerminalPAS** is executable: its
27
+ `pasStudy` analysis carries a `verificationBinding` for the
28
+ `phased-array-systems` engine, so `sysml2kit verify` runs a real study and
29
+ all five requirements pass with margin (see
30
+ [verification](verification.md)).
30
31
 
31
32
  The library demonstrates the intended division of labor: domain vocabulary
32
33
  as SysML v2 model content, generic mechanics in the kit, physics engines
@@ -70,8 +70,57 @@ a provenance `source` (`sysml2kit.verify <engine>==<version> <timestamp>`)
70
70
  and a `verificationVerdict` metadata on each requirement, replacing prior
71
71
  results so reruns do not accumulate.
72
72
 
73
+ ## Fidelity ladders and allocation
74
+
75
+ An analysis may carry several bindings at different fidelities. Two more
76
+ reserved metadata keys describe the ladder: `fidelity` (a rung label such
77
+ as `analytic` or `pattern`) and `costSeconds` (the declared wall-clock
78
+ estimate that orders the rungs):
79
+
80
+ ```python
81
+ builder.metadata(
82
+ model,
83
+ analysis,
84
+ {
85
+ "engine": "phased-array-systems",
86
+ "configRef": "study.yaml",
87
+ "fidelity": "analytic",
88
+ "costSeconds": 0.001,
89
+ },
90
+ name="verificationBinding",
91
+ )
92
+ builder.metadata(
93
+ model,
94
+ analysis,
95
+ {
96
+ "engine": "phased-array-systems-pattern",
97
+ "configRef": "study.yaml",
98
+ "fidelity": "pattern",
99
+ "costSeconds": 1.0,
100
+ },
101
+ name="verificationBinding",
102
+ )
103
+ ```
104
+
105
+ The runner's `policy` decides which rungs execute. `all` (default) runs
106
+ every rung and reports a verdict per rung, with the cross-rung `spread` as
107
+ an honest error bar. `cheapest` runs only the lowest-cost rung per
108
+ analysis. `escalate` runs the cheapest rungs, ranks must-requirements by
109
+ margin thinness (`|margin| / |threshold|`), and spends the remaining
110
+ `budget_s` escalating the thinnest to the next rung; those verdicts carry
111
+ `escalated_from`. Every run records `seconds_by_fidelity`, so allocation
112
+ claims are auditable.
113
+
114
+ ```bash
115
+ sysml2kit verify model.json --policy escalate --budget-s 5 --report run.json
116
+ ```
117
+
118
+ Validation rule S2K010 rejects two bindings on one analysis that share a
119
+ fidelity label and warns when only some sibling bindings declare one.
120
+
73
121
  Over MCP, the `requirements_verify` tool runs the same flow with
74
- entry-point engines only, returning `passed`, must-failures, and the report
122
+ entry-point engines only (plus `policy` and `budget_s` parameters),
123
+ returning `passed`, must-failures, `seconds_by_fidelity`, and the report
75
124
  path.
76
125
 
77
126
  ## End to end with the RF library
@@ -1,6 +1,7 @@
1
1
  [build-system]
2
- # hatchling 1.32 emits Metadata-Version 2.5, which twine (and the release
3
- # pipeline's checks) do not accept yet; cap until the toolchain catches up.
2
+ # hatchling 1.32 emits Metadata-Version 2.5. Current twine accepts it, but
3
+ # the release pipeline's build-and-inspect action vendors an older twine
4
+ # that rejects it (seen failing 2026-08-21); keep the cap until it updates.
4
5
  requires = ["hatchling>=1.27,<1.32", "hatch-vcs>=0.4"]
5
6
  build-backend = "hatchling.build"
6
7
 
@@ -48,7 +49,7 @@ dependencies = [
48
49
  [project.optional-dependencies]
49
50
  # sysmlpy has a single maintainer; cap the minor version and bump deliberately
50
51
  # after checking the release notes.
51
- parse = ["sysmlpy>=0.36.2,<0.37"]
52
+ parse = ["sysmlpy>=0.36.3,<0.37"]
52
53
  graph = ["networkx>=3.2"]
53
54
  # mcp 2.0.0 removed mcp.server.fastmcp; stay on 1.x until the surface is ported.
54
55
  mcp = ["mcp>=1.26,<2"]
@@ -73,7 +74,7 @@ test = [
73
74
  "respx>=0.22",
74
75
  ]
75
76
  lint = ["mypy>=1.15", "ruff>=0.16", "types-PyYAML>=6.0"]
76
- parse-test = ["sysmlpy>=0.36.2,<0.37"]
77
+ parse-test = ["sysmlpy>=0.36.3,<0.37"]
77
78
  mcp-test = ["mcp>=1.26,<2"]
78
79
  verify-test = ["pyyaml>=6.0"]
79
80
  docs = ["mkdocs-material>=9.5", "mkdocstrings[python]>=0.27"]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.0'
22
- __version_tuple__ = version_tuple = (0, 3, 0)
21
+ __version__ = version = '0.4.0'
22
+ __version_tuple__ = version_tuple = (0, 4, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,6 +9,7 @@ hundred lines against the REST/JSON binding and returns sysml2kit models.
9
9
  from __future__ import annotations
10
10
 
11
11
  import types
12
+ import uuid
12
13
  from typing import Any, Self
13
14
 
14
15
  import httpx
@@ -19,6 +20,9 @@ from sysml2kit.model.container import Model
19
20
  from .errors import ApiError
20
21
  from .models import Branch, Commit, Project
21
22
 
23
+ #: Deterministic ids for pushed OwningMembership records.
24
+ _MEMBERSHIP_NAMESPACE = uuid.UUID("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
25
+
22
26
 
23
27
  class SysMLApiClient:
24
28
  """Synchronous client; use as a context manager to reuse one connection."""
@@ -140,18 +144,67 @@ class SysMLApiClient:
140
144
  branch: str | None = None,
141
145
  message: str | None = None,
142
146
  ) -> Commit:
143
- """Create a commit whose change set inserts every element of the model."""
144
- change = [
145
- {"@type": "DataVersion", "payload": self._to_server_record(record)}
146
- for record in model_to_json(model)
147
- ]
147
+ """Push a model as one or two commits (elements, then ownership).
148
+
149
+ The pilot re-mints element ids and resolves references only against
150
+ already-committed rows, so ownership cannot ride along with the
151
+ elements. Each element therefore carries its local id in
152
+ ``aliasIds``; after the element commit, the aliases map local ids to
153
+ server-minted ids and a second commit adds one ``OwningMembership``
154
+ per ownership pair. A model without ownership pushes as a single
155
+ commit. The returned commit is the head (the membership commit when
156
+ one was needed).
157
+ """
158
+ change = []
159
+ for record in model_to_json(model):
160
+ adapted = self._to_server_record(record)
161
+ adapted["aliasIds"] = [record["@id"]]
162
+ change.append({"@type": "DataVersion", "payload": adapted})
148
163
  payload: dict[str, Any] = {"@type": "Commit", "change": change}
149
164
  if message:
150
165
  payload["description"] = message
151
166
  path = f"/projects/{project_id}/commits"
152
167
  if branch:
153
168
  path += f"?branchId={branch}"
154
- return Commit.model_validate(self._post(path, payload))
169
+ element_commit = Commit.model_validate(self._post(path, payload))
170
+ if not model.owner:
171
+ return element_commit
172
+
173
+ minted: dict[str, str] = {}
174
+ listing = self._get(f"/projects/{project_id}/commits/{element_commit.id}/elements")
175
+ for summary in listing:
176
+ record = summary
177
+ if "aliasIds" not in record:
178
+ record = self.get_element(project_id, element_commit.id, summary["@id"])
179
+ for alias in record.get("aliasIds") or []:
180
+ minted[str(alias)] = record["@id"]
181
+
182
+ memberships = []
183
+ for member, owner in model.owner.items():
184
+ member_id, owner_id = minted.get(str(member)), minted.get(str(owner))
185
+ if member_id is None or owner_id is None:
186
+ continue # e.g. an element the server rejected
187
+ memberships.append(
188
+ {
189
+ "@type": "DataVersion",
190
+ "payload": {
191
+ "@type": "OwningMembership",
192
+ "@id": str(uuid.uuid5(_MEMBERSHIP_NAMESPACE, f"{owner}->{member}")),
193
+ "memberElement": {"@id": member_id},
194
+ "membershipOwningNamespace": {"@id": owner_id},
195
+ },
196
+ }
197
+ )
198
+ if not memberships:
199
+ return element_commit
200
+ ownership_payload: dict[str, Any] = {
201
+ "@type": "Commit",
202
+ "previousCommit": {"@id": element_commit.id},
203
+ "change": memberships,
204
+ }
205
+ if message:
206
+ ownership_payload["description"] = f"{message} (ownership)"
207
+ return Commit.model_validate(self._post(path, ownership_payload))
155
208
 
156
209
  def head_commit(self, project_id: str) -> Commit:
157
210
  """Return the newest commit of a project; raises ApiError when empty."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Field
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
6
 
7
7
 
8
8
  class Project(BaseModel):
@@ -24,6 +24,14 @@ class Branch(BaseModel):
24
24
  name: str | None = None
25
25
  head: str | None = None
26
26
 
27
+ @field_validator("head", mode="before")
28
+ @classmethod
29
+ def _head_ref_to_id(cls, value: object) -> object:
30
+ # The pilot sends head as an identified ref: {"@id": "..."}.
31
+ if isinstance(value, dict):
32
+ return value.get("@id")
33
+ return value
34
+
27
35
 
28
36
  class Commit(BaseModel):
29
37
  """A commit within a project."""
@@ -0,0 +1,156 @@
1
+ """Runtime patches for sysmlpy's ANTLR visitor, applied under a version guard.
2
+
3
+ Three upstream defects lose data before the raw dict exists; fixes are
4
+ open upstream (mycr0ft/sysmlpy #6, #7, and #9, filed as issue #8). Until a
5
+ release ships them, this module applies equivalent patches to
6
+ ``sysmlpy.antlr_visitor`` at backend import:
7
+
8
+ - ``allocate X to Y`` endpoints reach the dict (a ``part`` connector, the
9
+ same key ``ConnectionUsage`` uses),
10
+ - ``dependency [name] [from] A to B;`` statements are dispatched instead of
11
+ silently dropped,
12
+ - braced metadata bodies surface their feature texts (``bodyFeatures``).
13
+
14
+ Guards: only sysmlpy < 0.37, only when every patched attribute still looks
15
+ as expected; any mismatch logs a warning and leaves sysmlpy untouched, in
16
+ which case the documented pinned-loss behavior applies.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from importlib import metadata
23
+ from typing import Any
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _APPLIED_MARKER = "_sysml2kit_patched"
28
+
29
+
30
+ def _dependency_dict(visitor: Any, ctx: Any, prefix: Any) -> dict[str, Any]:
31
+ decl = ctx
32
+ if hasattr(ctx, "dependencyDeclaration") and ctx.dependencyDeclaration():
33
+ decl = ctx.dependencyDeclaration()
34
+ identification = None
35
+ if hasattr(decl, "identification") and decl.identification():
36
+ identification = visitor._build_identification_dict(decl.identification())
37
+ clients: list[dict[str, Any]] = []
38
+ suppliers: list[dict[str, Any]] = []
39
+ after_to = False
40
+ for child in decl.getChildren():
41
+ class_name = child.__class__.__name__
42
+ if class_name == "QualifiedNameContext":
43
+ names = child.getText().split("::")
44
+ (suppliers if after_to else clients).append({"name": "QualifiedName", "names": names})
45
+ elif not after_to and hasattr(child, "getText") and child.getText() == "to":
46
+ after_to = True
47
+ return {
48
+ "name": "PackageMember",
49
+ "prefix": prefix,
50
+ "ownedRelatedElement": {
51
+ "name": "DefinitionElement",
52
+ "ownedRelatedElement": {
53
+ "name": "Dependency",
54
+ "identification": identification,
55
+ "client": clients,
56
+ "supplier": suppliers,
57
+ "body": {"name": "RelationshipBody", "ownedRelatedElement": []},
58
+ },
59
+ },
60
+ }
61
+
62
+
63
+ def _find_node(tree: Any, name: str) -> dict[str, Any] | None:
64
+ if isinstance(tree, dict):
65
+ if tree.get("name") == name:
66
+ return tree
67
+ for key, value in tree.items():
68
+ if key != "name":
69
+ found = _find_node(value, name)
70
+ if found is not None:
71
+ return found
72
+ elif isinstance(tree, list):
73
+ for item in tree:
74
+ found = _find_node(item, name)
75
+ if found is not None:
76
+ return found
77
+ return None
78
+
79
+
80
+ def apply_patches() -> bool:
81
+ """Patch sysmlpy's visitor in place; returns whether patches are active."""
82
+ try:
83
+ import sysmlpy.antlr_visitor as visitor
84
+ except ImportError:
85
+ return False
86
+ if getattr(visitor, _APPLIED_MARKER, False):
87
+ return True
88
+ try:
89
+ version = metadata.version("sysmlpy")
90
+ except metadata.PackageNotFoundError:
91
+ version = "0"
92
+ if not version.startswith("0.3"):
93
+ logger.warning("sysmlpy %s outside the patch guard; pinned-loss behavior applies", version)
94
+ return False
95
+ required = (
96
+ "_make_allocation_usage_dict",
97
+ "_visit_definition_element_dict",
98
+ "_visit_metadata_feature_dict",
99
+ "_build_connector_part_dict",
100
+ "_build_identification_dict",
101
+ )
102
+ if not all(hasattr(visitor, attr) for attr in required):
103
+ logger.warning("sysmlpy visitor surface changed; skipping fidelity patches")
104
+ return False
105
+
106
+ original_allocation = visitor._make_allocation_usage_dict
107
+ original_definition_element = visitor._visit_definition_element_dict
108
+ original_metadata = visitor._visit_metadata_feature_dict
109
+
110
+ def patched_allocation(ctx: Any, prefix: Any = None) -> dict[str, Any]:
111
+ result: dict[str, Any] = original_allocation(ctx, prefix)
112
+ try:
113
+ aud = None
114
+ if (
115
+ ctx is not None
116
+ and hasattr(ctx, "allocationUsageDeclaration")
117
+ and ctx.allocationUsageDeclaration()
118
+ ):
119
+ aud = ctx.allocationUsageDeclaration()
120
+ if aud is not None and hasattr(aud, "connectorPart") and aud.connectorPart():
121
+ node = _find_node(result, "AllocationUsage")
122
+ if node is not None:
123
+ node["part"] = visitor._build_connector_part_dict(aud.connectorPart())
124
+ except Exception:
125
+ logger.exception("allocate endpoint patch failed; endpoints dropped")
126
+ return result
127
+
128
+ def patched_definition_element(def_elem_ctx: Any, prefix: Any = None) -> Any:
129
+ try:
130
+ if hasattr(def_elem_ctx, "dependency") and def_elem_ctx.dependency():
131
+ return _dependency_dict(visitor, def_elem_ctx.dependency(), prefix)
132
+ except Exception:
133
+ logger.exception("dependency patch failed; statement dropped")
134
+ return original_definition_element(def_elem_ctx, prefix)
135
+
136
+ def patched_metadata(ctx: Any) -> dict[str, Any]:
137
+ result: dict[str, Any] = original_metadata(ctx)
138
+ try:
139
+ body = ctx.metadataBody() if hasattr(ctx, "metadataBody") else None
140
+ if body is not None and getattr(body, "LBRACE", lambda: None)():
141
+ texts = []
142
+ elements = body.metadataBodyElement()
143
+ for element in elements or []:
144
+ texts.append(element.getText())
145
+ if texts:
146
+ result["bodyFeatures"] = texts
147
+ except Exception:
148
+ logger.exception("metadata body patch failed; values dropped")
149
+ return result
150
+
151
+ visitor._make_allocation_usage_dict = patched_allocation
152
+ visitor._visit_definition_element_dict = patched_definition_element
153
+ visitor._visit_metadata_feature_dict = patched_metadata
154
+ setattr(visitor, _APPLIED_MARKER, True)
155
+ logger.info("sysmlpy %s fidelity patches active (upstream: mycr0ft/sysmlpy#6, #7, #9)", version)
156
+ return True