The time seam: one clock across the views (design)¶
Status: adopted 2026-08-30. Nothing below is implemented; this is the contract for the arc. Decisions: a dedicated dependency-free
Clockobject owns the state, with the scrubber one subscriber among equals (Q1); the Cesium bridge runs shared-rate local integration with bounded-drift reconciliation (~4 Hz) and exact convergence on pause (Q2); step-only traces refuse the globe by default –seconds_per_stepopts in, accepts a SCALAR or a PER-STEP sequence/mapping (steps take unequal durations), durations the model itself states (time triggers, occurrence durations) are used first-class and never counted as synthetic, and the scrubber labels only the synthetic segments (Q3); the trace-to-mission binding rides the existing seams now –model_waypointsplus an epoch attribute typedTime::Iso8601DateTime– withMissionPhasemetadata arriving in phase 3 to retire the name heuristics (Q4);Clock,link_time, and the scrubber live inlongeron.widgets, the Cesium bridge stays inmission3d(Q5); phase 1 lands after provenance layers 1-2 within 0.12, phase 2 immediately behind it, phase 3 rides the 0.13 geometry arc (Q6); the playhead does not join the selection seam in this design – recorded as the natural follow-on once both seams are stable (Q7).
Goal: make time a shared, linkable state across every time-aware view. The diagram replay player, the Cesium mission replay, and any future time-aware view each keep a private clock today, and nothing links them. The time seam gives them one clock, so scrubbing one view scrubs them all. A state machine’s transitions then fire in the diagram at the same moments the craft moves on the globe.
The seam is the temporal analogue of the selection seam. The selection
seam made “what is selected” one state with many subscribers. The time
seam makes “when are we” the same kind of state. The design mirrors
the selection contract clause for clause, and it reuses the same
wiring discipline (kernel-side observers, first-fixpoint writes, an
unlink() disposer).
All empirical claims below were verified against longeron 0.10.0 at
commit ad27a8b (the DeepScout examples, the shipped stdlib, and the
live widget sources).
The pattern to mirror: the selection seam¶
Tutorial 3 states the selection contract, and every linked surface obeys it:
Each surface exposes its selection as a widget trait.
Selecting in one surface writes the same qualified name into the others.
Each write settles at its first fixpoint, so no echo loops back.
Switching the diagram kind preserves the selection.
The mechanics behind the contract are worth naming, because the time
seam copies them. There is no central hub. link.link_selection
composes existing per-view seams pairwise: the diagrams’ on_select
callback, the 3D viewer’s highlight_json and picked_json traits,
the scoreboard’s selected trait. Echo suppression is equality
coalescing (a write of an equal value does not re-fire) plus explicit
reentrancy guards where two directions meet (the syncing flag in
grand_dashboard). The wiring runs kernel-side through traitlets
observers, so it works headless, and every link returns an unlink()
callable.
The temporal restatement of the contract:
Each time-aware view exposes its playhead as a widget trait.
Scrubbing one view writes the same instant into the others.
Each write settles at its first fixpoint, so no echo loops back.
Swapping a view (or its payload) preserves the playhead.
What longeron has today¶
Three representations of one execution exist, each with its own clock surface. Nothing links them at view time.
The trace clock¶
StateMachine (interpreter.py) owns the simulation clock: self.now,
a float starting at 0.0. Only two things advance it. A plain number
in the event feed calls advance(duration), and advance fires due
time triggers (accept after d / accept at t) in deadline order,
computing each deadline as node.entered_at + offset (for after) or
offset (for at). The floats are unitless. Seconds are a
convention, stated in docstrings and honored by every consumer, never
stated in the model.
replay.record_timeline observes a simulation through the machine’s
on_step hook and produces a Timeline:
t_start/t_end– the recorded span (alwayst_start == 0.0);step_mode–Truewhent_end == 0.0(a pure event cascade with no clock advance); then every key below is a step index, not a time;n_steps– the number of observed steps;tracks– per-state keyframes{qname: [(t, active)]}, recorded on change only;fired–FiredTransition(t, source, target, event)instants;env_steps– per-step scalar env snapshots[(t, {name: value})](the telemetry:SortieStates.batterylands here);parents– the composite-state relation the front-end tints with.
record_action_timeline produces the same shape for action
executions, always in step mode. Timeline.to_json rounds every key
to 3 decimals.
replay_widget bakes the state (or action) diagram to SVG and
animates the timeline over it. Its widget traits: svg,
timeline_json, width_px, and time – a bidirectional
playhead (sim time, or step index in step mode). The front-end
writes time at ~4 Hz while playing (a 250 ms throttle). A
kernel-side write seeks the playhead, and it stops playback
first. The front-end ignores an echo of its own write (|delta| < 1e-9). Playback is a requestAnimationFrame loop; the speed select
multiplies wall time (1x = 1 step/s in step mode).
The mission clock¶
analysis.mission3d synthesizes a MissionTrack from the model:
epoch– adatetime; the default is the fixed instant 2026-01-01T12:00:00Z, so the CZML is deterministic;samples–(t, lat, lon, alt)keyframes,tin seconds pastepoch;waypoints– the planned route;phases– motion segments(t0, t1, phase, qname), whereqnameis the instance-qualified leaf state that drove the segment;duration– the last phase end.
from_replay builds the track from a state-machine execution: it
calls record_timeline itself, classifies each leaf state’s
activation interval into a motion phase by name substrings
(_PHASE_RULES, overridable per state name via phases=), and maps
route-phase intervals onto the waypoint polyline proportionally.
Timed timelines map onto track seconds 1:1 (scale 1.0). Step-mode
timelines scale by seconds_per_step (default 10.0).
MissionTrack.to_czml bakes a CZML document whose first packet is the
clock: interval (epoch to epoch+duration), currentTime,
multiplier = max(1, round(duration / 40.0)) (playback takes ~40 s of
wall clock), range: CLAMPED, step: SYSTEM_CLOCK_MULTIPLIER.
Cesium’s own timeline bar and animation dial are the playback UI.
mission_viewer’s widget traits: czml_json, label, height_px,
ion_token, imagery, picked_json (the pick seam, shared with
viewer3d), and time – a bidirectional playhead in seconds past
the track epoch. The front-end converts with
JulianDate.secondsDifference(currentTime, startTime) and writes at
~4 Hz on the Cesium clock’s onTick. A kernel-side write seeks
viewer.clock.currentTime; it does not pause a playing Cesium
clock. The echo tolerance is 1e-3.
The third representation: occurrences at M0¶
m0.from_timeline reads a Timeline as an interpretation: every
contiguous state activation becomes an occurrence Individual
(id qname@k) with start / end / duration slots, owned by a
root that spans the recording. Tutorial 5 teaches this as
traces-are-interpretations. The occurrence slots live on the same
axis as the timeline keys, so a playhead t selects “the occurrences
alive at t” by a slot comparison. The time seam does not touch M0,
but the shared axis is what makes that future query coherent.
What links them today: nothing¶
The two widgets’ time traits are private. grand_dashboard composes
the mission pane beside the diagram, scoreboard, and 3D panes and
wires every selection seam, but no time wiring exists (verified by
reading the wiring map: no observer touches either time trait).
Tutorial 7 aligns the two clocks by hand: it computes cruise_s = routeM / cruiseSpeedMps from mission_values and authors that number
into the event feed, so the simulated flying interval matches the
physical route time. The correspondence lives in a notebook cell, not
in the model and not in any shared object. And the diagram replay and
the globe each record their own simulation: replay_widget calls
record_timeline, from_replay calls it again. The runs are
deterministic and therefore equal, but the invariant is fragile and
the work is duplicated.
The standards boundary: what the language states about time¶
The seam’s vocabulary should come from what the model already states, per the model-derived posture. The inventory:
Time triggers are grammar- and interpreter-real. The grammar
carries TriggerExpression with kind = ('at' | 'after' | 'when')
(SysML.g4, under the spec’s §8.2.2.17.4 send-and-accept rules), model.py carries TriggerKind = Literal["at", "after", "when"], and StateMachine.advance
implements the deadline semantics. This is the one place the language
already drives the trace clock.
The Time package is vendored and resolves. Longeron ships
Time.sysml in src/longeron/_stdlib/quantities/. Verified against
0.10.0 with the standard library attached: Time::Clock (a part def
whose currentTime : TimeInstantValue “advances monotonically over
its lifetime”), Time::universalClock, Time::TimeOf and
Time::DurationOf (calcs from an Occurrence and a Clock to an
instant and a duration), Time::TimeScale (an IntervalScale with a
definitionalEpoch), Time::TimeInstantValue, Time::UTC (the UTC
time scale, epoch 1958-01-01), and Time::Iso8601DateTime (a UTC
instant carried as an ISO 8601 string) all resolve. The package hands
the seam its vocabulary: a clock, an instant, a scale with an epoch.
The kernel underneath dangles, as expected. Time references
Clocks::Clock and ISQSpaceTime::TimeValue; both dangle (verified),
because the KerML kernel libraries are not vendored and ISQSpaceTime
awaits the units design’s vendoring decision. The posture from the
units and geometry designs applies unchanged: resolution is good where
the library ships, and the seam never depends on the dangling parts.
KerML’s ordering substrate is parse-only. Occurrences and
successions are the spec’s ordering machinery. Longeron’s Occurrences
shim declares the abstract Occurrence, kerml.py parses occurrence
declarations, and model.py carries Succession elements (the action
executor’s _succession_plan already walks them). The seam’s ordering
claims (events indexed by t, occurrences with lifetimes) are the
executable shadow of that substrate, not a new theory of time.
What the model could state and does not. No shipped model states
the unit of sim time (the floats are bare), no model states a mission
epoch, and no model states which motion phase a flight state drives
(the name heuristics in _PHASE_RULES guess it). Each gap has a
standard-vocabulary fix, taken up in the binding section below.
The alignment verdict¶
clock |
axis |
zero |
advanced by |
kernel surface |
|---|---|---|---|---|
trace |
sim seconds (bare floats; step index in step mode) |
machine start |
numbers in the event feed; |
|
mission |
seconds past epoch |
|
sample interpolation |
|
Cesium |
|
CZML |
|
|
The verdict is friendlier than the three-clock table suggests. For a
timed trace, trace seconds and track seconds are already the same
number: from_replay copies timeline keys onto sample times at
scale 1.0. Verified end to end on the shipped flight demo: the
FLIGHT_EVENTS feed yields t_end = 168.0, the track’s duration = 168.0, the phases carry the same instants
((2.0, 8.0, 'takeoff', ...), (8.0, 158.0, 'route', ...)), and the
CZML clock spans 12:00:00Z to 12:02:48Z. The Cesium clock is the track
clock plus an epoch, and MissionViewer already divides the epoch
back out: both widgets’ time traits speak seconds on the same axis
today.
So the seam needs no conversion layer for the timed case. It needs shared state, wiring, and honesty about three real gaps:
Step-mode traces have no time axis. The replay widget scrubs the step index; the globe fabricates seconds via
seconds_per_step. One playhead cannot silently mean both.The sim clock’s unit is a convention. Nothing in the model says the bare floats are seconds. The units design keeps runtime values as floats, so this stays an annotation problem, not a runtime one.
The trace-to-mission correspondence is authored by hand. The event feed must be written so the flying interval matches the route time (tutorial 7’s
cruise_s), the waypoints arrive as a Python argument, and the phase mapping guesses from state names.
The design¶
The clock contract¶
One small kernel-side object holds the shared state. It has no front-end, no timer, and no dependencies (plain Python callbacks, so the core package stays dependency-free; the widgets already bring traitlets for their own traits):
class Clock:
"""The shared playhead for one linked group of views."""
t: float # the playhead, in axis units
playing: bool # someone is animating
rate: float # axis units per wall second (1.0 = real time)
span: tuple[float, float] # (t0, t1); seeks clamp into it
step_mode: bool # the axis is a step index
def seek(self, t: float) -> None # clamps, fans out, coalesces equals
def play(self) -> None
def pause(self) -> None
def set_rate(self, rate: float) -> None
def observe(self, callback) -> Callable[[], None] # returns unobserve
The no-echo discipline is the selection seam’s, restated for floats:
a seek to a value within the coalescing tolerance of the current
t does not fan out, every subscriber applies the same rule before
writing back, and each write therefore settles at its first fixpoint.
The tolerance is one JSON quantum (1e-3, matching
Timeline.to_json’s 3-decimal rounding and the mission widget’s
existing echo guard).
The clock owns no wall-clock timer. Views animate (the replay widget’s
requestAnimationFrame loop, Cesium’s clock); the kernel holds state
and fans it out. While playing, every animating view integrates t
locally at the shared rate and reconciles against the clock at the
sync rate: a follower whose local time drifts past a bounded tolerance
snaps to the clock’s t. On pause, every view converges exactly.
This is the non-fighting rule: no view is the permanent owner, drift
is bounded while playing, and equality holds at rest.
The timebase: one recording, many views¶
One object aligns a trace with its optional mission binding, so every view plays the same recording:
@dataclass
class Timebase:
timeline: Timeline # the recorded truth (replay.py)
track: MissionTrack | None # the optional globe binding
seconds_per_step: float | None # step-mode globe binding only
@property
def span(self) -> tuple[float, float] # the shared axis
def events_at(self, t0, t1) -> list[FiredTransition]
def env_at(self, t) -> dict[str, Any] # the telemetry row
def phase_at(self, t) -> tuple[str, str] | None
Building it requires one kernel refactor, small and honest:
from_replay’s track synthesis splits into a function over an
existing Timeline (track_from_timeline(timeline, waypoints=..., phases=..., ...)), with from_replay kept as the recording
convenience. replay_widget and mission_viewer gain timeline= /
track= parameters so they accept the prebuilt recording instead of
re-simulating. The axis is the timeline’s own axis: sim seconds, or
the step index in step mode. The track, when present, shares that axis
by construction (the 1:1 mapping the verdict established); its epoch
stays a track property that only the Cesium bridge touches.
The wiring: link_time¶
The temporal link_selection:
clock = Clock(span=timebase.span, step_mode=timebase.step_mode)
unlink = link_time(clock, player, mission, scrubber)
link_time attaches each view through a small adapter that knows the
view’s trait name and its axis mapping:
view |
trait |
mapping |
|---|---|---|
replay widget |
|
identity (both modes) |
mission viewer |
|
identity (timed); |
scrubber widget |
|
identity |
Each adapter observes the view’s trait and calls clock.seek, and
observes the clock and writes the trait, both sides applying the
coalescing tolerance. link_time returns an unlink() that detaches
every adapter, mirroring link_selection. The wiring is kernel-side
traitlets observers, so a headless test drives clock.seek(42.0) and
asserts both traits moved without a browser.
The Cesium bridge¶
Cesium has its own clock, multiplier, timeline bar, and animation dial, and users will keep using them. The bridge must be bidirectional and non-fighting:
Kernel -> Cesium. A clock seek writes the
timetrait; the existing front-end handler already setscurrentTimeand requests a render.play/pause/set_rateneed three small front-end additions: setviewer.clock.shouldAnimateandviewer.clock.multiplier = ratefrom matching traits.Cesium -> kernel. The existing
onTickreporter already writestimeat ~4 Hz. Two additions: reportshouldAnimateflips (the user pressed the Cesium dial) andmultiplierchanges, so the clock’splaying/ratefollow the dial.Non-fighting. While playing, Cesium integrates its own clock at
multiplier = rateand the bridge only reconciles drift beyond the tolerance. The CZML document clock keeps its bakedmultiplieras the initial rate; the bridge overrides it at link time.
rate semantics are fixed across views: axis units per wall second.
For a timed trace rate = 1.0 is real time, and the Cesium multiplier
equals rate exactly. The replay widget’s speed select maps to the
same number (its step mode already defines 1x as one step per second).
Loss tolerance: the reconciliation protocol¶
The widget protocol is fire-and-forget: a congested channel drops comm
messages (jupyter-server’s iopub rate limiter, websocket reconnects
mid-burst), trait sync only sends changes, and a stale in-flight
report can race a kernel seek even with zero drops. Twice in browser
CI that combination split the seam permanently: a scrub-to-20 during
playback stopped the player’s front-end, but a stale ~4 Hz report
re-seeked the kernel clock to ~74.7 and no future message corrected
either side. The seam therefore carries a small reconciliation
protocol (longeron.widgets._seam, its docstring holds the full
anatomy); the kernel clock is the source of truth and front-ends
reconcile to it:
Generation stamps. Every kernel push bumps
_seam_gen, so every push is a real message and a front-end can see a missed one (a stamp gap) and ask for full state.Acknowledged reports. Reports echo the last applied stamp (
_seam_ack). A machine report (the ~4 Hz playback integration) carrying an old stamp is REJECTED and answered with an unconditional full-state re-push – which also heals dropped pushes, because the stale reports keep arriving until a re-push lands. User actions (_seam_intentbumped in the same message) outrank a raced push: intent is new truth, not an echo of old state.Trailing-edge verify. ~1.5 s after seam traffic quiesces the front-end asks the kernel to re-state truth (one shot, re-armed by traffic – never a hot timer). A dropped report thus heals by visible REVERSION to kernel truth; the user retries, and no silent split survives.
The protocol adds no steady-state traffic: while a view plays, its
reports are accepted without echoes exactly as before, and the
reconciliation acts only on rejection, on a stamp gap, or at a burst’s
trailing edge. tests/browser/test_browser_lossy_seam.py proves the
contract by injecting counted message loss in each direction.
The scrubber widget¶
A small anywidget: a play/pause button, a rate select, a range slider
over the span, a readout clock, and tick marks at timebase event
instants (fired transitions), with the env_at telemetry row as an
optional readout line. It is the replay widget’s control bar promoted
to a standalone surface, and it subscribes to the clock like any other
view. It renders alone under a dashboard, beside views that have no
chrome of their own (the state-highlight SVG, a future strip chart).
Step-only traces: the honest degradation¶
A pure event cascade records in step mode, and steps are not seconds. The degradation:
The clock’s axis is the step index:
span = (0, n_steps - 1),step_mode = True, and the scrubber labels the readoutstep k / N(the replay widget already does).The globe binding is refused by default:
link_timeraises when a step-mode timebase carries a track built without an explicitseconds_per_step. Passing one opts in; the adapter scales, and the scrubber labels the axis synthetic (step k / N (x10 s)).Everything else works unchanged: step-scrubbing the diagram, the telemetry readout, the tick marks.
This mirrors the house posture on honest reporting: a fabricated second is displayed as fabricated, never silently blended with a real one.
The model-stated binding¶
Three Python-side conventions can migrate into the model, each on standard or precedented vocabulary, none blocking the seam:
Waypoints already migrate today:
model_waypointsreads(lat, lon, alt[, t])off a mission part’s children through the interpreter. The seam’s demos should use it instead of theATLANTA_LOOPconstant.The epoch becomes a mission attribute typed
Time::Iso8601DateTime(vendored, resolves). The track builder reads it when present and falls back to the deterministic default. The model then states when the sortie flies, in the standard’s own ISO 8601 form.The phase mapping becomes metadata: a
MissionPhasemetadata def inanalysis_conventions.sysml(the established home for tool-facing conventions), applied per flight state (@MissionPhase { phase = "route"; }), read by the track builder before the name heuristics. The heuristics stay as the fallback, and the model-derived posture gains the truth where it matters.
The sim-time unit gap stays an annotation: the units design keeps
runtime values as floats, so the seam documents seconds-by-convention
and leaves bracket-checking of after arguments to the units arc.
Subscriber inventory¶
subscriber |
seam surface |
status |
|---|---|---|
diagram replay player |
|
exists; wire only |
state-diagram active-state highlight |
the replay widget’s SVG stage (tracks + parents drive it) |
exists inside the player; no separate wiring needed |
Cesium mission replay |
|
trait exists; bridge is phase 2 |
scrubber widget |
its own |
new, phase 1 |
telemetry strip chart |
|
future; the data already rides every |
dashboard what-if over time |
|
future |
deployment-pose animation (geometry design, phase 3) |
poses keyed by the same recorded timeline |
future consumer, not a dependency |
The flagship demo: the go-around sortie¶
The demo composes the seam’s whole story from shipped parts, verified
against 0.10.0. DeepScout::SortieStates carries the go-around trap:
each entry to airborne burns 30% battery, and the go-around
transition re-enters airborne past the launch guard. With a timed
feed (launch, 45.0, goAround, 45.0, goAround, 45.0, goAround,
30.0, land, 5.0):
the recorded battery telemetry steps 100 -> 70 -> 40 -> 10 -> -20, crossing the
SafeSortiefloor between t = 90 and t = 135;the track flies the loop for 165 s and lands (verified:
duration = 170.0, phasesroutethenground);one clock scrubs all three surfaces: the state diagram pulses the
airborne -> airborneedge at t = 45, 90, 135 while the craft loops on the globe, and the scrubber’s telemetry readout shows the pack going negative.
Two honest details the demo teaches. First, the name heuristics do not
classify airborne (no _PHASE_RULES hint matches), so the demo
passes phases={"airborne": "route"} – exactly the gap the
MissionPhase metadata closes. Second, the sequence exactly as
verify.sequences finds it (launch, goAround, goAround, goAround,
no clock advances) is a pure cascade: it records in step mode and
exercises the degradation path (step-scrubbing, no globe), which the
demo shows deliberately before the timed feed.
Performance envelope¶
Sync rate. Both existing widgets throttle trait writes to ~4 Hz (250 ms), and the seam keeps that number for reconciliation. Followers animate locally at frame rate, so the 4 Hz bound governs drift correction, not smoothness. The recommended drift tolerance is 0.25 axis units.
Scrub granularity. Timeline keys round to 3 decimals (1 ms), the replay slider steps at
span / 500, and the clock’s coalescing tolerance is1e-3. A 168 s mission scrubs at ~0.3 s per slider step.Per-seek cost. The replay draw is one binary search per state track plus CSS class toggles (the shipped machines have 4-6 tracks); the Cesium seek is one
requestRenderunderrequestRenderMode. Both are already interactive at slider rate.Event density. The shipped demos are tiny (3-4 fired records, 4-5 env steps). The structures scale: keyframe lookup is logarithmic, fired-record matching precomputes once at render, and timelines with thousands of keyframes stay within the payload discipline
to_json’s rounding established. The scrubber’s tick marks should thin above ~100 events (draw a density band instead).Headless truth. The clock and
link_timeare pure kernel code. Tests driveseek/playand assert trait fan-out, mirroring how the selection seam is tested without a browser.
What we deliberately do not build¶
No kernel-side timer. The kernel never runs a wall-clock loop (no asyncio scheduler in a notebook kernel); views animate, the kernel holds state. A headless
play()therefore movestonly when a front-end or a test advances it.No implicit global clock. Linking is explicit and scoped, like
link_selection. Two dashboards in one notebook keep two clocks.No cross-recording synchronization. One clock serves one timebase. Comparing two sorties under one playhead is a later design, and it starts from this one’s axis discipline.
No playhead persistence. The view-persistence design’s decision 5 (selection is transient focus, not configuration) applies verbatim to the playhead.
No timezone machinery. Epochs are UTC datetimes, as
mission3d._isoalready enforces.No Cesium vendoring change. The bridge is a few lines in the existing ESM; the CDN posture stands.
Phasing¶
The finish-then-tag posture holds, and the seam yields to the standing arcs: the curriculum owns the 0.12 headline, provenance layers 1-2 follow it, and geometry phase 1 opens 0.13.
Phase 1 – the kernel and the player (the smallest honest slice).
Timebase+ thetrack_from_timelinesplit,Clock,link_time, the scrubber widget, andtimeline=/track=parameters on the two builders. Deliverable: the diagram replay and the scrubber scrub together, headless tests prove the fixpoint discipline, and the go-around demo runs in step mode.Phase 2 – the globe. The Cesium bridge additions (
shouldAnimate/multipliertraits, drift reconciliation), the timed go-around demo, a clock row ingrand_dashboard, and the tutorial 9 epilogue that scrubs the diagram and the globe together.Phase 3 – the model binding and telemetry.
MissionPhasemetadata, the model epoch, demos onmodel_waypoints, and the telemetry strip chart overenv_steps. The geometry design’s deployment animation arrives later as a consumer.
Each phase is independently shippable. Phase 1 alone delivers the seam’s core: one clock, many views, no echo.
Open questions¶
Clock ownership: a dedicated kernel object, or traits on a hub widget (the scrubber)? A hub widget would conflate state with chrome and die with its DOM node. Recommendation: the dedicated dependency-free
Clock; the scrubber is one subscriber among equals.Cesium bridge direction-of-truth: single driver, or bounded drift? A single-driver rule (whoever pressed play owns
t) is simpler but fights the Cesium dial, which users will press. Recommendation: shared-rate local integration with bounded-drift reconciliation at ~4 Hz and exact convergence on pause.Step-only traces: refuse the globe, or fabricate seconds? Recommendation: refuse by default; an explicit
seconds_per_stepopts in and the scrubber labels the axis synthetic. Never blend a fabricated second silently.Where does the model state the trace-to-mission binding? Recommendation: waypoints via
model_waypointsand an epoch attribute typedTime::Iso8601DateTimenow (both on existing seams);MissionPhasemetadata inanalysis_conventions.sysmlin phase 3 to retire the name heuristics.Toolkit placement: where do
Clock,link_time, and the scrubber live? Candidates: growlongeron.replay, a sibling ofanalysis/link.py, or a new module. Recommendation: a newlongeron.widgetsmodule for the three toolkit pieces (they serve core replay and analysis alike, and neither existing home covers both); the Cesium bridge stays inmission3dwith its ESM.Timing against the 0.12 arcs? The seam is small but touches two widget front-ends. Recommendation: phase 1 lands as a short arc after provenance layers 1-2 within 0.12, phase 2 immediately behind it; phase 3 rides the 0.13 geometry arc, whose deployment animation wants the clock anyway.
Does the playhead join the selection seam? A playhead
tselects the occurrence individuals alive att, which could drive the inspector. Recommendation: not in this design; record it as the natural follow-on once both seams are stable.
References¶
Longeron surfaces:
longeron.replay(Timeline,record_timeline,replay_widget),longeron.analysis.mission3d(MissionTrack,from_replay,mission_viewer),longeron.interpreter(StateMachine.now,advance, trigger deadlines),longeron.m0(from_timeline),longeron.analysis.linkandlongeron.diagrams.on_select()(the selection seam’s mechanics),longeron.analysis.grand(the composed dashboard,FLIGHT_EVENTS,ATLANTA_LOOP).Vendored stdlib:
src/longeron/_stdlib/quantities/Time.sysml(Clock,TimeOf,TimeScale,UTC,Iso8601DateTime),KernelShim.sysml(Occurrences), grammar ruletriggerExpression(SysML.g4).OMG Systems Modeling Language (SysML) v2.0, Part 1: state machines and accept triggers (
at/after/when), §9.8 Quantities and Units (theTimepackage,TimeScale, ISO 8601 representations).Sibling designs: units (floats-only invariant, vendoring posture), geometry (phasing precedent, the deployment-animation consumer), provenance (the 0.12 arc it queues behind), view persistence (decision 5, transient focus), the notebooks rebuild (tutorial ownership of the seams).
Verified versions: longeron 0.10.0 at
ad27a8b; CesiumJS 1.144.0 (the pinned CDN releasemission3dloads).