diff --git a/.agents/skills/testing-pilot-corpora-gate/SKILL.md b/.agents/skills/testing-pilot-corpora-gate/SKILL.md index f9ee0145ce..1fe1064bfb 100644 --- a/.agents/skills/testing-pilot-corpora-gate/SKILL.md +++ b/.agents/skills/testing-pilot-corpora-gate/SKILL.md @@ -183,7 +183,7 @@ gate's own helpers are package-private but reusable (`pilotCorporaGate.files(t)` `actionlint`, `shellcheck`, `python3 scripts/check-doc-links.py`, `gofmt`, `go vet`, `go run -C tools ./cmd/pilot-diff` (validators pre-downloaded; ~4min, prints e.g. -the headline the committed baseline holds — `389 file(s), 359 fully agreeing; 45 agreed +the headline the committed baseline holds — `390 file(s), 360 fully agreeing; 45 agreed diagnostic(s), 44 only ours, 92 only the pilot's` at the `2026-08` pin, so read it from `docs/project/pilot-differential-baseline.json` rather than from this line) and `make lint` (staticcheck+gosec, ~2min) all work. There is **no** `yamllint` and **no** diff --git a/.agents/skills/testing-pilot-differential/SKILL.md b/.agents/skills/testing-pilot-differential/SKILL.md index 673089e10e..29bdab142b 100644 --- a/.agents/skills/testing-pilot-differential/SKILL.md +++ b/.agents/skills/testing-pilot-differential/SKILL.md @@ -24,7 +24,7 @@ GNU-format diagnostics **relative to `--root`**. Consequences for testing: - Measured at the `2026-08` pin after the view concern framing round retired the six view-body `frame` `syntax` rows (bare parameters already at their effective range `[0..*]`, so the adjudicated `Behaviors.kerml:14` multiplicity warning is gone while the `[1]` `RocketEquation` - inputs keep its warning at `delta-v-budget.sysml:93`): `389 file(s), 359 fully agreeing; 45 agreed, + inputs keep its warning at `delta-v-budget.sysml:93`): `390 file(s), 360 fully agreeing; 45 agreed, 44 only ours, 92 only the pilot's`, JSON totals `openSysMLDiagnostics 90 / pilotDiagnostics 138 / severityMismatch 1`; the two only-ours rows the multiplicity rule added are the expected `action-step-multiplicity-not-fixed` warnings on `training/18. Action Performance/Action Performance Example.sysml:10` and @@ -148,7 +148,7 @@ warning (the `[1]` `RocketEquation` inputs still produce the warning at `delta-v-budget.sysml:93`), is current: the action-step multiplicity rule adds the two expected `action-step-multiplicity-not-fixed` warnings on `takePhoto[*]` in the training corpus and `takePicture[*]` in `Camera Example/Camera.sysml`, and the view concern framing round retired the -six view-body `frame` `syntax` rows on `examples`; a live run gives `389 file(s), 359 fully +six view-body `frame` `syntax` rows on `examples`; a live run gives `390 file(s), 360 fully agreeing; 45 agreed, 44 only ours, 92 only the pilot's`, byte-identical to the committed baseline, and `docs/project/pilot-differential.md`'s "Results" table matches. The prior rebaseline, when the Legend of the Red Dragon example left for its own repository, gave diff --git a/.agents/skills/testing-pilot-execution-referee/SKILL.md b/.agents/skills/testing-pilot-execution-referee/SKILL.md index e0446dbd79..840e601209 100644 --- a/.agents/skills/testing-pilot-execution-referee/SKILL.md +++ b/.agents/skills/testing-pilot-execution-referee/SKILL.md @@ -150,7 +150,7 @@ pilot answers the representation's own. See `pilot-exec-diff: :: model no/such/model.sysml: stat : no such file or directory`. - **Additivity.** `go run -C tools ./cmd/pilot-diff` must still print the headline the - committed baseline holds (`389 file(s), 359 fully agreeing; 45 agreed + committed baseline holds (`390 file(s), 360 fully agreeing; 45 agreed diagnostic(s), 44 only ours, 92 only the pilot's` at the `2026-08` pin — read it from the baseline JSON, not from this line, since each fix round moves it) and `jq -S` diff clean against `docs/project/pilot-differential-baseline.json`; `git status --porcelain` diff --git a/.agents/skills/testing-pilot-xpect/SKILL.md b/.agents/skills/testing-pilot-xpect/SKILL.md index 60c0cd9db6..a88187967c 100644 --- a/.agents/skills/testing-pilot-xpect/SKILL.md +++ b/.agents/skills/testing-pilot-xpect/SKILL.md @@ -423,7 +423,7 @@ census in `w5c_census_test.go` is live two ways: perturb one pinned triple (e.g. ## Regression neighbour `go run -C tools ./cmd/pilot-diff` (~1m12s) must still print the headline the *committed* baseline holds — -at the `2026-08` pin that is `389 file(s), 359 fully agreeing; 45 agreed diagnostic(s), 44 +at the `2026-08` pin that is `390 file(s), 360 fully agreeing; 45 agreed diagnostic(s), 44 only ours, 92 only the pilot's`. Read the number out of `docs/project/pilot-differential-baseline.json` rather than trusting this line, since a landing fix round moves it. When the baseline is itself stale (it was at `19a3ce03`, holding 273 / 281 / 317), a diff --git a/README.md b/README.md index 4e0c44b7cb..b041c6de6b 100644 --- a/README.md +++ b/README.md @@ -338,11 +338,11 @@ The project is under active development, with the core infrastructure operationa **Measured against the pinned reference** (`PILOT_TAG=2026-08`, artifact `0.62.0`). Every number below is generated by `make docs-counts` from the committed baselines and gated; none of them is typed in by hand. -- **Corpus agreement:** 359 of 389 files agree diagnostic-by-diagnostic; 44 diagnostics are ours alone and 92 the reference's alone, and the first number must be read by root: the aggregate includes candidate conformance differences, intentional execution-scope warnings on reference corpora and diagnostics from our own examples ([differential](docs/project/pilot-differential.md), `go run -C tools ./cmd/pilot-diff`). +- **Corpus agreement:** 360 of 390 files agree diagnostic-by-diagnostic; 44 diagnostics are ours alone and 92 the reference's alone, and the first number must be read by root: the aggregate includes candidate conformance differences, intentional execution-scope warnings on reference corpora and diagnostics from our own examples ([differential](docs/project/pilot-differential.md), `go run -C tools ./cmd/pilot-diff`). - **Declared-diagnostic silence:** of the 512 declared `errors` rows in the reference's own Xpect suites, we report nothing for 0. 245 we report word-for-word; 248 wording-only and 7 location-only differences are agreement in substance and are not counted as gaps; 0 more we report as a warning and 2 elsewhere in the file ([Xpect oracle](docs/project/pilot-xpect.md), `go run -C tools ./cmd/pilot-xpect`). - **Scope agreement:** 230 of 230 declared scope assertions match exactly (same source). - **Permissiveness gaps:** of 313 invalid models we wrote ourselves, the reference rejects 4 that we accept by default, and 300 both reject; 4 further cases agree only when we are asked strictly. We authored every one of these cases ourselves, so the denominator measures the reach of our own corpus and not our conformance; agreement reached only under an opt-in strict mode is weaker evidence than agreement by default ([rejection oracle](docs/project/pilot-rejection.md), `go run -C tools ./cmd/pilot-reject`). -- **Declared errata:** the registry declares 12 defect(s) in the published reference material — 4 with a specification-derived correction, 8 documented without one, since no intended reading can be inferred ([OMG issues](docs/project/omg-issues.md), `tools/oracle/errata`). Every figure above is as published and stays the conformance statement; running the same oracles over the corrected text instead reports 360 of 389 files agreeing, 43 diagnostics ours alone and 92 the reference's alone, 0 declared rows we are silent on, and 0 of 313 authored cases the reference alone rejects. The corrected figures are diagnostic only: an erratum never reclassifies a divergence category, and the published corpus is never edited. +- **Declared errata:** the registry declares 12 defect(s) in the published reference material — 4 with a specification-derived correction, 8 documented without one, since no intended reading can be inferred ([OMG issues](docs/project/omg-issues.md), `tools/oracle/errata`). Every figure above is as published and stays the conformance statement; running the same oracles over the corrected text instead reports 361 of 390 files agreeing, 43 diagnostics ours alone and 92 the reference's alone, 0 declared rows we are silent on, and 0 of 313 authored cases the reference alone rejects. The corrected figures are diagnostic only: an erratum never reclassifies a divergence category, and the published corpus is never edited. - **Self-assessed surface:** the action, state-machine and classifier-behavior rows have no external referee at all — the four refereed figures above cannot see them, because the pinned artifact evaluates expressions but executes neither actions nor state machines. [Spec compliance](docs/project/spec-compliance.md) counts them. What these numbers cannot show: the OMG corpora are demonstrations rather than an official conformance suite; the differential is one-directional, comparing the diagnostics the two implementations report on the same files; the Xpect suites are the pilot authors' test intent rather than a certification oracle; and none of these is a percentage of the specification — no global compliance figure is claimed anywhere. @@ -354,7 +354,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Test coverage:** top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation), behind golden ASTs, negatives, execution conformance cases, golden traces, runtime robustness cases and gRPC conformance and robustness cases. The figures are counted from the tree when the documentation site is built into the test inventory of [spec compliance](docs/project/spec-compliance.md), never committed, so a branch adding a test does not rewrite this page. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 110/110 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 16 non-normative OpenSysML extensions: `OpenSysML Libraries/AnalysisRecords.sysml`, `DiagramLayout.sysml`, `DocumentQueries.sysml`, `IdentityMetadata.sysml`, `MOSA.sysml`, `MigrationMetadata.sysml`, `OOSEM.sysml`, `OpenSysMLMathFunctions.kerml`, `OpenSysMLRenderings.sysml`, `RandomFunctions.kerml`, `Simulation.sysml`, `StateActivity.kerml`, `StateMachines.sysml`, `StateSpaceIntegration.sysml`, `Stochastic.sysml` and `SysMLValidation.sysml`. Conformance verified by [stdlib_conformance_test.go](internal/workspace/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (every conformance case passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). -**Reference differential:** 389 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 359 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run -C tools ./cmd/pilot-diff`. +**Reference differential:** 390 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 360 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run -C tools ./cmd/pilot-diff`. **Rejection oracle:** the reverse direction — do we reject what the reference rejects? 313 hand-written invalid models validated by both implementations, 304 rejected by both, 0 the pinned pilot rejects and we accept; the remainder only we reject — the control-node succession rules the pinned pilot leaves unimplemented and a non-Boolean succession guard it accepts once the standard library types it — and every permissiveness gap is enumerated with a reproducer and likely root cause in [the rejection oracle](docs/project/pilot-rejection.md), reproducible with `go run -C tools ./cmd/pilot-reject`. We wrote every case, so the count measures our coverage of the rejection surface, not our conformance — a sample, not a proof. **Training examples:** 100/100 files report no semantic errors, gated by `tests/corpus/testdata/training_examples_expected.txt`; the gate does not count execution-scope warnings. Download with `./scripts/download-training-examples.sh` (from the [OMG training directory](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/sysml/src/training)). See [training examples](docs/project/training-examples.md) for analysis. **Semantic layer:** a complete implementation of runtime operators, feature chains and validation rules. See [examples/semantic-layer/](examples/semantic-layer/) for a full demonstration. diff --git a/api/proto/sysml.pb.go b/api/proto/sysml.pb.go index f9ac777a28..923bdfa538 100644 --- a/api/proto/sysml.pb.go +++ b/api/proto/sysml.pb.go @@ -10968,7 +10968,7 @@ func (x *DocumentState) GetEnclosing() []string { // session's trace, in the order the run made it. type DocumentEvent struct { state protoimpl.MessageState `protogen:"open.v1"` - // "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard". + // "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate". Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` // The clock's instant when the record was made: a quantity in the clock's // unit when the library reduces one, else a bare real of clock units. diff --git a/api/proto/sysml.proto b/api/proto/sysml.proto index 29646ff204..00fd3a4a37 100644 --- a/api/proto/sysml.proto +++ b/api/proto/sysml.proto @@ -2213,7 +2213,7 @@ message DocumentState { // DocumentEvent is one row an `Events` query answered: one record of the // session's trace, in the order the run made it. message DocumentEvent { - // "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard". + // "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate". string kind = 1; // The clock's instant when the record was made: a quantity in the clock's // unit when the library reduces one, else a bare real of clock units. diff --git a/changes/unreleased/run-trace-renderings.added.md b/changes/unreleased/run-trace-renderings.added.md new file mode 100644 index 0000000000..715cf0c51d --- /dev/null +++ b/changes/unreleased/run-trace-renderings.added.md @@ -0,0 +1,2 @@ +- **Render recorded runs as timelines and message sequences.** The CLI and REPL can write a behavior's state occupancy and ordered messages as text, Mermaid or PlantUML without adding model-view vocabulary. Source links connect sequence participants to their type declarations and PlantUML timeline lanes and single-state spans to their declarations. +- A terminated machine's `terminate` event also appears in state-run traces. diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java index 0b5022bf6d..79868c4593 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/DocumentValue.java @@ -264,7 +264,7 @@ record DocumentState( * binding one is refused. * * @param kind {@code "accept"}, {@code "send"}, {@code "transition"}, {@code "entry"}, {@code - * "exit"}, {@code "do"}, {@code "choice"} or {@code "guard"} + * "exit"}, {@code "do"}, {@code "choice"}, {@code "guard"} or {@code "terminate"} * @param time the instant the record was written at, in the runtime clock's unit — a {@link * QuantityValue} when the clock carries a unit, a plain number otherwise * @param text the record as the trace prints it diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEvent.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEvent.java index 1bb66066e7..aab8b03cf0 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEvent.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEvent.java @@ -66,7 +66,7 @@ private DocumentEvent() { private volatile java.lang.Object kind_ = ""; /** *
-   * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+   * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
    * 
* * string kind = 1 [json_name = "kind"]; @@ -87,7 +87,7 @@ public java.lang.String getKind() { } /** *
-   * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+   * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
    * 
* * string kind = 1 [json_name = "kind"]; @@ -1321,7 +1321,7 @@ public Builder mergeFrom( private java.lang.Object kind_ = ""; /** *
-     * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+     * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
      * 
* * string kind = 1 [json_name = "kind"]; @@ -1341,7 +1341,7 @@ public java.lang.String getKind() { } /** *
-     * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+     * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
      * 
* * string kind = 1 [json_name = "kind"]; @@ -1362,7 +1362,7 @@ public java.lang.String getKind() { } /** *
-     * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+     * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
      * 
* * string kind = 1 [json_name = "kind"]; @@ -1379,7 +1379,7 @@ public Builder setKind( } /** *
-     * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+     * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
      * 
* * string kind = 1 [json_name = "kind"]; @@ -1393,7 +1393,7 @@ public Builder clearKind() { } /** *
-     * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+     * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
      * 
* * string kind = 1 [json_name = "kind"]; diff --git a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEventOrBuilder.java b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEventOrBuilder.java index 12d84f0aa0..14e3f1cb95 100644 --- a/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEventOrBuilder.java +++ b/client/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/DocumentEventOrBuilder.java @@ -12,7 +12,7 @@ public interface DocumentEventOrBuilder extends /** *
-   * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+   * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
    * 
* * string kind = 1 [json_name = "kind"]; @@ -21,7 +21,7 @@ public interface DocumentEventOrBuilder extends java.lang.String getKind(); /** *
-   * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard".
+   * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate".
    * 
* * string kind = 1 [json_name = "kind"]; diff --git a/client/node/src/core/document.ts b/client/node/src/core/document.ts index 5b2f63330c..35f69a3c90 100644 --- a/client/node/src/core/document.ts +++ b/client/node/src/core/document.ts @@ -190,7 +190,7 @@ export class DocumentState { /** A row an `Events` query answered: one record of a session's trace. Answered only. */ export class DocumentEvent { - /** "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard". */ + /** "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate". */ readonly kind: string; /** The instant the record was written at: a quantity, an int or a real. */ readonly time: DocumentValue; diff --git a/client/node/src/generated/sysml_pb.ts b/client/node/src/generated/sysml_pb.ts index d943d6d93c..483b377fbb 100644 --- a/client/node/src/generated/sysml_pb.ts +++ b/client/node/src/generated/sysml_pb.ts @@ -6161,7 +6161,7 @@ export const DocumentStateSchema: GenMessage = /*@__PURE__*/ */ export type DocumentEvent = Message<"sysml.DocumentEvent"> & { /** - * "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard". + * "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate". * * @generated from field: string kind = 1; */ diff --git a/client/opensysml/documents.go b/client/opensysml/documents.go index 3bcff3c860..9dba3cfdf1 100644 --- a/client/opensysml/documents.go +++ b/client/opensysml/documents.go @@ -100,8 +100,8 @@ type DocumentState struct { // DocumentEvent is a row an `Events` query answered: one record of a session's // trace. It is answered, never bound. type DocumentEvent struct { - // Kind is "accept", "send", "transition", "entry", "exit", "do", "choice" - // or "guard". + // Kind is "accept", "send", "transition", "entry", "exit", "do", "choice", + // "guard" or "terminate". Kind string // Time is the run's clock when the record was made: a Quantity when the // clock carries a unit, a Real otherwise. diff --git a/client/python/opensysml/document.py b/client/python/opensysml/document.py index 5a133fcd74..f51aa9b2c6 100644 --- a/client/python/opensysml/document.py +++ b/client/python/opensysml/document.py @@ -159,7 +159,7 @@ class DocumentEvent: Attributes: kind: ``"accept"``, ``"send"``, ``"transition"``, ``"entry"``, - ``"exit"``, ``"do"``, ``"choice"`` or ``"guard"`` + ``"exit"``, ``"do"``, ``"choice"``, ``"guard"`` or ``"terminate"`` time: The instant the record was written at, in the runtime clock's unit — a :class:`~opensysml.values.Quantity` when the clock carries one, a plain number otherwise diff --git a/client/rust/conformance/sysml.descriptor.binpb b/client/rust/conformance/sysml.descriptor.binpb index 5d35d17b23..d7e8c23e92 100644 Binary files a/client/rust/conformance/sysml.descriptor.binpb and b/client/rust/conformance/sysml.descriptor.binpb differ diff --git a/client/rust/opensysml/src/proto/sysml/sysml.rs b/client/rust/opensysml/src/proto/sysml/sysml.rs index cb17005a5c..186cfcbc59 100644 --- a/client/rust/opensysml/src/proto/sysml/sysml.rs +++ b/client/rust/opensysml/src/proto/sysml/sysml.rs @@ -2759,7 +2759,7 @@ pub struct DocumentState { /// session's trace, in the order the run made it. #[derive(Clone, PartialEq, ::prost::Message)] pub struct DocumentEvent { - /// "accept", "send", "transition", "entry", "exit", "do", "choice" or "guard". + /// "accept", "send", "transition", "entry", "exit", "do", "choice", "guard" or "terminate". #[prost(string, tag="1")] pub kind: ::prost::alloc::string::String, /// The clock's instant when the record was made: a quantity in the clock's diff --git a/cmd/sysml/check.go b/cmd/sysml/check.go index e9f38c1cf5..8d285b9d1c 100644 --- a/cmd/sysml/check.go +++ b/cmd/sysml/check.go @@ -602,6 +602,9 @@ func runChecks(files []string, exprs []string, c checks) int { } sess := newSession() + if len(renderRuns) > 0 { + sess.SetRecording(true) + } sess.SetCheckDiverge(c.checker.diverge) sess.SetCheckProperties(c.checker.properties) sess.SetCheckInputs(c.checker.inputs) @@ -789,7 +792,7 @@ func runChecks(files []string, exprs []string, c checks) int { rep.verdict(v) } c.runQueries(sess, rep) - return rep.finish() + return finishRunCheck(rep, sess) } for _, value := range c.actions { name, performer := repl.SplitBehavior(value) @@ -805,7 +808,7 @@ func runChecks(files []string, exprs []string, c checks) int { } c.runQueries(sess, rep) - return rep.finish() + return finishRunCheck(rep, sess) } // runQueries executes each -run-query after the behaviors named have run, so a diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go index 2c172343b6..685609f7fe 100644 --- a/cmd/sysml/main.go +++ b/cmd/sysml/main.go @@ -106,6 +106,7 @@ var ( debugMode bool quietMode bool traceMode bool + renderRuns stringSlice schedule schedulePolicy listEngines bool probeEngines bool @@ -395,7 +396,18 @@ func runCLI() int { // Get positional arguments (files to load) args := flag.Args() - if renderForm != "" && renderView == "" && renderAllDir == "" { + if flagGiven("render-run") { + if message := runRenderModeMisuse(); message != "" { + fmt.Fprintln(os.Stderr, errPrefix, message) + return 2 + } + if _, err := runRenderTargetsFromFlags(); err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + } + + if renderForm != "" && renderView == "" && renderAllDir == "" && len(renderRuns) == 0 { fmt.Fprintln(os.Stderr, "sysml: -render-form is the form -render or -render-all writes; name the view to render with -render or a directory with -render-all") return 2 } @@ -407,8 +419,8 @@ func runCLI() int { fmt.Fprintln(os.Stderr, "sysml: -render-overlay is what -render or -render-all draws over a requirement rendering's structure; name the view to render with -render or a directory with -render-all") return 2 } - if renderLink != "" && renderView == "" && renderAllDir == "" && renderDoc == "" && renderDocsDir == "" { - fmt.Fprintln(os.Stderr, "sysml: -render-link links rendered elements to their source; name what to render with -render, -render-all, -render-document or -render-documents") + if renderLink != "" && renderView == "" && renderAllDir == "" && renderDoc == "" && renderDocsDir == "" && len(renderRuns) == 0 { + fmt.Fprintln(os.Stderr, "sysml: -render-link links rendered elements to their source; name what to render with -render, -render-all, -render-document, -render-documents or -render-run") return 2 } if renderPorts != "" && renderView == "" && renderAllDir == "" { diff --git a/cmd/sysml/render_run.go b/cmd/sysml/render_run.go new file mode 100644 index 0000000000..a51cf4cc8f --- /dev/null +++ b/cmd/sysml/render_run.go @@ -0,0 +1,177 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/exec/runtrace" + "github.com/Open-MBEE/OpenSysML/internal/frontend/repl" + "github.com/Open-MBEE/OpenSysML/internal/ir/view" +) + +type runRenderTarget struct { + kind runtrace.Kind + path string + form view.Form +} + +func runRenderModeMisuse() string { + switch { + case len(renderRuns) == 0: + return "-render-run needs a value of the form =" + case modelChecks.jsonOut && renderRunWritesStdout(): + return "-render-run cannot write to stdout with -json; name a file for the rendering" + case flagGiven("compare-results"): + return "-render-run cannot be combined with -compare-results" + case len(modelChecks.actions) == 0 && len(modelChecks.states) == 0 && !modelChecks.advance.given: + return "-render-run needs -action, -state or -advance to record a behavior run" + case schedule.text == "explore": + return "-render-run cannot render -schedule explore; render one declared or replayed run" + case engine.text == "check" || engine.text == "smt" || engine.text == "all": + return "-render-run cannot render -engine check, smt or all; run one behavior schedule" + case modelChecks.runs.given || flagGiven("runs"): + return "-render-run cannot be combined with -runs" + case len(modelChecks.sweeps) > 0 || modelChecks.samples.given || flagGiven("sweep") || flagGiven("samples"): + return "-render-run cannot be combined with -sweep or -samples" + case len(modelChecks.records) > 0 || flagGiven("record-run"): + return "-render-run cannot be combined with -record-run" + case flagGiven("render") || renderView != "" || flagGiven("render-all") || renderAllDir != "": + return "-render-run cannot be combined with -render or -render-all" + case flagGiven("render-document") || renderDoc != "" || flagGiven("render-documents") || renderDocsDir != "": + return "-render-run cannot be combined with -render-document or -render-documents" + case flagGiven("convert") || convertFormat != "" || flagGiven("migrate") || migrateFormat != "": + return "-render-run cannot be combined with -convert or -migrate" + case flagGiven("query") || queryText != "": + return "-render-run cannot be combined with -query" + case outputPath != "": + return "-render-run names each output path; do not combine it with -output" + case renderPalette != "" || renderStyle != "" || renderPorts != "" || renderUnplaced != "": + return "-render-palette, -render-style, -render-ports and -render-unplaced apply to model renderings, not -render-run" + } + return "" +} + +func renderRunWritesStdout() bool { + for _, value := range renderRuns { + _, path, ok := strings.Cut(value, "=") + if ok && path == "-" { + return true + } + } + return false +} + +func runRenderTargetsFromFlags() ([]runRenderTarget, error) { + targets := make([]runRenderTarget, 0, len(renderRuns)) + for _, value := range renderRuns { + kindText, path, ok := strings.Cut(value, "=") + if !ok || kindText == "" || path == "" { + return nil, fmt.Errorf("-render-run takes =, not %q", value) + } + kind, ok := runtrace.ParseKind(kindText) + if !ok { + return nil, fmt.Errorf("unknown -render-run kind %q; want %s", kindText, strings.Join(runRenderKindNames(), ", ")) + } + form := view.Form(renderForm) + if renderForm == "" { + if path == "-" { + form = view.FormText + } else { + var found bool + form, found = runRenderFormFromPath(path) + if !found { + return nil, fmt.Errorf("-render-run path %q has no recognized form extension; use -render-form text, mermaid or plantuml", path) + } + } + } else if !slices.Contains(view.Forms(), form) { + return nil, fmt.Errorf("unknown rendering form %q; -render-form takes %s", renderForm, formList()) + } + probe := &view.Rendering{Kind: runRenderViewKind(kind), Run: true} + if _, err := probe.WriteWith(form, view.Options{Links: view.Links{Template: renderLink}}); err != nil { + return nil, fmt.Errorf("-render-run %s: %w", kind, err) + } + targets = append(targets, runRenderTarget{kind: kind, path: path, form: form}) + } + return targets, nil +} + +func runRenderKindNames() []string { + kinds := runtrace.Kinds() + names := make([]string, len(kinds)) + for i, kind := range kinds { + names[i] = string(kind) + } + return names +} + +func runRenderViewKind(kind runtrace.Kind) view.Kind { + if kind == runtrace.KindTimeline { + return view.KindTimeline + } + return view.KindSequence +} + +func runRenderFormFromPath(path string) (view.Form, bool) { + switch strings.ToLower(filepath.Ext(path)) { + case ".mmd", ".mermaid": + return view.FormMermaid, true + case ".puml", ".plantuml": + return view.FormPlantUML, true + case ".txt": + return view.FormText, true + case ".dot", ".gv": + return view.FormDot, true + default: + return "", false + } +} + +func finishRunCheck(rep *reporter, sess *repl.Session) int { + status := rep.finish() + if len(renderRuns) == 0 { + return status + } + targets, err := runRenderTargetsFromFlags() + if err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + var sites view.Sites + if renderLink != "" { + sites, err = sess.ViewSites() + if err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + } + for _, target := range targets { + rendering, err := sess.RunTraceRendering(target.kind) + if err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + artifact, err := rendering.WriteWith(target.form, view.Options{ + Width: artifactWidth(target.path, terminalWidth()), + Links: view.Links{Template: renderLink, Sites: sites}, + }) + if err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + name := "run " + string(target.kind) + reportRenderNoticesFrom(rendering, name) + if target.path == "-" { + if err := writeArtifact(artifact, target.form); err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + } else if err := writeArtifactFile(target.path, artifact, target.form); err != nil { + fmt.Fprintln(os.Stderr, errPrefix, err) + return 2 + } + } + return status +} diff --git a/cmd/sysml/render_run_test.go b/cmd/sysml/render_run_test.go new file mode 100644 index 0000000000..b82245bf39 --- /dev/null +++ b/cmd/sysml/render_run_test.go @@ -0,0 +1,246 @@ +package main + +import ( + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +func TestRenderRunExampleMatchesGoldens(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "examples", "run-timeline", "run-timeline.sysml")) + if err != nil { + t.Fatal(err) + } + binary := buildCLI(t) + dir := t.TempDir() + outputs := map[string]string{ + "example-timeline.text.golden": filepath.Join(dir, "timeline.txt"), + "example-timeline.mermaid.golden": filepath.Join(dir, "timeline.mmd"), + "example-timeline.plantuml.golden": filepath.Join(dir, "timeline.puml"), + "example-sequence.text.golden": filepath.Join(dir, "sequence.txt"), + "example-sequence.mermaid.golden": filepath.Join(dir, "sequence.mmd"), + "example-sequence.plantuml.golden": filepath.Join(dir, "sequence.puml"), + } + args := []string{ + "-instantiate", "RunTimeline::mission", + "-state", "RunTimeline::Controller::modes RunTimeline::mission.controller", + "-state", "RunTimeline::Instrument::modes RunTimeline::mission.instrument", + "-advance", "6", + } + keys := make([]string, 0, len(outputs)) + for golden := range outputs { + keys = append(keys, golden) + } + sort.Strings(keys) + for _, golden := range keys { + path := outputs[golden] + kind := "timeline" + if strings.Contains(golden, "sequence") { + kind = "sequence" + } + args = append(args, "-render-run", kind+"="+path) + } + got := check(t, binary, string(data), args...) + if got.status != 0 { + t.Fatalf("exit status = %d\n%s", got.status, got.output()) + } + for _, golden := range keys { + path := outputs[golden] + actual, err := os.ReadFile(path) // #nosec G304 -- the path is created by the test. + if err != nil { + t.Fatal(err) + } + goldenPath := filepath.Join("..", "..", "internal", "exec", "runtrace", "testdata", golden) + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatal(err) + } + if string(actual) != string(want) { + t.Errorf("%s differs from %s", path, goldenPath) + } + } +} + +func TestRenderRunWritesEachFormAfterTheVerdict(t *testing.T) { + binary := buildCLI(t) + dir := t.TempDir() + timelinePath := filepath.Join(dir, "timeline.txt") + sequencePath := filepath.Join(dir, "sequence.mmd") + got := check(t, binary, behaviorModel, + "-state", "Mission::Cycle", "-advance", "15", + "-render-run", "timeline="+timelinePath, + "-render-run", "sequence="+sequencePath, + ) + if got.status != 0 { + t.Fatalf("exit status = %d\n%s", got.status, got.output()) + } + if !strings.Contains(got.stdout, `Started state machine executor for "Mission::Cycle"`) { + t.Errorf("the run verdict is missing from stdout:\n%s", got.stdout) + } + if strings.Contains(got.stdout, "[trace]") { + t.Errorf("silent recording printed trace lines:\n%s", got.stdout) + } + timeline, err := os.ReadFile(timelinePath) // #nosec G304 -- this path is created by the test. + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(timeline), "run - timeline rendering") || + !strings.Contains(string(timeline), "working") { + t.Errorf("timeline output is missing the run's state occupancy:\n%s", timeline) + } + sequence, err := os.ReadFile(sequencePath) // #nosec G304 -- this path is created by the test. + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(sequence), "sequenceDiagram") || + !strings.Contains(string(sequence), "the run recorded no message") { + t.Errorf("sequence output is missing its empty-run rendering:\n%s", sequence) + } + if !strings.Contains(got.stderr, "wrote "+timelinePath) || !strings.Contains(got.stderr, "wrote "+sequencePath) { + t.Errorf("stderr does not report both artifacts:\n%s", got.stderr) + } +} + +func TestRenderRunLinksTimelineAndSequence(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "examples", "run-timeline", "run-timeline.sysml")) + if err != nil { + t.Fatal(err) + } + binary := buildCLI(t) + dir := t.TempDir() + timelinePath := filepath.Join(dir, "timeline.puml") + sequencePath := filepath.Join(dir, "sequence.mmd") + got := check(t, binary, string(data), + "-instantiate", "RunTimeline::mission", + "-state", "RunTimeline::Controller::modes RunTimeline::mission.controller", + "-state", "RunTimeline::Instrument::modes RunTimeline::mission.instrument", + "-advance", "6", + "-render-link", "https://example.test/src/{file}#L{line}", + "-render-run", "timeline="+timelinePath, + "-render-run", "sequence="+sequencePath, + ) + if got.status != 0 { + t.Fatalf("exit status = %d\n%s", got.status, got.output()) + } + timeline, err := os.ReadFile(timelinePath) // #nosec G304 -- this path is created by the test. + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(timeline), `[[https://example.test/src/`) || + !strings.Contains(string(timeline), `#L`) || + !strings.Contains(string(timeline), `RunTimeline::mission.controller.modes`) { + t.Errorf("linked timeline is missing source links or visible labels:\n%s", timeline) + } + sequence, err := os.ReadFile(sequencePath) // #nosec G304 -- this path is created by the test. + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(sequence), "link n0: Source @ https://example.test/src/") || + !strings.Contains(string(sequence), "RunTimeline::mission.controller") { + t.Errorf("linked sequence is missing participant links or visible labels:\n%s", sequence) + } +} + +func TestRenderRunRejectsInvalidLinkBeforeWriting(t *testing.T) { + binary := buildCLI(t) + path := filepath.Join(t.TempDir(), "timeline.puml") + got := check(t, binary, behaviorModel, + "-state", "Mission::Cycle", "-advance", "1", + "-render-link", "https://example.test/{unknown}", + "-render-run", "timeline="+path, + ) + if got.status != 2 || !strings.Contains(got.stderr, "unknown link template placeholder {unknown}") { + t.Fatalf("status = %d, want invalid-link refusal:\n%s", got.status, got.output()) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("rendering output path exists after invalid-link refusal: stat error = %v", err) + } +} + +func TestRenderRunWritesStdoutWithoutLosingTheVerdict(t *testing.T) { + binary := buildCLI(t) + got := check(t, binary, behaviorModel, + "-state", "Mission::Cycle", "-advance", "1", + "-render-run", "timeline=-", + ) + if got.status != 0 { + t.Fatalf("exit status = %d\n%s", got.status, got.output()) + } + for _, want := range []string{`Started state machine executor for "Mission::Cycle"`, "run - timeline rendering", "waiting"} { + if !strings.Contains(got.stdout, want) { + t.Errorf("stdout is missing %q:\n%s", want, got.stdout) + } + } + if strings.Contains(got.stdout, "[trace]") { + t.Errorf("silent recording printed trace lines:\n%s", got.stdout) + } +} + +func TestRenderRunRefusesJSONStdoutAndCompareResults(t *testing.T) { + binary := buildCLI(t) + dir := t.TempDir() + cases := []struct { + name string + args []string + want string + path string + }{ + { + name: "JSON stdout", + args: []string{ + "-state", "Mission::Cycle", "-advance", "1", "-json", + "-render-run", "timeline=" + filepath.Join(dir, "timeline.txt"), + "-render-run", "sequence=-", + }, + want: "-render-run cannot write to stdout with -json; name a file for the rendering", + path: filepath.Join(dir, "timeline.txt"), + }, + { + name: "compare results", + args: []string{ + "-state", "Mission::Cycle", "-advance", "1", + "-compare-results", filepath.Join(dir, "missing.json"), + "-render-run", "timeline=" + filepath.Join(dir, "timeline.txt"), + }, + want: "-render-run cannot be combined with -compare-results", + path: filepath.Join(dir, "timeline.txt"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := check(t, binary, behaviorModel, tc.args...) + if got.status != 2 || !strings.Contains(got.stderr, tc.want) { + t.Errorf("status = %d, want 2 with %q:\n%s", got.status, tc.want, got.output()) + } + if _, err := os.Stat(tc.path); !os.IsNotExist(err) { + t.Errorf("rendering output path exists after refusal: stat error = %v", err) + } + }) + } +} + +func TestRenderRunRejectsUnsupportedModesAndForms(t *testing.T) { + binary := buildCLI(t) + cases := []struct { + name string + args []string + want string + }{ + {"no behavior", []string{"-render-run", "timeline=run.txt"}, "needs -action, -state or -advance"}, + {"unknown kind", []string{"-state", "Mission::Cycle", "-render-run", "other=run.txt"}, "unknown -render-run kind"}, + {"unknown extension", []string{"-state", "Mission::Cycle", "-render-run", "timeline=run.svg"}, "use -render-form"}, + {"dot refused", []string{"-state", "Mission::Cycle", "-render-run", "timeline=run.dot"}, "not written as dot"}, + {"model rendering conflict", []string{"-state", "Mission::Cycle", "-render-run", "timeline=run.txt", "-render", "Demo::view"}, "cannot be combined with -render"}, + {"query conflict", []string{"-state", "Mission::Cycle", "-render-run", "timeline=run.txt", "-query", "sysml:name=*"}, "cannot be combined with -query"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := check(t, binary, behaviorModel, tc.args...) + if got.status != 2 || !strings.Contains(got.stderr, tc.want) { + t.Errorf("status = %d, want 2 with %q:\n%s", got.status, tc.want, got.output()) + } + }) + } +} diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index 36a9161aeb..a050f40e25 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -682,7 +682,7 @@ func registerFlags(fs *flag.FlagSet) { fs.StringVar(&renderAllDir, "render-all", "", "Render every declared view into this directory") fs.StringVar(&renderForm, "render-form", "", "Form -render or -render-all writes: text, mermaid, markdown, dot, plantuml, d2, csv or tsv (csv and tsv for a table or matrix); D2 writes tree, interconnection, state, action, sequence, requirement, definition and package renderings, not case or mixed; default from the destination for -render, each kind's machine form for -render-all") fs.StringVar(&renderPalette, "render-palette", "", "Palette the dot, mermaid, plantuml or d2 form fills nodes from, by keyword family: okabe-ito, tol-bright, tol-muted, tol-light, brewer-set2, brewer-dark2, viridis or cividis; default black and white") - fs.StringVar(&renderLink, "render-link", "", "Link template for rendered elements: {file} is the path as loaded; use absolute paths for vscode:// or file:// links. Placeholders: {file}, {line}, {col}, {qname}, {id}") + fs.StringVar(&renderLink, "render-link", "", "Source link template for -render, -render-all, -render-document, -render-documents and -render-run. Placeholders: {file}, {line}, {col}, {qname}, {id}; {file} is the path as loaded (use absolute paths for vscode:// or file:// links)") fs.StringVar(&renderStyle, "render-style", "", "Drawing style of the dot or mermaid form: pilot (default), the Pilot visualizer's black and white, or cameo, the look of Cameo Systems Modeler; applies to -render, -render-all and document diagrams") fs.StringVar(&renderPorts, "render-ports", "", "How much of a part's ports -render or -render-all draws on an interconnection or mixed rendering: minimal (default), the ports its interconnection edges end at, each a small square on the part's border named beside it, or full, every port, labelled name : Type") fs.StringVar(&renderOverlay, "render-overlay", "", "What -render or -render-all draws over a requirement rendering's structure: verdicts runs the verification cases verifying each requirement and colours and labels it by their verdicts; default none, a purely structural drawing") @@ -697,6 +697,7 @@ func registerFlags(fs *flag.FlagSet) { fs.BoolVar(&debugMode, "debug", false, "Report every diagnostic over the whole session buffer, with the pass that produced it") fs.BoolVar(&quietMode, "quiet", false, "Report errors only, suppressing warnings") fs.BoolVar(&traceMode, "trace", false, "Report each execution step: expression evaluation, calc invocation, action tokens, state transitions") + fs.Var(&renderRuns, "render-run", "Render the trace of a run as timeline or sequence into path (`-` is stdout); form comes from -render-form or the path extension") fs.BoolVar(&memStats, "memstats", false, "Report on stderr what the run cost: wall time, memory allocated, memory taken from the OS") fs.Var(&deprecatedFlag{instead: "-to has been replaced by -convert, as `sysml model.sysml -convert ttl`"}, "to", "Replaced by -convert, which names the output format") @@ -858,6 +859,7 @@ func optionGroups() []usage.OptionGroup { usage.Opt("debug", ""), usage.Opt("quiet", ""), usage.Opt("trace", ""), + usage.Opt("render-run", "="), usage.Opt("cpuprofile", fileArg), usage.Opt("memprofile", fileArg), usage.Opt("memstats", ""), diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index 449cdbc9d1..e2fb38afe0 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -841,11 +841,11 @@ Every behavioral feature must have: **Measured against the pinned reference** (`PILOT_TAG=2026-08`, artifact `0.62.0`). Every number below is generated by `make docs-counts` from the committed baselines and gated; none of them is typed in by hand. -- **Corpus agreement:** 359 of 389 files agree diagnostic-by-diagnostic; 44 diagnostics are ours alone and 92 the reference's alone, and the first number must be read by root: the aggregate includes candidate conformance differences, intentional execution-scope warnings on reference corpora and diagnostics from our own examples ([differential](../project/pilot-differential.md), `go run -C tools ./cmd/pilot-diff`). +- **Corpus agreement:** 360 of 390 files agree diagnostic-by-diagnostic; 44 diagnostics are ours alone and 92 the reference's alone, and the first number must be read by root: the aggregate includes candidate conformance differences, intentional execution-scope warnings on reference corpora and diagnostics from our own examples ([differential](../project/pilot-differential.md), `go run -C tools ./cmd/pilot-diff`). - **Declared-diagnostic silence:** of the 512 declared `errors` rows in the reference's own Xpect suites, we report nothing for 0. 245 we report word-for-word; 248 wording-only and 7 location-only differences are agreement in substance and are not counted as gaps; 0 more we report as a warning and 2 elsewhere in the file ([Xpect oracle](../project/pilot-xpect.md), `go run -C tools ./cmd/pilot-xpect`). - **Scope agreement:** 230 of 230 declared scope assertions match exactly (same source). - **Permissiveness gaps:** of 313 invalid models we wrote ourselves, the reference rejects 4 that we accept by default, and 300 both reject; 4 further cases agree only when we are asked strictly. We authored every one of these cases ourselves, so the denominator measures the reach of our own corpus and not our conformance; agreement reached only under an opt-in strict mode is weaker evidence than agreement by default ([rejection oracle](../project/pilot-rejection.md), `go run -C tools ./cmd/pilot-reject`). -- **Declared errata:** the registry declares 12 defect(s) in the published reference material — 4 with a specification-derived correction, 8 documented without one, since no intended reading can be inferred ([OMG issues](../project/omg-issues.md), `tools/oracle/errata`). Every figure above is as published and stays the conformance statement; running the same oracles over the corrected text instead reports 360 of 389 files agreeing, 43 diagnostics ours alone and 92 the reference's alone, 0 declared rows we are silent on, and 0 of 313 authored cases the reference alone rejects. The corrected figures are diagnostic only: an erratum never reclassifies a divergence category, and the published corpus is never edited. +- **Declared errata:** the registry declares 12 defect(s) in the published reference material — 4 with a specification-derived correction, 8 documented without one, since no intended reading can be inferred ([OMG issues](../project/omg-issues.md), `tools/oracle/errata`). Every figure above is as published and stays the conformance statement; running the same oracles over the corrected text instead reports 361 of 390 files agreeing, 43 diagnostics ours alone and 92 the reference's alone, 0 declared rows we are silent on, and 0 of 313 authored cases the reference alone rejects. The corrected figures are diagnostic only: an erratum never reclassifies a divergence category, and the published corpus is never edited. - **Self-assessed surface:** the action, state-machine and classifier-behavior rows have no external referee at all — the four refereed figures above cannot see them, because the pinned artifact evaluates expressions but executes neither actions nor state machines. [Spec compliance](../project/spec-compliance.md) counts them. What these numbers cannot show: the OMG corpora are demonstrations rather than an official conformance suite; the differential is one-directional, comparing the diagnostics the two implementations report on the same files; the Xpect suites are the pilot authors' test intent rather than a certification oracle; and none of these is a percentage of the specification — no global compliance figure is claimed anywhere. diff --git a/docs/internals/design/observing-a-run.md b/docs/internals/design/observing-a-run.md index 2d682940c1..b570bb1319 100644 --- a/docs/internals/design/observing-a-run.md +++ b/docs/internals/design/observing-a-run.md @@ -60,12 +60,13 @@ element, and the values. - **The trace is already an event stream, not a log.** `TraceRecord` (`internal/exec/runtime/trace.go`) carries a `TraceKind` — `transition`, `entry`, `exit`, `do`, `accept`, `send`, `choice`, - `guard`, and `line` for the printed-only records — a `TraceOrigin` (the clock instant, the - object whose behavior made it, that behavior), the state or the transition's `From`/`To`, the - event and its typed `Payload`, and the `Target` of a send. `NewEventRecorder` already keeps the - stream for queries (`DocumentQueries`, the checker's state stream) without printing it; the - REPL's `%trace on` and the CLI's `-trace` print it. Every kind a listener needs to fire on is - already recorded at the point it happens; what is missing is a *sink other than the slice*. + `guard`, `terminate`, and `line` for the printed-only records — a `TraceOrigin` (the clock + instant, the object whose behavior made it, that behavior), the state or the transition's + `From`/`To`, the event and its typed `Payload`, and the `Target` of a send. `NewEventRecorder` + already keeps the stream for queries (`DocumentQueries`, the checker's state stream) without + printing it; the REPL's `%trace on` and the CLI's `-trace` print it. Every kind a listener needs + to fire on is already recorded at the point it happens; what is missing is a *sink other than the + slice*. - **The configuration is already a fact the executor answers.** `StateExecutor.ActiveStates()` and `ActiveLeaves()` give the active-state set with its region structure (a composite with regions is active with one leaf per region — [orthogonal regions](orthogonal-regions.md)); diff --git a/docs/manual/query-cookbook.md b/docs/manual/query-cookbook.md index a8adf35f22..172f04611e 100644 --- a/docs/manual/query-cookbook.md +++ b/docs/manual/query-cookbook.md @@ -1823,7 +1823,8 @@ sysml> %run-query Accepted root=spareDome `kind` names the records to keep — `accept`, `send`, `transition`, `entry`, `exit`, `do`, `choice` (a due order or region order the run drew, with -`alternatives` and `taken`) or `guard` (one it could not evaluate), several +`alternatives` and `taken`), `guard` (one it could not evaluate) or `terminate` +(a state machine's performance ending), several separated by commas, `all` by default — and a `source` left out reads every object's records. `since` and `before` take a duration or a bare number of the clock's seconds; a bound that is not a duration (`1 [m]`), or an interval diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index 51abc63919..dbcb189b12 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -61,8 +61,8 @@ "name": "examples", "dir": "examples", "origin": "ours", - "files": 51, - "digest": "sha256:0f6842f811dfd7a78b6218b8183fb8f43410a45b909b10ef9a6c4db14d8d6831" + "files": 52, + "digest": "sha256:363fc56b49d358d4146b7f7393eec10c67cbf0b7f90d553c1fa7e7c3951982d9" }, { "name": "probes", @@ -79,11 +79,11 @@ "digest": "sha256:7899f774dc78406305374ab88f654965b502e10fdc6cc8085eec7a16226b8996" } ], - "recorded": "2026-10-06" + "recorded": "2026-10-07" }, "totals": { - "files": 389, - "filesFullyAgreeing": 359, + "files": 390, + "filesFullyAgreeing": 360, "agreement": 45, "severityMismatch": 1, "openSysMLOnly": 44, @@ -931,8 +931,8 @@ "name": "examples", "dir": "examples", "totals": { - "files": 51, - "filesFullyAgreeing": 45, + "files": 52, + "filesFullyAgreeing": 46, "agreement": 0, "severityMismatch": 0, "openSysMLOnly": 7, @@ -1777,8 +1777,8 @@ } ], "totals": { - "files": 389, - "filesFullyAgreeing": 360, + "files": 390, + "filesFullyAgreeing": 361, "agreement": 45, "severityMismatch": 1, "openSysMLOnly": 43, diff --git a/docs/project/pilot-differential.md b/docs/project/pilot-differential.md index e7884bdaab..eee918fc35 100644 --- a/docs/project/pilot-differential.md +++ b/docs/project/pilot-differential.md @@ -241,7 +241,7 @@ nor double-counted as two independent disagreements. --- -## Results (pilot `2026-08`, 389 files) +## Results (pilot `2026-08`, 390 files) | Root | Files | Fully agreeing | Ours | Pilot | Agreed | Severity-only | Only ours | Only pilot | |---|---:|---:|---:|---:|---:|---:|---:|---:| @@ -250,9 +250,9 @@ nor double-counted as two independent disagreements. | `examples/pilot-corpora/sysml-validation` | 56 | 56 | 0 | 0 | 0 | 0 | 0 | 0 | | `examples/pilot-corpora/kerml-examples` | 58 | 56 | 9 | 0 | 0 | 0 | 9 | 0 | | `tests/testdata` | 21 | 11 | 55 | 77 | 45 | 1 | 9 | 31 | -| `examples` | 51 | 45 | 7 | 61 | 0 | 0 | 7 | 61 | +| `examples` | 52 | 46 | 7 | 61 | 0 | 0 | 7 | 61 | | `tools/referee/diff/testdata` (probes) | 4 | 1 | 6 | 0 | 0 | 0 | 6 | 0 | -| **Total** | **389** | **359** | **90** | **138** | **45** | **1** | **44** | **92** | +| **Total** | **390** | **360** | **90** | **138** | **45** | **1** | **44** | **92** | **Read the `only ours` total by root, never as one number.** Step 2 removes nine resolver false positives from the reference's **own** corpora: `pilot-examples` 16 → **7** and @@ -1184,7 +1184,7 @@ page's history. | Count | Now | |---|---:| -| overall: fully agreeing / only ours / our diagnostics | **359 / 44 / 90** | +| overall: fully agreeing / only ours / our diagnostics | **360 / 44 / 90** | | only pilot | **92** | | pilot diagnostics | **138** | | severity-only | **1** | diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md index cf4fafbb58..6c512fe5fa 100644 --- a/docs/project/roadmap.md +++ b/docs/project/roadmap.md @@ -1935,9 +1935,10 @@ the inverse — the held objects whose machine is in the named state, by leaf, b by dotted path — over the same population `all T` and `Objects` read (X5, Q2). `Events(source, kind, since, before)` answers **event rows**: the trace in the order the run made it — accepts, sends, transitions, state entry, exit and do steps, `choice` draws with their alternatives and the -one taken (region order and due order among them), unevaluable guards — each with its instant on -`Context.Clock()` (A5), its object and machine, the states it touches, its payload and the line -`-trace` prints; `kind` keeps one or several kinds and `[since, before)` is inclusive at the start, +one taken (region order and due order among them), unevaluable guards and state-machine +termination — each with its instant on `Context.Clock()` (A5), its object and machine, the states it +touches, its payload and the line `-trace` prints; `kind` keeps one or several kinds and +`[since, before)` is inclusive at the start, exclusive at the end, in the clock's unit or as a duration. The representation queried is the one the runtime records: `runtime.TraceRecorder` keeps a typed `TraceRecord` per event and the printer writes `-trace`'s lines from those records, so the two cannot disagree and the printed trace is diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 9fa290c48d..a3edb37ab8 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -2657,6 +2657,15 @@ boundaries; the landed Track E behavior is recorded in the execution rows and th | `trace_test.go` | Golden trace test infrastructure | ~200 | | `trace_calc_test.go` | Trace determinism and canonical rendering unit tests | ~180 | +### Run renderings + +| Run output | Implementation | Tests | Status | +|---|---|---|---| +| State occupancy over clock time, per object machine, with transitions and choice/guard marks; parallel leaves are keyed by state and region, colliding labels use the shortest distinguishing state path or region, termination closes occupancy, and the 200-span cap retains stable lane prefixes ending no later than each lane's first dropped instant while preserving earlier ends across inactive gaps | `internal/exec/runtrace/timeline.go` `Timeline`, `stateKey`, `applyTimelineLimit`; `internal/ir/view/run_timeline.go` text, Mermaid and PlantUML writers | `internal/exec/runtrace/runtrace_test.go`; parallel-region, termination and capped run-rendering goldens under `internal/exec/runtrace/testdata/` | ✅ Implemented and tested | +| Ordered sends and accepts between objects, including unmatched and environment messages; nonzero message serials pair exactly, while serial-zero traces retain FIFO event/target pairing | `internal/exec/runtrace/sequence.go` `Sequence`; `internal/exec/runtime/trace.go` `TraceRecord.Message` | `internal/exec/runtrace/runtrace_test.go`; serial-pairing and existing run-rendering goldens under `internal/exec/runtrace/testdata/` | ✅ Implemented and tested | +| State trace records retain the written state path and innermost orthogonal region; termination records retain their printed line while exposing the ending instant as an `Events(kind = "terminate")` row | `internal/exec/runtime/trace.go` `TraceRecord`, `TraceTerminate`; `internal/exec/runtime/state_executor.go` `StateExecutor.RegionOf`, `StateExecutor.terminateMachine`; `internal/doc/queryexec/events.go` `eventKinds`; `internal/exec/runtrace/timeline.go` | `internal/exec/runtime/trace_records_test.go` `TestStateTraceRecordsCarryWrittenPathsAndInnermostRegions`; runtrace termination fixture and golden | ✅ Implemented and tested | +| Located declarations flow from state-entry/exit records into run views: PlantUML timeline lanes link to machine declarations and single-state spans to state declarations; run sequence participants link to their instance types, while parallel spans and Mermaid gantt timelines remain unlinked | `internal/exec/runtime/state_executor.go` `stateSourceOrigin`; `internal/exec/runtime/trace.go` `TraceRecord.Source`; `internal/exec/runtrace/timeline.go`, `sequence.go`; `internal/ir/view/run_timeline.go`, `links.go` | linked run-rendering goldens under `internal/exec/runtrace/testdata/`; `internal/ir/view/run_timeline_test.go`; CLI and REPL run-rendering link tests | ✅ Implemented and tested | + ### Runtime bounds: every limit a model can reach A run is bounded, and a bound that silently changed a result would be the worst outcome, so each @@ -3186,7 +3195,7 @@ It does not mutate the workspace or re-derive a parallel semantic representation | `Except(source, exclude)` and `Union(source, other)` are ordered set operations over rows: `Except` keeps the source rows absent from `exclude` once each in source order, `Union` emits the source rows then the rows of `other` not yet present, each row once; identity is the semantic identity traversal deduplicates by — `symbols.KeyOf` for a model element, the instance for a held object, for a verdict its assertion with the object it was checked on (its path when no object is held), for a state its object, machine and state path, and for an event its record's place in the trace; `Union` refuses inputs whose projected columns differ (`ErrorInvalidArgument`) | `queryexec/setops.go` `executor.evaluateExcept`, `executor.evaluateUnion`, `setKeyOf`, `sameColumns` | `queryexec/setops_test.go:TestExecuteSetOperationsOverElements`, `:TestExecuteSetOperationsComposeCoverage`, `:TestExecuteSetOperationsKeepProjectedColumns`, `:TestExecuteSetOperationsOverSessionObjects`, `:TestExecuteSetOperationsOverVerdicts`, `:TestExecuteSetOperationsOverStatesAndEvents` | ✅ Implemented | | `States(source)` answers the state each session object's machine is in **now**, as **state rows**: one per active leaf across every orthogonal region, the object as `object`/`path` (the `ValueObject` the source row was), `machine` the exhibited usage, `name` the leaf, `statePath` its dotted path under the machine (`on.run`; a synthesized region owner of a `parallel` state is left out), `region` the orthogonal region the leaf runs in and `enclosing` the active composite states above it, the row standing for the state declaration so its own properties read too. `InState(name)` is the inverse: the held objects, each once, whose machine is in the named leaf or composite state, by simple name or dotted path, as object rows. Both read the session's held roots as `Objects` does (`ErrorNoRuntime` without a session), refuse an object exhibiting no state machine (`ErrorNoStateMachine`), a source row that is no object (`ErrorNotAnObject`) and a state no held machine declares (`ErrorUnknownState`); they read `StateExecutor.ActiveStates` and move no clock | `queryplan/plan.go` `OperationStates`, `OperationInState`; `queryplan/compiler.go` (`DocumentQueries::States`, `InState`); `queryexec/value.go` `ValueState`; `queryexec/state.go` `State`, `StateValue`, `Value.State`; `queryexec/states.go` `executor.evaluateStates`, `executor.evaluateInState`, `executor.objectArgument`, `stateMachinesOf`, `activeState`, `regionName`, `machineName`, `stateSymbol`, `stateNamed`, `machineDeclaresState`, `executor.statePropertyValues`; `queryexec/errors.go` `ErrorNoStateMachine`, `ErrorUnknownState`, `ErrorStateRow`; `runtime/state_executor.go` `StateExecutor.ActiveStates`, `EnclosingStates`, `StatePath`; `libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml` `States`, `InState`, `State` | `queryexec/states_test.go:TestExecuteStatesListsEveryActiveLeaf`, `:TestExecuteStateRowsThroughRowOperations`, `:TestExecuteInStateFindsObjectsByLeafOrEnclosingState`, `:TestExecuteStatesRefusals`; `repl/docquery_states_test.go:TestRunQueryStatesOverSession`; `repl/cookbook_states_test.go:TestCookbookStateAndEventRecipes` | ✅ Implemented | | A terminated state machine holds no active configuration, so `States` answers no row for its object and `InState` matches nothing, while a machine that reached `done` reports it as the final state; a `States` or `Events` `source` bound to an object the run destroyed is refused with `ErrorObjectDestroyed` naming the object and the activation mark of its destruction, rather than answering a stale row or surfacing an unevaluable-feature failure; a destroyed object leaves the population — `Objects`, `InState` and element-derived sources skip it and its label still resolves for `Events` rows, while a source naming only destroyed objects is the same refusal | `runtime/lifetimes.go` `Context.Destroyed`; `queryexec/objects.go` `executor.eachSessionObject`, `executor.evaluateObjects`; `queryexec/states.go` `executor.evaluateInState`, `executor.objectArgument`, `executor.objectsDeclaredBy`, `executor.objectDestroyedError`; `queryexec/errors.go` `ErrorObjectDestroyed` | `queryexec/robustness_state_event_queries_test.go:TestQueryRobustnessStateEventQueries`; `runtime/robustness_state_event_queries_test.go:TestRuntimeRobustnessStateEventQueries`; `grpc/robustness_docquery_states_events_test.go:TestGRPCRobustnessDocumentQueryStatesEvents` | ✅ Faithful | -| The trace is a typed relation the printer writes from: `TraceRecorder` keeps a `TraceRecord` per accept, send, transition, state entry, exit and do step, `choice` draw (the alternatives and the one taken, due order and region order included) and unevaluable guard, each with its `TraceOrigin` — the clock's instant, the object and the behavior — beside the free-text lines the other tracers write, and `Entries()` prints every record through `TraceRecord.Line`, so `-trace`/`%trace` output is byte-for-byte what it was and cannot drift from the record; `%trace off` and `Clear` discard it. `Events(source = null, kind = "all", since = null, before = null)` reads the records in the order the run made them as **event rows**: `kind`, `time` (the origin's instant as a quantity in the clock's unit), `object`/`path`/`machine`, `state`/`from`/`to`, `target` (a send's addressee), `event`, `payload` (`name = value` per field), `alternatives`/`taken` (a choice) and `text` (the printed line); `source` keeps the records of the objects behind its rows (every object's when left out), `kind` one or several comma-separated kinds, and `[since, before)` — inclusive start, exclusive end — a bare number in the clock's unit or a duration converted through `Context.ClockMagnitude`. Refused: a session recording no trace (`ErrorNoTrace`), a kind the record has not (`ErrorInvalidArgument`), a bound that is no instant — a non-duration quantity, a clock carrying no unit — or a `before` at or before `since` (`ErrorInvalidInterval`), and `OwnedElements`, `Descendants`, `Ancestors`, `RelatedElements`, `States`, `Verdicts` and `Events` themselves over a state or event row (`ErrorStateRow`, `ErrorEventRow`); `WhereFeature`, `OrderBy`, `Project` and `Column` read the state and event properties before the element's own, and `WhereName`/`WhereType` the state declaration or the object's type | `runtime/trace.go` `TraceKind`, `TraceOrigin`, `TraceRecord`, `TraceRecord.Line`, `TraceRecorder.Records`, `TraceRecorder.Entries`, `RecordAccept`, `RecordSend`, `RecordStateTransition`, `RecordStateEntry`, `RecordStateExit`, `RecordDoStep`, `RecordNote`; `runtime/context.go` `Context.ClockMagnitude`; `queryplan/plan.go` `OperationEvents`; `queryexec/value.go` `ValueEvent`; `queryexec/event.go` `Event`, `EventValue`, `Value.Event`; `queryexec/events.go` `executor.evaluateEvents`, `executor.eventKindArgument`, `executor.instantArgument`, `eventKinds`; `queryexec/errors.go` `ErrorNoTrace`, `ErrorInvalidInterval`, `ErrorEventRow`; `repl/trace.go` (the session's recorder); `cmd/sysml/check.go` `checks.runQueries` (queries run after `-state`/`-action` and `-advance`); `libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml` `Events`, `Event` | `runtime/trace_test.go:TestExecutionTrace` (goldens unchanged); `queryexec/events_test.go:TestExecuteEventsReadsTheTraceInOrder`, `:TestExecuteEventsByKind`, `:TestExecuteEventsIntervalIsClosedOpen`, `:TestExecuteEventRowsThroughRowOperations`, `:TestExecuteEventsRefusals`; `repl/docquery_states_test.go:TestRunQueryEventsOverSession`; `cmd/sysml/run_query_test.go:TestRunQueryOverStatesAndTrace` | ✅ Implemented | +| The trace is a typed relation the printer writes from: `TraceRecorder` keeps a `TraceRecord` per accept, send, transition, state entry, exit and do step, `choice` draw (the alternatives and the one taken, due order and region order included), unevaluable guard and state-machine termination, each with its `TraceOrigin` — the clock's instant, the object and the behavior — beside the free-text lines the other tracers write, and `Entries()` prints every record through `TraceRecord.Line`, so `-trace`/`%trace` output is byte-for-byte what it was and cannot drift from the record; `%trace off` and `Clear` discard it. `Events(source = null, kind = "all", since = null, before = null)` reads the records in the order the run made them as **event rows**: `kind`, `time` (the origin's instant as a quantity in the clock's unit), `object`/`path`/`machine`, `state`/`from`/`to`, `target` (a send's addressee), `event`, `payload` (`name = value` per field), `alternatives`/`taken` (a choice) and `text` (the printed line); `source` keeps the records of the objects behind its rows (every object's when left out), `kind` one or several comma-separated kinds, and `[since, before)` — inclusive start, exclusive end — a bare number in the clock's unit or a duration converted through `Context.ClockMagnitude`. Refused: a session recording no trace (`ErrorNoTrace`), a kind the record has not (`ErrorInvalidArgument`), a bound that is no instant — a non-duration quantity, a clock carrying no unit — or a `before` at or before `since` (`ErrorInvalidInterval`), and `OwnedElements`, `Descendants`, `Ancestors`, `RelatedElements`, `States`, `Verdicts` and `Events` themselves over a state or event row (`ErrorStateRow`, `ErrorEventRow`); `WhereFeature`, `OrderBy`, `Project` and `Column` read the state and event properties before the element's own, and `WhereName`/`WhereType` the state declaration or the object's type | `runtime/trace.go` `TraceKind`, `TraceOrigin`, `TraceRecord`, `TraceRecord.Line`, `TraceRecorder.Records`, `TraceRecorder.Entries`, `RecordAccept`, `RecordSend`, `RecordStateTransition`, `RecordStateEntry`, `RecordStateExit`, `RecordDoStep`, `RecordNote`; `runtime/context.go` `Context.ClockMagnitude`; `queryplan/plan.go` `OperationEvents`; `queryexec/value.go` `ValueEvent`; `queryexec/event.go` `Event`, `EventValue`, `Value.Event`; `queryexec/events.go` `executor.evaluateEvents`, `executor.eventKindArgument`, `executor.instantArgument`, `eventKinds`; `queryexec/errors.go` `ErrorNoTrace`, `ErrorInvalidInterval`, `ErrorEventRow`; `repl/trace.go` (the session's recorder); `cmd/sysml/check.go` `checks.runQueries` (queries run after `-state`/`-action` and `-advance`); `libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml` `Events`, `Event` | `runtime/trace_test.go:TestExecutionTrace` (goldens unchanged); `queryexec/events_test.go:TestExecuteEventsReadsTheTraceInOrder`, `:TestExecuteEventsByKind`, `:TestExecuteEventsIntervalIsClosedOpen`, `:TestExecuteEventRowsThroughRowOperations`, `:TestExecuteEventsRefusals`; `repl/docquery_states_test.go:TestRunQueryEventsOverSession`; `cmd/sysml/run_query_test.go:TestRunQueryOverStatesAndTrace` | ✅ Implemented | | A state or event row renders everywhere a query result does: `%run-query`/`-run-query` print a state row as `. in ` and an event row as `t= .: `; a document cell is that text in Markdown and PDF, and in HTML a `span.sysml-state` with `data-machine`, `data-state`, `data-region` or a `span.sysml-event` with `data-event-kind`, `data-time`, each with the object's `data-object`; `RunDocumentQuery` answers the `state` and `event` arms of `DocumentValue` (`DocumentState`: `object`, `machine`, `name`, `path`, `region`, `enclosing`, `state`, `text`; `DocumentEvent`: `kind`, `time`, `object`, `machine`, `state`, `from`, `to`, `target`, `event`, `payload`, `alternatives`, `taken`, `text`) over the held population, whose run is traced from the first `Instantiate` into a recorder keeping the most recent `OPENSYSML_GRPC_MAX_HELD_EVENTS` records (default 100,000; an `Events` interval reaching a dropped record is a `trace-truncated` error, `FAILED_PRECONDITION`), and refuses either bound as a parameter with `INVALID_ARGUMENT`; the Go and Python clients decode them as `DocumentState`/`DocumentEvent` on the row and in cells and refuse to bind one, Node, Java and Rust carry the regenerated stubs | `repl/docquery.go` `formatQueryValue`; `docir/evaluate.go`; `docrender/markdown.go`, `docrender/html.go` (the PDF backend hands the HTML-input engines the HTML backend's page); `grpc/docquery.go` `documentState`, `documentEvent`, `boundValue`; `grpc/objects.go` `Service.objects` (the traced population); `api/proto/sysml.proto` `DocumentState`, `DocumentEvent`; `client/opensysml/documents.go` `DocumentState`, `DocumentEvent`, `Row.State`, `Row.Event`; `client/python/opensysml/document.py` `DocumentState`, `DocumentEvent`, `DocumentRow.state`, `DocumentRow.event`; `tools/cmd/conformance/pkgclient.go` `cellToProto`/`cellFromProto` | `docrender/states_test.go:TestMarkdownStateReportGolden` (`testdata/state_report.golden.md`), `:TestHTMLStateReport`; `docpdf/docpdf_test.go:TestRenderStateReportPage`; `docpdf/integration_test.go:TestRenderStateReportWithInstalledEngines` (real engines, skipped when absent); `repl/docquery_states_test.go`; `cmd/sysml/run_query_test.go:TestRunQueryOverStatesAndTrace`; gRPC conformance `document_query_states`, `document_query_in_state`, `document_query_events`; `client/opensysml/states_test.go:TestRunDocumentQueryAnswersStateAndEventRows`, `:TestStateAndEventRowsAreNotBound`; `client/python/tests/test_document.py::test_a_state_row_decodes_to_the_object_and_its_state`, `::test_an_event_row_decodes_to_the_trace_record`, `::test_a_state_or_event_binding_is_refused` | ✅ Implemented | | A relationship-derived column traverses from each row's element — an object row's declaration, the assertion of a verdict row, the state or behavior a state or event row is of — with the breadth-first traversal `RelatedElements` uses (every relationship kind and direction it accepts, bounded by `maxDepth`, deduplicated by semantic identity, ordered as reached, charged to the shared visit budget); `list` yields the related elements as one multi-valued cell (an empty cell when none), `count` an integer and `any` a boolean that stops at the first element reached, and the cells are read downstream by name — `WhereFeature` and `OrderBy` over a projected column read its cells (an element compares and orders as its qualified name, under the text operators), a document table groups by it, and `%run-query`, Markdown, HTML and `RunDocumentQuery` carry every value as they carry a multi-valued `documentation` cell. An unknown relationship kind, an invalid direction and an exhausted budget are the typed failures `RelatedElements` raises, naming the column; a row no element declares (an event posted from outside the run) is a typed `undeclared-row` failure naming the column and the row | `queryexec/related_column.go` `relatedColumn`, `relatedColumnOf`, `executor.evaluateRelatedCell`, `columnScoped`; `queryexec/related.go` `executor.validateRelationship`, `executor.traverseRelated`; `queryexec/where_related.go` `executor.hasRelated`; `queryexec/computed.go` `computedColumns`, `executor.evaluateColumnCell`; `queryexec/operations.go` `projectedColumn`, `executor.featureValues`, `compareValue`, `executor.compareOrdered`; `queryexec/errors.go`; `docplan/compiler.go` `columnNames` | `queryexec/related_column_test.go:TestExecuteRelatedColumnsBuildATraceabilityMatrix`, `:TestExecuteRelatedColumnFollowsDepthAndDirection`, `:TestExecuteRelatedColumnReportsItsColumnInErrors`, `:TestExecuteRelatedColumnLeavesTableConstructionUncharged`, `:TestExecuteRelatedColumnAnyStopsAtTheFirstElement`, `:TestExecuteRelatedColumnsFilterAndOrderDownstream`, `:TestExecuteRelatedColumnElementsCompareByQualifiedName`, `:TestExecuteRelatedColumnTraversesFromAnObjectsDeclaration`, `:TestExecuteRelatedColumnRefusesARowNoElementDeclares`; `grpc/related_column_test.go:TestRunDocumentQueryCarriesRelatedColumns`; `docplan/runs_test.go:TestCompileGroupedTableSeesRelatedColumns` | ✅ Implemented | diff --git a/docs/project/view-rendering-forms.md b/docs/project/view-rendering-forms.md index 3b2c8bc9c3..a086f2d08b 100644 --- a/docs/project/view-rendering-forms.md +++ b/docs/project/view-rendering-forms.md @@ -1010,6 +1010,10 @@ origins and bundled library declarations — is not linked. | D2 | Nodes, containers, pseudostate glyphs, edges and sequence lifelines and messages carry `link: "url"` after their `class`, which D2 draws as an SVG anchor for every one of them. Pins are not linked: a port's link is its owner's. | | Mermaid state diagram | Simple states are linked; composite states are not. | | Mermaid sequence diagram | Participants receive `link` statements; messages are not linked. Mermaid CLI 11.16.0 drops participant URL fragments in SVG. | +| Run timeline, PlantUML | A lane links to its state-machine declaration; a span links only when it represents one state. Parallel spans with several active states are unlinked. | +| Run timeline, Mermaid gantt | No links: `click`/`href` directives do not produce anchors in Mermaid CLI's SVG output. | +| Run sequence, Mermaid and PlantUML | Object participants link to the declaration of their instance type. The environment participant and messages are unlinked because trace records have no message source site. | +| Run text | No links; labels remain the recorded paths and states. | The writers emit no link syntax when links are disabled or no site is available. @@ -1131,6 +1135,8 @@ refuse D2: | --- | --- | --- | | CLI | `-render -render-form mermaid\|dot\|plantuml\|d2`; `-render-all ` writes `.mmd`, `.dot`, `.puml` or `.d2`; `-render-palette ` fills nodes in each form where applicable | [`docs/reference/cli.md`](../reference/cli.md#rendering-a-view) | | REPL | `%render mermaid\|dot\|plantuml\|d2 [palette] [pilot\|cameo]`; `%help` names the options; form, palette and style complete where accepted | [`docs/reference/repl-commands.md`](../reference/repl-commands.md#rendering-a-view) | +| CLI run output | `-render-run timeline=` or `sequence=` writes text, Mermaid or PlantUML; `-render-link` links PlantUML timeline lanes and single-state spans and sequence participants; DOT is refused | [`docs/reference/cli.md`](../reference/cli.md#rendering-a-run) | +| REPL run output | `%render-run timeline\|sequence [text\|mermaid\|plantuml\|dot] [link=