8bac01f2a2
Implementation-side canonical mirror of the design thread. Records the ratified decisions plus seven amendments raised against the register: itemKey live in v1, derived/resource state split, state-neutral prewarm, mode-owned drop policy, authoring-time label validation + instance sets in the format, deterministic/observational probe fields, static instancing.
208 lines
14 KiB
Markdown
208 lines
14 KiB
Markdown
# Vision Graph — Decisions Register
|
|
|
|
Implementation-side canonical mirror of the decisions ratified in the `Vision graph group`
|
|
design thread. Useful-bin owns the spec prose; this file is the contract the implementation
|
|
is built against, maintained by Vision graph (implementer).
|
|
|
|
**Source of truth:** the group thread. Sections are pasted whole into the group; this file
|
|
mirrors them so the implementation has one authoritative copy under version control.
|
|
|
|
## Reference cases
|
|
|
|
| id | case | what it tests |
|
|
|----|------|---------------|
|
|
| A | Useful-bin's FrontierPlacement pipeline | **fidelity** — golden event/CSV parity against the existing pipeline |
|
|
| B | Vision builder's `blob-find → ROI-repivot → edge-intersect` (N=1) | **geometry** — expressible with typed ports instead of a global reference system |
|
|
| C | Vision builder's `LeerformRecognitionControl` | **expressiveness** — could the client that abandoned the engine have stayed in it? |
|
|
|
|
Case C matters most as a guard: an inexpressive model is not extended, it is *escaped*, and
|
|
escaping costs the editor, per-node inspection, hot reload and operator tuning in one step.
|
|
Two production clients are running that way today (`LindtLeerformPlugin`, `CandyboxPlugin`
|
|
— zero references to `WorkflowList` or `BaseOperation`, returning `errorNames: []`).
|
|
|
|
---
|
|
|
|
## Ratified decisions
|
|
|
|
- **D1 — Execution.** Push-based frame-lockstep synchronous tick, ordered fan-out. The
|
|
normative requirement is *in-order, gap-free delivery to stateful nodes within a session*;
|
|
lockstep is one implementation of that guarantee, not the guarantee itself. The tick never
|
|
blocks on I/O.
|
|
- **D2 — Ports.** `frame` ports (0-or-1 typed value per tick: image, mask, scalar, point-set,
|
|
detection, geometry) and `event` ports (0..N stamped records per tick). No third category
|
|
for geometry — typed `frame` ports subsume it. Overlay must never be the sole carrier of
|
|
data.
|
|
- **D3 — Fan-out.** No dynamic map/subgraph operator in v1. Probes and verdicts are addressed
|
|
as `(nodeId, itemKey?)`. *See A1 — the register's "itemKey always null in v1" is contested.*
|
|
- **D4 — Node contract.** `(params, inputs, state) → (outputs, state', verdict, overlay)`,
|
|
pure apart from declared state. Isolation-testability is an explicit spec goal.
|
|
*See A2 — `state` must be split.*
|
|
- **D5 — Verdict.** `execution: ran | skipped{reason: gated | disabled | upstream-unavailable}
|
|
| error` always present; `judgment: pass | reject` optional, inspection nodes only; plus a
|
|
human-readable status sentence and mandatory per-node `durationMs` every tick. `skipped`
|
|
never aggregates as `pass`.
|
|
- **D6 — Buffers.** Edge values immutable, tick-scoped, engine-owned. "Copy what you keep" is
|
|
a stated node obligation. `mutates-input` ⇒ private copy when the producing port has >1
|
|
consumer; pure readers share (read-only broadcast to many consumers is normal and free).
|
|
Bounded history on an input port is a *declared* retention, bounded in both frames and
|
|
bytes, maintained once per edge and shared by all declaring consumers.
|
|
- **D7 — Two warm-up phases, never merged.** `prewarm` (per-process resource readiness) and
|
|
`prime` (per-session stateful convergence over real frames, all sinks suppressed).
|
|
*See A3 — `prewarm` must be state-neutral by construction.*
|
|
- **D8 — Absent values.** Declared **per input port**: `absent→skip | last-value (with
|
|
staleness bound) | error`. Gating (dynamic, "no value this tick" is normal) is a *different
|
|
mechanism* from `Enabled=false` (static authoring config ⇒ pre-run validation error unless
|
|
the node declares pass-through). Never merge them. The declaration must be visible in the
|
|
editor next to the port — the person who gets this wrong is an operator disabling a node at
|
|
3am, not the graph's author.
|
|
- **D9 — Manifests.** Param kinds: scalar, enum, bool, string, expression, roi, reference,
|
|
file, lut, curve. Plus constraints, defaults, output-only flag, and per-param change class
|
|
`live | resets-node-state | requires-rebuild`. Spatial params carry explicit coordinate
|
|
space (`image-absolute | relative-to-connected-port`) and normalization (`normalized |
|
|
pixels`). Persistence is generic from the manifest — nodes never hand-write it. One
|
|
versioned format. Stable Guid ≠ label. *See A5 — label validation and instance sets.*
|
|
- **D10 — Expressions.** Kept for scalar arithmetic. Identifiers resolve **only** to the
|
|
node's own connected ports plus graph config, by id, scoped to one execution. Never to
|
|
another node by display label.
|
|
- **D11 — Introspection record.** `{id, itemKey?, execution, judgment?, status, durationMs,
|
|
outputs, overlayElements, preview?}`. Overlays are renderer-agnostic draw commands.
|
|
Previews are opt-in subscriptions, throttled, copied at emit — zero subscribers ⇒ zero cost
|
|
*by construction*. Scalar time-series is a first-class preview type. Execute-up-to-node
|
|
included and honours D7 phases.
|
|
- **D12 — Replay-first.** The recorded probe log *is* the serialization of the inspection
|
|
contract; live attach is the same format with a tail; the log doubles as the golden record.
|
|
Per-probe decimation policy; large blobs stored out-of-line by reference.
|
|
- **D13 — Determinism.** Event-level equality, not bitwise, given a deterministic source.
|
|
Replay *forces* drop-policy off. Events ordered and stamped by `frameIndex`/`streamTime`;
|
|
`wallClock` provably source-derived or excluded. Determinism is a declared execution mode.
|
|
*See A4 and A6.*
|
|
- **D14 — Session generation.** Watch mode is a runner-level work source, not a graph node.
|
|
The runner owns durable cross-session state (processed keys, append handles, spools), which
|
|
keeps "node state never crosses sessions" true as written. Sources declare
|
|
`continuous | per-item` session semantics.
|
|
- **D15 — Source capabilities as data.** `seekable`, `triggerable | free-running`, session
|
|
semantics, native rate/format/resolution, drop policy. Drops always counted and reported —
|
|
never a silent `DropOldest`. *See A4 — policy is mode-governed, not source-chosen.*
|
|
- **D16 — Config layering.** `defaults < YAML < env < per-run` is authoring-time. Export fully
|
|
resolves into a frozen self-contained artifact with provenance; the **resolved artifact** is
|
|
the unit of reproducibility. Overrides hitting a `resets-node-state` param are flagged as
|
|
state resets. Environment-resolved values (e.g. codec fallback `avc1→mp4v`) are reported,
|
|
never silent, and captured in the run record.
|
|
- **D17 — Granularity.** Node boundaries are tuning/reuse boundaries; probe boundaries are
|
|
visibility boundaries. Never split a node to see inside it — add a probe. A node may be
|
|
internally coarse when it declares batching.
|
|
- **D18 — Layering.** engine (no UI dependency) → runtime driver → shells. Headless is the
|
|
same driver with nobody subscribed. Hot reload: params by node id, debounced; structural
|
|
edits rebuild; D9 change classes govern state.
|
|
- **D19 — v2 reservations v1 must not preclude.** Map/subgraph keyed by item identity;
|
|
identity as a first-class typed value with declared provenance; subgraph-diff rebuilds;
|
|
parallel branches. *See A7 — add static instancing, ranked first.*
|
|
- **D20 — Acceptance tests.** Reference cases A, B, C above.
|
|
|
|
---
|
|
|
|
## Open amendments (raised by Vision graph, pending ratification)
|
|
|
|
- **A1 — `itemKey` is populated in v1, not null.** D3's "always null in v1" contradicts two
|
|
live requirements. Useful-bin's `MetalScorer` emits one verdict *per placement*, and "see
|
|
the results of each node" for that node means a placement-addressed list. Vision builder's
|
|
`ArrayModelMatchingOperation` ships `Dictionary<int,bool> IsGood` — per-cell verdicts, keyed
|
|
by grid index, in production today — which the UI cannot surface, showing only an `All()`
|
|
rollup so the operator cannot see which cell failed. Fixed-N addressing is needed *without*
|
|
a map operator. Only *dynamic-map* item keys are absent from v1. Left as "always null" the
|
|
field ships dead, nothing populates it, no UI renders it, and reference case A's core
|
|
requirement fails.
|
|
- **A2 — Split declared state into `derived` and `resource`.** D4's single `state` bucket is
|
|
unimplementable for 14 of Vision builder's 53 operations (`InferenceSession`,
|
|
`YoloV8Predictor`, NetMQ sockets, a spawned Docker container). `derived` state (MOG2 model,
|
|
LK previous pyramid, latch counters, metal accumulator) is learned from the frame sequence,
|
|
cannot be reconstructed from params, and **must serialize**. `resource` state is
|
|
reconstructible from params and **must not serialize** — it is rebuilt by `prewarm`. A node
|
|
snapshot is `derived` only; restoring means `prewarm` then load `derived`. This is also what
|
|
makes snapshots cheap (serializing a background model, not an ONNX session).
|
|
- **A3 — `prewarm` must be state-neutral by construction.** Vision builder's `WarmUp()`
|
|
executes the whole recipe against an empty source; that is safe *only because he has no
|
|
state*. Run the same design on a graph with MOG2 and the background model trains on a black
|
|
synthetic frame before `prime` starts — priming an already-poisoned model. Fix falls out of
|
|
A2: the node contract separates resource acquisition from tick, `prewarm` invokes only the
|
|
former, and cannot reach `derived` state because it is not yet allocated. ONNX shape
|
|
specialization still requires a real inference at the declared format/resolution, so
|
|
`prewarm` may run inference without invoking the state-update path.
|
|
- **A4 — Drop policy is a property of the execution mode, not of the source.** D15 assigns it
|
|
per-source. Evidence against: *every* Vision builder source hardcodes
|
|
`BoundedChannelOptions(1) { FullMode = DropOldest }`, **including the file-based emulation
|
|
source**, which additionally drains backlog before reading. His replay path silently drops
|
|
frames off disk, so re-running the same folder is not guaranteed to see the same frames.
|
|
Four authors made the same local choice four times because no mode existed to distinguish
|
|
batch from live. Resolution: *capability* is per-source (a live camera cannot block
|
|
indefinitely; a file can), *policy* is chosen by mode and validated against capability —
|
|
so `deterministic` mode is rejected at validation when a source declares it cannot block.
|
|
"Determinism only holds for seekable sources" then falls out as a validation rule rather
|
|
than a caveat someone forgets.
|
|
|
|
| | `deterministic` (batch) | `realtime` |
|
|
|---|---|---|
|
|
| source | seekable | live |
|
|
| `prime` | silent replay pass | eat first N seconds |
|
|
| queue full | **block** | **drop + count** |
|
|
| golden comparison | guaranteed | not claimed |
|
|
|
|
- **A5 — Authoring-time validation, and the format must admit instance sets.** Two additions
|
|
to D9. (i) Label uniqueness *and* identifier-safety validated at authoring time: the shipped
|
|
`gold_b97.hrcp` contains a node labelled `Red wings` — with a space, therefore not a valid
|
|
Python identifier, so any expression referencing it throws at evaluation. It sits at a named
|
|
client, undetected, because nothing happens to reference it. A sibling also kept its default
|
|
label, indistinguishable among thirteen near-identical nodes. (ii) The artifact format must
|
|
be able to represent a node *definition plus configured instance set*, not only flat
|
|
independent node records — otherwise A7 requires a format migration.
|
|
- **A6 — Probe-log fields are marked `deterministic` or `observational`.** Per-node
|
|
`durationMs` is mandatory (D5) and is the instrument clients use to budget cycle time, but
|
|
it is never reproducible — same class as `wallClock`. Golden comparison runs over
|
|
deterministic fields only. Without a declared non-deterministic field set, R13 fails on
|
|
timing noise and someone disables the test.
|
|
- **A7 — Add "static instancing over a declared set" to D19, ranked first.** The v2 fan-out
|
|
question was framed as subgraph-instancing vs batch ports. Production demanded neither.
|
|
`gold_b97.hrcp` is thirteen hand-duplicated `YoloPickDetected` nodes — one per chocolate
|
|
class — each reading the same mask with its own label, own size thresholds, own verdict.
|
|
N is fixed and known at authoring time, so it is not dynamic map; they need thirteen
|
|
*separately addressable* verdicts, so it is not batch ports. The shape is one node
|
|
definition, N configured instances, per-instance params, per-instance verdict, expanded at
|
|
authoring time. It is static (validates like a normal graph), needs no scheduler work,
|
|
reuses the `itemKey` addressing A1 makes live, and fixes the thirteen-copies maintenance
|
|
cost. It is the only one of the three shapes a real recipe has actually demanded.
|
|
|
|
## Consolidated lifecycle
|
|
|
|
```
|
|
build → prewarm(resource only, synthetic frame at declared format+resolution)
|
|
→ sessionBegin → prime(derived convergence, sinks suppressed)
|
|
→ tick* → flush(event-windowed nodes emit) → sessionEnd → dispose
|
|
```
|
|
|
|
`stop` (graceful: finishes tick, runs `flush`, closes sinks, drains spools) and `abort`
|
|
(discard) are distinct signals; any node may raise either and all nodes must observe both.
|
|
`flush` runs on `stop`, not on `abort`. Session generation sits **outside** this loop, in the
|
|
runner.
|
|
|
|
`execute-up-to-node` is *prime-then-prefix*, not pure prefix evaluation, and must be
|
|
**idempotent** — running it twice must not double-train a background model. Requires snapshot
|
|
restore (A2) or a fresh session per invocation; it must never mutate the live session's
|
|
`derived` state.
|
|
|
|
## Identity provenance
|
|
|
|
Identity is a first-class typed value carrying its provenance. The axis is not uniqueness but
|
|
the **strength of the continuity claim**:
|
|
|
|
| provenance | continuity |
|
|
|---|---|
|
|
| grid index | definitional — cannot be wrong |
|
|
| detection id | exact within a frame, asserts nothing across frames |
|
|
| placement / segment id | a detector's assertion — `forcedCut` proves it can be wrong |
|
|
| track id | inferred — swaps, merges, splits, occlusion re-entry |
|
|
|
|
Discontinuity events (appear, disappear, merge, split, forced-cut) must be **observable by
|
|
the node accumulating state**, so it can invalidate rather than silently carry state across.
|
|
State accumulated under a weak continuity claim without this is the same failure class as a
|
|
`NoOrigin` default at (0,0): plausible output, no diagnostic.
|