Skip to content

spike: sysml-v2-parser round-trip against holon-viz emitter - #187

Closed
elasticdotventures wants to merge 2 commits into
mainfrom
spike/sysml-v2-parser-roundtrip
Closed

spike: sysml-v2-parser round-trip against holon-viz emitter#187
elasticdotventures wants to merge 2 commits into
mainfrom
spike/sysml-v2-parser-roundtrip

Conversation

@elasticdotventures

Copy link
Copy Markdown
Member

Summary

  • Spike-tests the sysml-v2-parser crate (crates.io) — flagged as the top Tier-0 candidate in the SysML v2 tooling survey (docs: SysML v2 tooling survey (epic part 1) #180) — against real SysML-v2 text: holon-viz's own SysmlV2Emitter output plus OMG sample corpora (RootPackageTest.sysml, PartTest.sysml, "Don't Panic" Batmobile).
  • New crate: crates/sysml-v2-parser-spike (throwaway investigation binary + 3 pinned integration tests, not production code).
  • Full write-up: docs/sysml-v2-parser-spike.md.

Key findings

  • holon-viz's SysmlV2Emitter output does not currently parse as valid SysML v2 — two independent bugs: (1) the emitted closing } sits inside a // line comment and never actually closes the block body, and (2) it emits block def (SysML v1 terminology) instead of SysML v2's part def. Filed as a finding here, not fixed in this PR (out of scope for a read-only spike).
  • The parser handles the smallest OMG sample (RootPackageTest.sysml) with zero diagnostics, but hits real gaps on more complex constructs (PartTest.sysml, Batmobile) that line up almost 1:1 with the crate's own open GitHub issues.
  • parse_for_editor() never panics across malformed/garbage/empty input and returns structured, actionable diagnostics — "resilient editor mode" holds up under adversarial input, not just the happy path.
  • Crate health: v0.54.0, 56 versions in ~4.5 months (high churn for 0.x), actively maintained (pushed 2 weeks ago), but tiny adoption (4 GitHub stars) and 21 open issues including real parsing gaps.

Recommendation

Adopt-with-caveats. See doc for full reasoning and recommended next steps (fix holon-viz's emitter bugs first, pin exact version, budget for upstream issue triage).

Test plan

  • cargo build -p sysml-v2-parser-spike
  • cargo test -p sysml-v2-parser-spike (3/3 pass)
  • cargo build --workspace (unaffected, builds clean)

Adds crates/sysml-v2-parser-spike, a throwaway investigation binary +
integration tests that feed holon-viz's SysmlV2Emitter output and OMG
sample corpora (RootPackageTest.sysml, PartTest.sysml, Don't Panic
Batmobile) through the sysml-v2-parser crate.

Findings (full write-up in docs/sysml-v2-parser-spike.md):
- holon-viz's SysmlV2Emitter output does not currently parse as valid
  SysML v2, for two independent reasons: (1) its emitted closing brace
  sits inside a // line comment and never actually closes the block
  body, and (2) it emits 'block def' (SysML v1 terminology) instead of
  SysML v2's 'part def'.
- The parser handles the smallest OMG sample (RootPackageTest.sysml)
  with zero diagnostics, but hits real gaps on more complex constructs
  (PartTest.sysml, Batmobile) that line up with the crate's own open
  GitHub issues.
- parse_for_editor() never panics across malformed/garbage/empty input
  and returns structured diagnostics with actionable suggestions -
  resilient editor mode holds up under adversarial input.

Recommendation: adopt-with-caveats. See doc for details.
elasticdotventures added a commit that referenced this pull request Aug 23, 2026
…sion/Cost (#184)

* feat: widen ArtifactKind/NodeType/OperationKind with Requirement/Decision/Cost

Task 3 from docs/systems-modeling-registry-rescope.md (epic part 2,
ledgrrr#181), backed by sysml-derive (#183) per the resolved LinkML
comparison (#3a).

arc-kit-au:
- NodeType gains Requirement/Decision/Cost variants (prefixes req/dec/cost)
- New Requirement/Decision/Cost structs, following the existing
  Transaction/Classification pattern (content-hashed node_id()), each
  annotated with #[derive(SysmlBlock)] from sysml-derive
- EvidenceNode gains matching variants; node_id()/node_type()/tx_id()
  updated (tx_id() -> None for all three, same as SourceDoc/ExtractedRow)
- arc-kit-au now depends on sysml-derive

ledger-core:
- ArtifactKind gains matching variants + canonical_name() arms
- arc_kit_bridge's From<ArtifactKind> for NodeType maps them explicitly
  (was previously falling through the _ => Unknown wildcard)
- OperationKind gains RecordDecision/RecordCost/ImportRequirement, each
  with a corresponding LedgerOperation impl (content-hashed, idempotent,
  same shape as CheckTaxDeadlineOp) and wired into
  OperationDispatcher::from_scheduled_events

ledgerr-mcp:
- evidence_node_type_label() (behind the legacy feature) gets the 3
  new NodeType arms it was missing

Verified: cargo test -p arc-kit-au (46 tests), cargo test -p ledger-core
--lib (184 tests), cargo build -p ledgerr-mcp --features legacy, and
cargo check --workspace --all-features all pass. clippy clean on the
touched files.

Retrofitting existing variants (Transaction/TaxCategory/etc.) onto
sysml-derive is a tracked follow-on, not done here, per the doc's own
scoping.

* fix(sysml-derive): map primitive scalars + DateTime to SysML ScalarValues

DateTime<Utc> previously emitted the literal, invalid text
`DateTime<Utc>` as a SysML v2 attribute type -- SysML v2's grammar has
no angle-bracket generic-parameter syntax, so this would fail to parse
under any conformant SysML v2 tool. bool/usize/etc. also passed through
as bare Rust keywords with no corresponding SysML type.

Map DateTime<_> -> ScalarValues::String, bool -> ScalarValues::Boolean,
unsigned ints -> ScalarValues::Natural, signed ints ->
ScalarValues::Integer, floats -> ScalarValues::Rational. Any other
single-type-argument generic is now a compile error instead of a
silent invalid-syntax emission. Opaque domain types (NodeId,
Confidence, Decimal) still pass through as bare names -- documented as
an intentional modeling assumption, not a bug.

Adds regression tests for the field types introduced by #184/#193
(Decimal, Confidence, bool, usize, DateTime<Utc>) that basic.rs never
exercised. Closes ledgrrr#195.

* fix(holon-viz): emit valid SysML v2 (part def, not block def); wire real parser validation into ufo-types

Ran SysmlV2Emitter's own output through the real sysml-v2-parser crate
(docs/sysml-v2-parser-spike.md, an existing unmerged spike) and confirmed
two bugs make it non-parseable:

- The closing '}' was on the same line as a trailing '//' comment, so the
  comment swallowed it -- the block was never syntactically closed.
- The emitter used SysML v1's 'block def' keyword. SysML v2 renamed this
  construct to 'part def'; 'block' is not a SysML v2 keyword at all.

Fixed both. Added ufo_types::sysml -- a shared Constraint/Satisfies-based
SysML v2 syntax validator wired to sysml-v2-parser (pinned to =0.54.0 per
the spike's crate-health findings; not wasm32-compatible, so this must
stay out of holon-viz's runtime dependency graph -- added to holon-viz
only as a dev-dependency, used in a new round-trip test that feeds the
emitter's own output through the real parser instead of just asserting on
substrings.

This is the concrete round-trip-closed signal the existing spike (PR #187)
called out as the next step.

* fix(sysml-derive): emit part def (not block def); validate against real SysML v2 grammar

Same bug holon-viz's SysmlV2Emitter had (ledgrrr#197): SysML v1 called
this construct Block/'block def'; SysML v2 renamed it to 'part def', and
'block' is not a SysML v2 keyword at all. Confirmed via the newly-wired
ufo_types::sysml::validate_sysml_v2 (real sysml-v2-parser crate, not a
hand-rolled heuristic) that the fixed output actually parses.

Adds tests/real_grammar_validation.rs: runs the actual generated
sysml_block_def() text for Transaction/Requirement/ExtractedRow/
ModelProposal/WorkbookRow (mirroring the real production structs from
#184/#193) through the real parser. This replaces the 'no angle brackets'
manual check used to validate the earlier DateTime/bool/usize scalar
mapping fix with genuine grammar validation, closing out ledgrrr#195.

* test(arc-kit-au): update block def -> part def assertion (sysml-derive fix)

* chore: regenerate viz-manifest.json (version drift, ledgrrr#194)

* feat(iso): dedicated ZLayer::SystemsModel variant (#185)

* feat(iso): add dedicated ZLayer::SystemsModel variant

Requirement/Decision/Cost content (ledgrrr#184) gets its own isometric
layer rather than folding into the existing 6 or the still-unimplemented
proposed Domain layer (docs/ontological-implementation-spec.md §6.1),
per decision 2 in docs/systems-modeling-registry-rescope.md: independent
toggle/color in the renderer over reusing an ontological-concepts layer.

index=6, base_z=816.0 (continuing the existing 136.0 spacing), color
#be185d (distinct from all 6 existing hexes). No HasVisualization impls
wired yet for Requirement/Decision/Cost themselves — that requires also
touching xtask's viz_manifest export + the checked-in viz-manifest.json
per iso_objects.rs's own convention, tracked as a follow-on alongside
task 6 (ledgerr-mcp/contract.rs wiring), not done here.

* feat: spike reqif-opa-mcp over MCP + Requirement converter (#186)

* feat(reqif-mcp-spike): Rust MCP client for reqif-opa-mcp + Requirement converter

Spike per decision 6 (docs/systems-modeling-registry-rescope.md §5/§6
task 5): reqif-opa-mcp is wrapped over MCP, not ported to Rust;
arc-kit-au stays the canonical decision+cost ledger.

New crate reqif-mcp-spike:
- McpHttpClient: minimal blocking client for reqif-opa-mcp's Streamable-
  HTTP MCP server (FastMCP 3.0.0b1, protocol 2024-11-05). Handles the
  initialize handshake + mcp-session-id header, the mandatory
  notifications/initialized follow-up, and tools/call, extracting the
  JSON-RPC result out of the single-frame SSE response body.
- RequirementRecord: mirrors reqif-opa-mcp's requirement-record.schema.json
  exactly (uid/key/subtypes/status/policy_baseline/rubrics/text/attrs).
- requirement_record_to_node(): converts a RequirementRecord into the
  arc-kit-au Requirement struct from ledgrrr#184 (ArtifactKind::Requirement /
  NodeType::Requirement). requirement_id<-uid, title<-key, rationale<-text,
  source<-attrs.source_standard(+source_url) falling back to the policy
  baseline id, status passed through, related_decisions always empty
  (reqif-opa-mcp carries no decision links; those are created later in
  arc-kit-au itself).

Verified live end-to-end against a real reqif-opa-mcp checkout
(uv sync --extra ingest-lite; uv run python -m reqif_mcp --http --port
8123) parsing both its own sample derived baselines,
samples/standards/derived/{nist_ssdf_dogfood,owasp_asvs_cwe}.reqif (4
and 7 requirements respectively), through reqif_parse -> reqif_query ->
requirement_record_to_node -> Requirement::node_id(), all producing
correct, deterministic req: NodeIds. That live run is captured as an
#[ignore]'d integration test (tests/live_server.rs) since it needs an
external repo checkout + Python/uv, not something CI or a fresh clone
has; re-run manually with REQIF_MCP_URL set to a running server.

cargo test -p reqif-mcp-spike: 5 unit tests pass, 1 ignored (live).
cargo check --workspace --all-features: clean. cargo clippy -p
reqif-mcp-spike --all-targets: clean.

* feat(ledgerr-mcp): wire Requirement/Decision/Cost into contract.rs (task 6)

- EvidenceArgs gains import_requirement/record_decision/record_cost
  actions, each constructing the corresponding arc-kit-au node
  (Requirement/Decision/Cost from ledgrrr#184) and inserting it into the
  evidence graph via EvidenceGraph::add_node. DuplicateNode is treated as
  idempotent success (content-hash dedup), matching the idempotent
  semantics already established for EvidenceBuilder's ensure_* methods.
- parse_evidence_node_type gains requirement/req, decision/dec, cost
  (plus previously-missing rnd_activity/tax_offset, same class of gap).
- Summary's node_counts and ListNodes' invalid-type error message
  extended to include the 3 new types.
- EVIDENCE_TOOL's contract.rs action list and purpose string updated.

Verified: new tests/evidence_requirement_decision_cost.rs (3 tests,
covering import+list+detail+summary, record_decision+record_cost, and
idempotent re-import) all pass; cargo build -p ledgerr-mcp --features
legacy clean (1 pre-existing unrelated warning).

* docs: regenerate mcp-capability-contract.md (drift from task 6)

Task 6 (ledgrrr#186's second commit) added import_requirement/
record_decision/record_cost to ledgerr_evidence's EVIDENCE_TOOL actions
in contract.rs but never re-ran regen-docs, so the checked-in
mcp-capability-contract.md drifted — caught by CI's check-drift step
on downstream stacked PRs (#190, #193).

Ran: cargo run -p ledgerr-mcp --bin regen-docs

* docs: regenerate viz-manifest.json (stale version field, 1.9.0 -> 1.10.0)

Pre-existing drift, unrelated to Requirement/Decision/Cost content (28
objects, unchanged) -- the workspace version was bumped to 1.10.0 at
some point after this artifact was last regenerated. Only surfaced now
because check-drift's earlier mcp-capability-contract.md failure (fixed
in dd168f7) was masking this second, independent drift.

Ran: cargo run -p xtask-mcpb -- export-viz-manifest

* feat(iso): HasVisualization + viz_manifest wiring for Requirement/Decision/Cost (#190)

* feat(iso): HasVisualization + viz_manifest wiring for Requirement/Decision/Cost

Adds dedicated SemanticType::{Requirement,Decision,Cost} variants
(iso.rs) and HasVisualization impls for arc_kit_au::node::{Requirement,
Decision,Cost} (iso_objects.rs, gated behind the arc-kit-au feature,
matching ontology.rs's arc_kit_bridge precedent), all routed to
ZLayer::SystemsModel (added in ledgrrr#185).

Wires the 3 new types into xtask's export_viz_manifest (now 31 domain
types, up from 28) and regenerates the checked-in viz-manifest.json.
Updates pipeline_e2e.rs's EXPECTED_ENTRY_COUNT and representative-type
assertions to match.

Verified: cargo test -p ledger-core --lib (185 passed), cargo test -p
ledgerr-mcp --test pipeline_e2e (manifest count test passes), cargo
check --workspace --all-features clean.

This closes out the one remaining gap from ledgrrr#185 (task 4 in
docs/systems-modeling-registry-rescope.md's §6), left explicitly
unfinished there pending this larger change.

* feat(arc-kit-au): retrofit #[derive(SysmlBlock)] onto existing node types (#193)

Applies the sysml-derive macro (feat/sysml-derive-spike, #183) to the 8
pre-existing arc-kit-au node structs that predate the systems-modeling
epic: SourceDoc, ExtractedRow, Transaction, Classification,
ModelProposal, OperatorApproval, ValidationIssue, WorkbookRow.

This was the second of two explicitly-deferred follow-ons from task 3
(#184) — the first (HasVisualization/viz_manifest wiring for
Requirement/Decision/Cost) landed as part of this same stack. The
derive is purely syntactic (walks named fields via syn, stringifies
each field's type via quote!) so it applies uniformly regardless of
field type — no per-struct special-casing needed, confirmed by
re-reading crates/sysml-derive/src/lib.rs before applying.

Verified: cargo test -p sysml-derive (2 tests), cargo test -p
arc-kit-au (46 tests, unchanged from pre-retrofit baseline), cargo
check --workspace --all-features (clean, one pre-existing unrelated
warning in ledgerr-mcp/src/fbar.rs), cargo clippy -p arc-kit-au
--all-features (clean).
elasticdotventures added a commit that referenced this pull request Aug 23, 2026
* feat(sysml-derive): spike a Rust-AST-to-SysML-v2 derive macro

New crate: #[derive(SysmlBlock)] walks a struct's named fields via syn
and generates a sysml_block_def() associated fn returning the
equivalent SysML-v2 'block def' text at compile time (matching
holon-viz's SysmlV2Emitter 'block def' terminology).

Field-type mapping: Vec<T> -> T[*], Option<T> -> T[0..1], everything
else -> plain T. Not yet checked against a real SysML-v2 grammar/parser
(Part 1's Tier 0 candidates) -- this is the spike from
docs/systems-modeling-registry-rescope.md #2a/#6 task 1, proving the
AST-walk direction before comparing it against the LinkML spike
(task 2) and wiring either into the real ArtifactKind/NodeType widening
(task 3).

Tests mirror arc-kit-au::Transaction's and ::Classification's field
shapes (name+type only, no dependency on arc-kit-au itself -- wiring
the derive onto the real production structs is task 3, not this spike)
and assert on the emitted block def text for both the Vec and Option
multiplicity branches.

Same technique this codebase already approved for a different target:
AGENTS.md (PM-3, 2026-05-13) sanctions #[derive(specta::Type)] for
Rust-to-TypeScript; this is Rust-to-SysML-v2/KerML instead.

* chore: regenerate viz-manifest.json (version drift 1.9.0 -> 1.10.0)

* feat: widen ArtifactKind/NodeType/OperationKind with Requirement/Decision/Cost (#184)

* feat: widen ArtifactKind/NodeType/OperationKind with Requirement/Decision/Cost

Task 3 from docs/systems-modeling-registry-rescope.md (epic part 2,
ledgrrr#181), backed by sysml-derive (#183) per the resolved LinkML
comparison (#3a).

arc-kit-au:
- NodeType gains Requirement/Decision/Cost variants (prefixes req/dec/cost)
- New Requirement/Decision/Cost structs, following the existing
  Transaction/Classification pattern (content-hashed node_id()), each
  annotated with #[derive(SysmlBlock)] from sysml-derive
- EvidenceNode gains matching variants; node_id()/node_type()/tx_id()
  updated (tx_id() -> None for all three, same as SourceDoc/ExtractedRow)
- arc-kit-au now depends on sysml-derive

ledger-core:
- ArtifactKind gains matching variants + canonical_name() arms
- arc_kit_bridge's From<ArtifactKind> for NodeType maps them explicitly
  (was previously falling through the _ => Unknown wildcard)
- OperationKind gains RecordDecision/RecordCost/ImportRequirement, each
  with a corresponding LedgerOperation impl (content-hashed, idempotent,
  same shape as CheckTaxDeadlineOp) and wired into
  OperationDispatcher::from_scheduled_events

ledgerr-mcp:
- evidence_node_type_label() (behind the legacy feature) gets the 3
  new NodeType arms it was missing

Verified: cargo test -p arc-kit-au (46 tests), cargo test -p ledger-core
--lib (184 tests), cargo build -p ledgerr-mcp --features legacy, and
cargo check --workspace --all-features all pass. clippy clean on the
touched files.

Retrofitting existing variants (Transaction/TaxCategory/etc.) onto
sysml-derive is a tracked follow-on, not done here, per the doc's own
scoping.

* fix(sysml-derive): map primitive scalars + DateTime to SysML ScalarValues

DateTime<Utc> previously emitted the literal, invalid text
`DateTime<Utc>` as a SysML v2 attribute type -- SysML v2's grammar has
no angle-bracket generic-parameter syntax, so this would fail to parse
under any conformant SysML v2 tool. bool/usize/etc. also passed through
as bare Rust keywords with no corresponding SysML type.

Map DateTime<_> -> ScalarValues::String, bool -> ScalarValues::Boolean,
unsigned ints -> ScalarValues::Natural, signed ints ->
ScalarValues::Integer, floats -> ScalarValues::Rational. Any other
single-type-argument generic is now a compile error instead of a
silent invalid-syntax emission. Opaque domain types (NodeId,
Confidence, Decimal) still pass through as bare names -- documented as
an intentional modeling assumption, not a bug.

Adds regression tests for the field types introduced by #184/#193
(Decimal, Confidence, bool, usize, DateTime<Utc>) that basic.rs never
exercised. Closes ledgrrr#195.

* fix(holon-viz): emit valid SysML v2 (part def, not block def); wire real parser validation into ufo-types

Ran SysmlV2Emitter's own output through the real sysml-v2-parser crate
(docs/sysml-v2-parser-spike.md, an existing unmerged spike) and confirmed
two bugs make it non-parseable:

- The closing '}' was on the same line as a trailing '//' comment, so the
  comment swallowed it -- the block was never syntactically closed.
- The emitter used SysML v1's 'block def' keyword. SysML v2 renamed this
  construct to 'part def'; 'block' is not a SysML v2 keyword at all.

Fixed both. Added ufo_types::sysml -- a shared Constraint/Satisfies-based
SysML v2 syntax validator wired to sysml-v2-parser (pinned to =0.54.0 per
the spike's crate-health findings; not wasm32-compatible, so this must
stay out of holon-viz's runtime dependency graph -- added to holon-viz
only as a dev-dependency, used in a new round-trip test that feeds the
emitter's own output through the real parser instead of just asserting on
substrings.

This is the concrete round-trip-closed signal the existing spike (PR #187)
called out as the next step.

* fix(sysml-derive): emit part def (not block def); validate against real SysML v2 grammar

Same bug holon-viz's SysmlV2Emitter had (ledgrrr#197): SysML v1 called
this construct Block/'block def'; SysML v2 renamed it to 'part def', and
'block' is not a SysML v2 keyword at all. Confirmed via the newly-wired
ufo_types::sysml::validate_sysml_v2 (real sysml-v2-parser crate, not a
hand-rolled heuristic) that the fixed output actually parses.

Adds tests/real_grammar_validation.rs: runs the actual generated
sysml_block_def() text for Transaction/Requirement/ExtractedRow/
ModelProposal/WorkbookRow (mirroring the real production structs from
#184/#193) through the real parser. This replaces the 'no angle brackets'
manual check used to validate the earlier DateTime/bool/usize scalar
mapping fix with genuine grammar validation, closing out ledgrrr#195.

* test(arc-kit-au): update block def -> part def assertion (sysml-derive fix)

* chore: regenerate viz-manifest.json (version drift, ledgrrr#194)

* feat(iso): dedicated ZLayer::SystemsModel variant (#185)

* feat(iso): add dedicated ZLayer::SystemsModel variant

Requirement/Decision/Cost content (ledgrrr#184) gets its own isometric
layer rather than folding into the existing 6 or the still-unimplemented
proposed Domain layer (docs/ontological-implementation-spec.md §6.1),
per decision 2 in docs/systems-modeling-registry-rescope.md: independent
toggle/color in the renderer over reusing an ontological-concepts layer.

index=6, base_z=816.0 (continuing the existing 136.0 spacing), color
#be185d (distinct from all 6 existing hexes). No HasVisualization impls
wired yet for Requirement/Decision/Cost themselves — that requires also
touching xtask's viz_manifest export + the checked-in viz-manifest.json
per iso_objects.rs's own convention, tracked as a follow-on alongside
task 6 (ledgerr-mcp/contract.rs wiring), not done here.

* feat: spike reqif-opa-mcp over MCP + Requirement converter (#186)

* feat(reqif-mcp-spike): Rust MCP client for reqif-opa-mcp + Requirement converter

Spike per decision 6 (docs/systems-modeling-registry-rescope.md §5/§6
task 5): reqif-opa-mcp is wrapped over MCP, not ported to Rust;
arc-kit-au stays the canonical decision+cost ledger.

New crate reqif-mcp-spike:
- McpHttpClient: minimal blocking client for reqif-opa-mcp's Streamable-
  HTTP MCP server (FastMCP 3.0.0b1, protocol 2024-11-05). Handles the
  initialize handshake + mcp-session-id header, the mandatory
  notifications/initialized follow-up, and tools/call, extracting the
  JSON-RPC result out of the single-frame SSE response body.
- RequirementRecord: mirrors reqif-opa-mcp's requirement-record.schema.json
  exactly (uid/key/subtypes/status/policy_baseline/rubrics/text/attrs).
- requirement_record_to_node(): converts a RequirementRecord into the
  arc-kit-au Requirement struct from ledgrrr#184 (ArtifactKind::Requirement /
  NodeType::Requirement). requirement_id<-uid, title<-key, rationale<-text,
  source<-attrs.source_standard(+source_url) falling back to the policy
  baseline id, status passed through, related_decisions always empty
  (reqif-opa-mcp carries no decision links; those are created later in
  arc-kit-au itself).

Verified live end-to-end against a real reqif-opa-mcp checkout
(uv sync --extra ingest-lite; uv run python -m reqif_mcp --http --port
8123) parsing both its own sample derived baselines,
samples/standards/derived/{nist_ssdf_dogfood,owasp_asvs_cwe}.reqif (4
and 7 requirements respectively), through reqif_parse -> reqif_query ->
requirement_record_to_node -> Requirement::node_id(), all producing
correct, deterministic req: NodeIds. That live run is captured as an
#[ignore]'d integration test (tests/live_server.rs) since it needs an
external repo checkout + Python/uv, not something CI or a fresh clone
has; re-run manually with REQIF_MCP_URL set to a running server.

cargo test -p reqif-mcp-spike: 5 unit tests pass, 1 ignored (live).
cargo check --workspace --all-features: clean. cargo clippy -p
reqif-mcp-spike --all-targets: clean.

* feat(ledgerr-mcp): wire Requirement/Decision/Cost into contract.rs (task 6)

- EvidenceArgs gains import_requirement/record_decision/record_cost
  actions, each constructing the corresponding arc-kit-au node
  (Requirement/Decision/Cost from ledgrrr#184) and inserting it into the
  evidence graph via EvidenceGraph::add_node. DuplicateNode is treated as
  idempotent success (content-hash dedup), matching the idempotent
  semantics already established for EvidenceBuilder's ensure_* methods.
- parse_evidence_node_type gains requirement/req, decision/dec, cost
  (plus previously-missing rnd_activity/tax_offset, same class of gap).
- Summary's node_counts and ListNodes' invalid-type error message
  extended to include the 3 new types.
- EVIDENCE_TOOL's contract.rs action list and purpose string updated.

Verified: new tests/evidence_requirement_decision_cost.rs (3 tests,
covering import+list+detail+summary, record_decision+record_cost, and
idempotent re-import) all pass; cargo build -p ledgerr-mcp --features
legacy clean (1 pre-existing unrelated warning).

* docs: regenerate mcp-capability-contract.md (drift from task 6)

Task 6 (ledgrrr#186's second commit) added import_requirement/
record_decision/record_cost to ledgerr_evidence's EVIDENCE_TOOL actions
in contract.rs but never re-ran regen-docs, so the checked-in
mcp-capability-contract.md drifted — caught by CI's check-drift step
on downstream stacked PRs (#190, #193).

Ran: cargo run -p ledgerr-mcp --bin regen-docs

* docs: regenerate viz-manifest.json (stale version field, 1.9.0 -> 1.10.0)

Pre-existing drift, unrelated to Requirement/Decision/Cost content (28
objects, unchanged) -- the workspace version was bumped to 1.10.0 at
some point after this artifact was last regenerated. Only surfaced now
because check-drift's earlier mcp-capability-contract.md failure (fixed
in dd168f7) was masking this second, independent drift.

Ran: cargo run -p xtask-mcpb -- export-viz-manifest

* feat(iso): HasVisualization + viz_manifest wiring for Requirement/Decision/Cost (#190)

* feat(iso): HasVisualization + viz_manifest wiring for Requirement/Decision/Cost

Adds dedicated SemanticType::{Requirement,Decision,Cost} variants
(iso.rs) and HasVisualization impls for arc_kit_au::node::{Requirement,
Decision,Cost} (iso_objects.rs, gated behind the arc-kit-au feature,
matching ontology.rs's arc_kit_bridge precedent), all routed to
ZLayer::SystemsModel (added in ledgrrr#185).

Wires the 3 new types into xtask's export_viz_manifest (now 31 domain
types, up from 28) and regenerates the checked-in viz-manifest.json.
Updates pipeline_e2e.rs's EXPECTED_ENTRY_COUNT and representative-type
assertions to match.

Verified: cargo test -p ledger-core --lib (185 passed), cargo test -p
ledgerr-mcp --test pipeline_e2e (manifest count test passes), cargo
check --workspace --all-features clean.

This closes out the one remaining gap from ledgrrr#185 (task 4 in
docs/systems-modeling-registry-rescope.md's §6), left explicitly
unfinished there pending this larger change.

* feat(arc-kit-au): retrofit #[derive(SysmlBlock)] onto existing node types (#193)

Applies the sysml-derive macro (feat/sysml-derive-spike, #183) to the 8
pre-existing arc-kit-au node structs that predate the systems-modeling
epic: SourceDoc, ExtractedRow, Transaction, Classification,
ModelProposal, OperatorApproval, ValidationIssue, WorkbookRow.

This was the second of two explicitly-deferred follow-ons from task 3
(#184) — the first (HasVisualization/viz_manifest wiring for
Requirement/Decision/Cost) landed as part of this same stack. The
derive is purely syntactic (walks named fields via syn, stringifies
each field's type via quote!) so it applies uniformly regardless of
field type — no per-struct special-casing needed, confirmed by
re-reading crates/sysml-derive/src/lib.rs before applying.

Verified: cargo test -p sysml-derive (2 tests), cargo test -p
arc-kit-au (46 tests, unchanged from pre-retrofit baseline), cargo
check --workspace --all-features (clean, one pre-existing unrelated
warning in ledgerr-mcp/src/fbar.rs), cargo clippy -p arc-kit-au
--all-features (clean).
elasticdotventures added a commit that referenced this pull request Aug 23, 2026
…eal parser validation into ufo-types

Ran SysmlV2Emitter's own output through the real sysml-v2-parser crate
(docs/sysml-v2-parser-spike.md, an existing unmerged spike) and confirmed
two bugs make it non-parseable:

- The closing '}' was on the same line as a trailing '//' comment, so the
  comment swallowed it -- the block was never syntactically closed.
- The emitter used SysML v1's 'block def' keyword. SysML v2 renamed this
  construct to 'part def'; 'block' is not a SysML v2 keyword at all.

Fixed both. Added ufo_types::sysml -- a shared Constraint/Satisfies-based
SysML v2 syntax validator wired to sysml-v2-parser (pinned to =0.54.0 per
the spike's crate-health findings; not wasm32-compatible, so this must
stay out of holon-viz's runtime dependency graph -- added to holon-viz
only as a dev-dependency, used in a new round-trip test that feeds the
emitter's own output through the real parser instead of just asserting on
substrings.

This is the concrete round-trip-closed signal the existing spike (PR #187)
called out as the next step.
elasticdotventures added a commit that referenced this pull request Aug 23, 2026
…eal parser validation into ufo-types (#197)

Ran SysmlV2Emitter's own output through the real sysml-v2-parser crate
(docs/sysml-v2-parser-spike.md, an existing unmerged spike) and confirmed
two bugs make it non-parseable:

- The closing '}' was on the same line as a trailing '//' comment, so the
  comment swallowed it -- the block was never syntactically closed.
- The emitter used SysML v1's 'block def' keyword. SysML v2 renamed this
  construct to 'part def'; 'block' is not a SysML v2 keyword at all.

Fixed both. Added ufo_types::sysml -- a shared Constraint/Satisfies-based
SysML v2 syntax validator wired to sysml-v2-parser (pinned to =0.54.0 per
the spike's crate-health findings; not wasm32-compatible, so this must
stay out of holon-viz's runtime dependency graph -- added to holon-viz
only as a dev-dependency, used in a new round-trip test that feeds the
emitter's own output through the real parser instead of just asserting on
substrings.

This is the concrete round-trip-closed signal the existing spike (PR #187)
called out as the next step.
elasticdotventures pushed a commit that referenced this pull request Aug 30, 2026
PR #187 (spike/sysml-v2-parser-roundtrip) found holon-viz's SysmlV2Emitter
produced invalid SysML v2 (comment-swallowed closing brace, `block def`
instead of `part def`). Both bugs were independently fixed on main in PR
#183 (c106a5e) before this doc landed. Cherry-picks only the investigation
doc; the throwaway sysml-v2-parser-spike crate stays on the closed PR's
branch since its regression test now asserts stale (pre-fix) behavior.
@elasticdotventures

Copy link
Copy Markdown
Member Author

Findings ported: the two holon-viz SysmlV2Emitter bugs this spike found (comment-swallowed closing brace, block def vs part def) were independently fixed on main in PR #183 (c106a5e) ahead of this branch ever merging. The investigation doc (docs/sysml-v2-parser-spike.md) has been cherry-picked and updated to reflect that onto #216, along with a pointer back to this branch for the throwaway sysml-v2-parser-spike crate if the investigation ever needs re-running. Closing as superseded.

elasticdotventures pushed a commit that referenced this pull request Aug 30, 2026
PR #187 (spike/sysml-v2-parser-roundtrip) found holon-viz's SysmlV2Emitter
produced invalid SysML v2 (comment-swallowed closing brace, `block def`
instead of `part def`). Both bugs were independently fixed on main in PR
#183 (c106a5e) before this doc landed. Cherry-picks only the investigation
doc; the throwaway sysml-v2-parser-spike crate stays on the closed PR's
branch since its regression test now asserts stale (pre-fix) behavior.
elasticdotventures added a commit that referenced this pull request Aug 31, 2026
…tings model (#216)

* fix(ledgrrr-settings): stop leaking test data; replace BurntToast/PowerShell; retire host-tray.rs

- NotificationBackend::PowerShell renamed to Native (serde alias keeps
  reading settings persisted under the old name) — the actual toast
  delivery is being replaced with native windows-rs calls in ledgerr-host,
  this crate only owns the shared data types.
- SettingsStore::with_backend(path, backend) added alongside new(), so
  tests can construct an isolated JsonFileBackend directly instead of
  going through Windows registry auto-selection — new()'s registry
  backend ignores its path argument entirely and always targets one
  fixed production key, which is correct for real callers but means any
  test using new() over a tempdir shares that one global mutable key
  with every other test and any real running instance.
- crates/ledgerr-host/src/bin/host-tray.rs moved to src/bin/legacy/ as
  reference-only (git mv staged this alongside the above; it's the first
  step of retiring the standalone host-tray.exe binary — completed in
  the following commits, which remove its [[bin]] entry and repoint
  host-tauri's tray at the same shared runtime it used).

* fix(ledgerr-desktop-agent): stop leaking test registry keys; drop stale host-tray refs

settings_server.rs's tests hit the same registry-auto-selection issue
fixed in ledgrrr-settings — repoint them at SettingsStore::with_backend
+ JsonFileBackend explicitly. status.rs/service_control.rs still
listed host-tray as a tray-binary candidate; it's being retired in
favor of host-tauri.exe, which already sorts first in that list.

* feat(notify): replace BurntToast/PowerShell dependency with native windows-rs toasts

The tray's toast notifications shelled out to powershell.exe running
Import-Module BurntToast; New-BurntToastNotification — requiring a
separately-installed PowerShell module on every machine.

A native windows::UI::Notifications-based ToastNotifier already existed
in this crate (notification::windows_toast) but was never wired into
the tray's actual notification path — only its own unused tests
exercised it. Added notify::native::NativeToastNotifier, implementing
the same notify::types::Notifier trait the tray already consumes,
wrapping that existing native implementation (and the stderr fallback
on non-Windows) instead of shelling out to any external process.

* refactor(tray): remove dead code, group notification toggles into a submenu

native.rs: drop the unused push_check helper, DYNAMIC_TEXT_IDS and
CHECK_ITEM_IDS constants, and fix a few compiler-flagged unused-mut/
unused-must-use warnings.

Group the four notification-event toggles (approval/submitted/failed/
completed) into a "Notify me for" submenu instead of sitting flat
among the other 10 top-level menu items — cuts the flat menu from 14
items to 11 and keeps the related settings together.

* refactor(tray): make ShowWindow's action injectable; harden against a down settings service

Ports this session's earlier work (originally built against a
SettingsStore this branch predates) onto the client/server settings
architecture already on main (SettingsClient, ledgrrr-service):

- TrayCommand::ShowWindow's action is now an injected closure instead
  of a hardcoded spawn of a separate process, so the same run()/
  handle_command can be reused by host-tauri (which shows its own
  webview) without any dependency on a standalone binary.
- Collapsed 7 near-identical toggle handlers into negate()/
  apply_toggle() helpers.
- Any single command's error (most realistically: ledgrrr-service isn't
  running yet) is now logged and the loop continues, instead of
  propagating via `?` out of run() and killing the whole tray — and, via
  host-tauri's exit-on-return, the whole app — over a transient failure.
  Only an explicit Quit ends the loop.
- run()'s own initial settings load falls back to defaults on failure
  instead of erroring out before ever creating the tray icon at all —
  confirmed live: with no settings service running, the tray now still
  appears (previously it silently never initialized).
- NotificationBackend::PowerShell -> Native throughout.

* feat(tray): give host-tauri's Windows tray full parity with the old host-tray.exe

Replaces the minimal Show/Exit-only stub with the shared
tray::runtime::run(), injecting "show/unminimize/focus the Tauri
webview" as the window-show action. host-tauri now has the full
toggle/notification/toast menu that was previously only in the
standalone host-tray.exe, fixing the toast-notifications-never-wired-up
bug on the main app's tray.

Also: main.rs's window-visibility check and enable_tray check both
silently fell back to unhelpful defaults (or, for enable_tray, silently
skipped tray setup entirely) whenever the settings load failed —
exactly the case when ledgrrr-service isn't running yet. Both now log
and use an explicit, sensible default (visible; tray enabled) instead.

* chore(tray): retire host-tray.exe as a build target

Removes its [[bin]] entry now that host-tauri.exe has full feature
parity (previous commit). Source stays at src/bin/legacy/host-tray.rs
for reference; it no longer compiles as part of any normal build.

* chore(justfile): repoint host-tray recipes at host-tauri

host-tray no longer exists as a build target (previous commit).

* docs: port sysml-v2-parser spike findings from PR #187

PR #187 (spike/sysml-v2-parser-roundtrip) found holon-viz's SysmlV2Emitter
produced invalid SysML v2 (comment-swallowed closing brace, `block def`
instead of `part def`). Both bugs were independently fixed on main in PR
#183 (c106a5e) before this doc landed. Cherry-picks only the investigation
doc; the throwaway sysml-v2-parser-spike crate stays on the closed PR's
branch since its regression test now asserts stale (pre-fix) behavior.

* feat(status): add FoundryLocalStatus detection to LedgrrrStatus

* feat(foundry_install_plan): add winget install-assist plan-before-mutation flow

* feat(tauri): wire Foundry Local install-plan/action commands

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw

* docs(foundry_install_plan): fix misleading doc comment (Foundry Local, not this repo's own MSIX)

* feat(lifecycle): add FoundryLocalLifecycle enum and status derivation

* feat(lifecycle): export FoundryLocalLifecycle as an SCXML statechart

* fix(ledgerr-host): close registry-key test-isolation gap in 3 test files

This PR's own with_backend()/JsonFileBackend fix for the registry-leak
issue (SettingsStore::new's Windows registry backend ignores its path
arg and always targets one fixed production key) landed in
ledgrrr-settings and ledgerr-desktop-agent, but missed 3 test files in
ledgerr-host itself — the crate that actually owns the tray's real
SettingsStore usage:

- tests/settings_roundtrip.rs (5 tests)
- tests/settings_atomicity.rs (2 tests)
- tests/tray_wiring_smoke.rs (tray_enable_roundtrips_through_store)

On a real Windows box these all still called SettingsStore::new(tempdir)
directly, silently opening the one production registry key instead of an
isolated per-test backend — the exact corruption/cross-contamination bug
this PR claims to have fixed elsewhere, and in the worst case
(tray_enable_roundtrips_through_store) capable of writing enable_tray:
false into a real running host-tauri.exe's live settings from `cargo
test`. CI never caught it: the only Windows job runs a packaging smoke
test, not `cargo test`, and on Linux the registry backend is compiled out
entirely so these pass cleanly via the (correctly path-scoped)
JsonFileBackend fallback regardless.

Applied the same with_backend(path, JsonFileBackend::new(path)) pattern
already used in ledgrrr-settings/src/store.rs and
ledgerr-desktop-agent/src/settings_server.rs.

Verified: cargo test -p ledgerr-host --tests — 112 passed, 0 failed.

* docs: add Phase 4 and Phase 5 implementation plans

* fix: address ledgrrr#216 review findings

- Restore docs/superpowers/specs/2026-08-29-tray-tauri-integration-design.md
  and its companion plan doc from the superseded feat/windows-desktop-dogfood
  branch — Justfile referenced the spec doc, but it was never ported when
  this branch superseded that one. Retrieved verbatim, status line updated
  to note the branch handoff.
- Add invoke_reports_blocked_on_non_windows_even_when_approved, gated
  #[cfg(not(windows))] — exercises the untested !plan.executable_now
  branch in foundry_install_plan.rs::invoke() on non-Windows CI targets.
  Deliberately excluded from Windows builds: on Windows, executable_now
  is always true, so calling invoke(approved: true) there would reach the
  real winget-spawning code path as a side effect of running the test
  suite — the existing invoke_without_approval_never_launches test already
  covers the approval gate safely on every target.

---------

Co-authored-by: brianh <brianh@promptexecution.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.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