Skip to content

feat(grpc): carry the diagnostic code on the wire - #124

Merged
HuiJun merged 3 commits into
mainfrom
fix/diagnostic-wire-code
Sep 8, 2026
Merged

feat(grpc): carry the diagnostic code on the wire#124
HuiJun merged 3 commits into
mainfrom
fix/diagnostic-wire-code

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What and why

The gRPC/Connect Diagnostic message carried only severity, message and span, so a client that wanted to tell a choice point from a guard note, or an unresolved name from a type error, had to match message prefixes. The runtime already assigns a stable identifier, passes.Diagnostic.Code, and DiagnosticToProto, RunNoteDiagnosticsToProto and ParserDiagnosticToProto dropped it.

  • api/proto/sysml.proto: message Diagnostic gains string code = 4; (additive; make proto-breaking against origin/main passes). Go, Java, Python, TypeScript and Rust stubs plus the Rust descriptor are regenerated with the repo's pinned generators.
  • internal/grpc/convert.go: every conversion copies the code. Parser errors are coded syntax by ParserDiagnosticToProto, the same code the workspace and REPL give them (the parser codes only its warnings; see parser.Diagnostic.Code). export.go codes a Convert input's syntax errors the same way. internal/core/edit/validate.go now gives an edit's parse errors the syntax code too, as the other reporters do, so ApplyEdits refusals cross coded rather than empty. No runtime behaviour or code constant changes.
  • Clients: client/opensysml (Diagnostic.Code), Python (Diagnostic.code), Node (ModelDiagnostic.code), Rust (Diagnostic.code), Java (Diagnostic record gains code; ModelException serializes it and its serialVersionUID moves to 2). The Go and Java conformance renderers round-trip the field.
  • Docs: docs/reference/wire-contract.md gets a Diagnostic field table, code in the examples, and the execution section now says the two run notes are distinguished by choice-point / guard-unevaluable, not by prefix; docs/reference/api.md; Python, Node and Java READMEs.
  • Changelog fragment: changes/unreleased/diagnostic-wire-code.added.md.

A diagnostic whose producer assigned no code sends "" (omitted in JSON); the wire contract documents that.

  • Capability diagnostic_codes: a service that populates Diagnostic.code advertises it in ServerInfo.capabilities, so a client can tell a finding that was assigned no code from a service that carries no code at all. Every response family that carries diagnostics passes through Service.filterDiagnosticCapabilities, which clears the code when the capability is withheld and leaves everything else untouched. The proto comment, service-transports.md capability table, wire contract and client READMEs describe it; Go (CapabilityDiagnosticCodes), Python (CAPABILITY_DIAGNOSTIC_CODES), Node (CAPABILITY_DIAGNOSTIC_CODES) and Java (Capabilities.DIAGNOSTIC_CODES) name it (the Rust client takes capability names as strings).

How it was verified

New internal/grpc/diagnostic_code_test.go asserts the code round-trips on ParseFile, ParseSources, GetDiagnostics-style validation codes, Evaluate, ExecuteAction (choice-point, guard-unevaluable), ExecuteState (choice-point), RunAnalysis (choice-point), Convert and ApplyEdits; TestDiagnosticCodes_Capability runs each family against a current and a capability-withheld service and checks the withheld one reports the same diagnostics with the code cleared, while TestCapabilities covers the advertisement. One handwritten-wrapper test per client: Go (TestADiagnosticCarriesItsCode), Python (test_diagnostic_code), Node (client.test.ts), Rust (a_diagnostic_carries_its_code), Java (PublicTypesTest.diagnosticCarriesTheWireCode).

gofmt -l .                       (empty)
go build ./...                   ok
go vet ./...                     ok
go test ./...                    ok (all packages)
make proto-lint                  ✓ Proto lint passed
make proto-breaking              ✓ No breaking schema changes
python3 scripts/changelog.py check  ok
make docs-check                  ok
clients/python  pytest           749 passed, 56 skipped
clients/node    npm test/lint/typecheck   111 passed
clients/rust    cargo test       all suites pass (with OPENSYSML_GRPC_BINARY set)

The Java Maven suite could not be run locally (Maven Central answered 429 for every dependency fetch); it relies on the CI run.

Checklist

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

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

End-to-end check of the wire field against a rebuilt local sysml-grpc, via Connect JSON (curl) and the Python client. All 19 paired Connect/gRPC responses matched.

Scenario Observed
Syntax error error, code="syntax"
Unresolved type error, code="unresolved"
Unordered action tokens info, code="choice-point"
Unevaluable guard (not first evaluated) info, code="guard-unevaluable"
Two enabled state transitions for one event info, code="choice-point"

Controls: resolved type, safe guard, single transition, clean model and no-op action emitted no diagnostics; division by zero in the first evaluated guard stays an execution error. Python Diagnostic.code/repr carry the codes; an omitted code reads as "".

Not exercised live: no current producer emits an uncoded diagnostic, so the empty-code path was checked only through serialization.

@devin-ai-integration devin-ai-integration Bot reopened this Sep 8, 2026
@devin-ai-integration
devin-ai-integration Bot force-pushed the fix/diagnostic-wire-code branch from ca22d60 to f38f323 Compare September 8, 2026 13:30
@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review September 8, 2026 13:46
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits September 8, 2026 13:59
Diagnostic gains string code = 4, populated from passes.Diagnostic.Code in
every conversion so clients branch on a stable identifier instead of a
message prefix. The Go, Python, Node, Rust and Java clients expose it.

Co-Authored-By: jason.han <[email protected]>
A service that populates Diagnostic.code advertises diagnostic_codes, so a
client can tell an uncoded finding from a service that carries no code at
all. Without the capability every code crosses empty; every response family
that carries diagnostics is gated the same way. The Go, Python, Node and
Java clients name the capability.

Co-Authored-By: jason.han <[email protected]>
@devin-ai-integration
devin-ai-integration Bot force-pushed the fix/diagnostic-wire-code branch 2 times, most recently from f38f323 to 22b8e1a Compare September 8, 2026 14:04
@HuiJun
HuiJun merged commit e4c43bc into main Sep 8, 2026
12 checks passed
@HuiJun
HuiJun deleted the fix/diagnostic-wire-code branch September 8, 2026 18:28
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