Skip to content

Commit ea36346

Browse files
sean2077OmX
andcommitted
Expand schema v1 into test-backed reference
Constraint: Next-stage plan requires schema reference sufficient to write valid graphs from docs Rejected: Introduce schema v2 now | current v1 behavior can be documented without breaking format Confidence: high Scope-risk: narrow Directive: Keep invalid fixtures aligned with docs/schema-v1.md examples Tested: ./scripts/agent_check.sh (28/28 CTest tests); git diff --check Not-tested: External schema-generation tooling Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent 6ddf43c commit ea36346

4 files changed

Lines changed: 324 additions & 8 deletions

File tree

CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ if(TOPOEXEC_BUILD_TESTING AND BUILD_TESTING)
193193
COMMAND topoexec graph validate ${CMAKE_CURRENT_SOURCE_DIR}/examples/invalid_unknown_field.yaml
194194
)
195195
set_tests_properties(cli_reject_invalid_unknown_field PROPERTIES WILL_FAIL TRUE)
196+
add_test(NAME cli_reject_missing_edge_kind
197+
COMMAND topoexec graph validate ${CMAKE_CURRENT_SOURCE_DIR}/examples/invalid_missing_edge_kind.yaml
198+
)
199+
set_tests_properties(cli_reject_missing_edge_kind PROPERTIES WILL_FAIL TRUE)
200+
add_test(NAME cli_reject_immediate_cycle_without_loop
201+
COMMAND topoexec graph validate ${CMAKE_CURRENT_SOURCE_DIR}/examples/invalid_immediate_cycle.yaml
202+
)
203+
set_tests_properties(cli_reject_immediate_cycle_without_loop PROPERTIES WILL_FAIL TRUE)
196204
add_test(NAME cmake_package_runtime_smoke
197205
COMMAND ${CMAKE_COMMAND}
198206
-DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}

docs/schema-v1.md

Lines changed: 264 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,274 @@
11
# Schema Version 1
22

3-
Required root fields:
3+
Schema v1 describes a single-process TopoExec runtime graph. The loader is strict: unknown fields are rejected at the root and inside known sections.
4+
5+
Runtime visibility rules for edge kinds, epochs, transactions, commits, triggers, and CompositeLoop ownership are defined in [runtime-semantics.md](runtime-semantics.md).
6+
7+
## Root
8+
9+
Required fields:
410

511
- `schema_version: 1`
612
- `graph`
713
- `lanes`
814
- `components`
915
- `edges`
1016

11-
Important semantics:
17+
Optional fields:
18+
19+
- `composite_loops`
20+
21+
Allowed root fields are exactly `schema_version`, `graph`, `lanes`, `components`, `edges`, and `composite_loops`.
22+
23+
## graph
24+
25+
```yaml
26+
graph:
27+
name: minimal
28+
kind: runnable
29+
clock:
30+
runtime_domain: steady
31+
event_domain: steady
32+
```
33+
34+
Fields:
35+
36+
- `name` required string.
37+
- `kind` optional string, default `runnable`; allowed values are `runnable` and `internal_test`.
38+
- `clock.runtime_domain` optional string, default `steady`; only `steady` is currently accepted.
39+
- `clock.event_domain` optional string, default `steady`; allowed values are `steady`, `system`, `device`, and `external`.
40+
41+
## lanes
42+
43+
`lanes` is a mapping from lane id to lane configuration:
44+
45+
```yaml
46+
lanes:
47+
main:
48+
type: event_loop
49+
hz: 100
50+
max_callback_ms: 5
51+
```
52+
53+
Fields:
54+
55+
- `type` required string; allowed values are `event_loop`, `fixed_rate`, and `thread_pool`.
56+
- `hz` optional number, default `0`.
57+
- `priority` optional string, default empty.
58+
- `max_callback_ms` optional integer, default `0`.
59+
- `max_threads` optional integer, default `0`; must be non-negative.
60+
- `thread_name` optional string.
61+
- `cpu_affinity` optional integer array.
62+
- `nice_priority` optional integer, default `0`.
63+
- `rt_policy` optional string, default `none`.
64+
- `rt_priority` optional integer, default `0`.
65+
- `isolation_intent` optional string, default `none`.
66+
67+
## components
68+
69+
`components` is a sequence. Each component must have an id, type, event sources, trigger policy, and execution lane.
70+
71+
```yaml
72+
components:
73+
- id: transform
74+
type: topoexec.transforms.Identity
75+
event_sources:
76+
- type: message
77+
inputs: [in]
78+
trigger_policy:
79+
type: any_input
80+
inputs: [in]
81+
execution:
82+
lane: main
83+
boundary:
84+
role: processing
85+
config:
86+
gain: 1
87+
```
88+
89+
Component fields:
90+
91+
- `id` required string; must be unique.
92+
- `type` required string.
93+
- `event_sources` optional sequence, default `[{type: manual}]`.
94+
- `trigger_policy` optional mapping, default `{type: manual}`.
95+
- `execution` required mapping.
96+
- `depends_on` optional string array; lifecycle dependencies must form a DAG.
97+
- `boundary` optional mapping.
98+
- `config` optional mapping of component-specific values.
99+
100+
### event_sources[]
101+
102+
Allowed fields:
103+
104+
- `id` optional string.
105+
- `type` optional string, default `manual`; allowed values are `manual`, `message`, `timer`, `request`, `action_goal`, `action_cancel`, `task_ready`, and `future_ready`.
106+
- `inputs` optional string array.
107+
- `input` optional string shorthand for one input.
108+
- `period_ms` optional integer; required positive value for `timer`.
109+
110+
`message` sources require at least one input.
111+
112+
### trigger_policy
113+
114+
Allowed fields:
115+
116+
- `type` optional string, default `manual`; allowed values are `manual`, `on_event`, `any_input`, `all_inputs`, `time_sync`, `batch`, `request`, and `task_ready`.
117+
- `inputs` optional string array.
118+
- `input` optional string shorthand for one input.
119+
- `batch_size` optional non-negative integer.
120+
- `batch_window_ms` optional non-negative integer.
121+
- `sync_slop_ms` optional non-negative integer.
122+
- `min_interval_ms` optional non-negative integer.
123+
- `max_latency_ms` optional non-negative integer.
124+
- `coalesce` optional boolean, default `false`.
125+
126+
Input-driven trigger policies require incoming edges for every listed input. `batch` requires `batch_size` or `batch_window_ms`.
127+
128+
### execution
129+
130+
Allowed fields:
131+
132+
- `lane` required string; must reference a lane id.
133+
- `reentrant` optional boolean, default `false`.
134+
- `priority` optional string, default `normal`.
135+
- `budget_ms` optional integer, default `0`.
136+
137+
### boundary
138+
139+
Allowed fields:
140+
141+
- `role` optional string, default `processing`; allowed values are `processing`, `input`, `output`, and `input_output`.
142+
- `descriptor` optional string.
143+
144+
For registry-backed `runnable` graphs, validation requires at least one input boundary and one output boundary.
145+
146+
## edges
147+
148+
`edges` is a sequence. Every edge must declare an explicit kind.
149+
150+
```yaml
151+
edges:
152+
- id: source_to_transform
153+
kind: immediate
154+
from: source.out
155+
to: transform.in
156+
policy:
157+
mode: queue
158+
capacity: 4
159+
overflow: drop_oldest
160+
copy_policy: shared_view
161+
```
162+
163+
Edge fields:
164+
165+
- `id` required string; must be unique.
166+
- `kind` required string; allowed values are `immediate`, `delay`, `state`, and `async`.
167+
- `from` required endpoint string, usually `component.output`.
168+
- `to` required endpoint string, usually `component.input`.
169+
- `policy` optional mapping.
170+
171+
Only `immediate` edges participate in immediate SCC analysis. Immediate cycles are invalid unless they exactly match one `composite_loops[]` entry. `delay`, `state`, and `async` edges break same-transaction feedback and become visible at a later epoch boundary.
172+
173+
### policy
174+
175+
Allowed fields:
176+
177+
- `mode` optional string, default `latest`; allowed values are `latest`, `queue`, `ring_buffer`, `latched`, `barrier`, and `previous_tick`.
178+
- `capacity` optional positive integer, default `1`.
179+
- `overflow` optional string, default `overwrite`; allowed values are `overwrite`, `drop_oldest`, `drop_newest`, `block`, `fail_fast`, and `reject`.
180+
- `lifespan_ms` optional integer, default `0`.
181+
- `deadline_ms` optional integer, default `0`.
182+
- `preserve_order` optional boolean, default `true`.
183+
- `allow_drop` optional boolean, default `true`.
184+
- `emit_health_events` optional boolean, default `true`.
185+
- `timestamp_domain` optional string, default `steady`; allowed values are `steady`, `system`, `device`, and `external`.
186+
- `copy_policy` optional string, default `copy`; allowed values are `copy`, `shared_view`, `loaned_view`, and `move_only`.
187+
- `owner` optional string, default `runtime`; allowed values are `producer`, `runtime`, and `consumer`.
188+
- `readers` optional string, default `single`; allowed values are `single` and `multi`.
189+
190+
Latest-style modes (`latest`, `latched`, `previous_tick`) cannot use `drop_newest` or `block`. `move_only` requires `readers: single`. State edges currently reject multiple writers to the same target endpoint.
191+
192+
## composite_loops
193+
194+
`composite_loops` is optional. Each entry must exactly match one immediate cyclic SCC.
195+
196+
```yaml
197+
composite_loops:
198+
- id: estimator_controller_loop
199+
components: [estimator, controller]
200+
loop_policy:
201+
type: fixed_point
202+
max_iterations: 3
203+
budget_ms: 5
204+
```
205+
206+
Fields:
207+
208+
- `id` required string.
209+
- `components` required non-empty string array.
210+
- `loop_policy` required mapping.
211+
212+
Loop policy fields:
213+
214+
- `type` required string; allowed values are `fixed_point`, `transaction`, `coalesced_event`, and `async_task`.
215+
- `budget_ms` optional non-negative integer.
216+
- `max_iterations` optional non-negative integer.
217+
- `max_inflight` optional non-negative integer.
218+
- `drop_policy` optional string.
219+
- `min_interval_ms` optional non-negative integer.
220+
- `convergence` optional string.
221+
222+
Current runtime execution is strongest for `fixed_point`; richer async and worker-pool policies remain future scope.
223+
224+
## Valid Minimal Example
225+
226+
```yaml
227+
schema_version: 1
228+
graph: {name: minimal, kind: runnable}
229+
lanes: {main: {type: event_loop}}
230+
components:
231+
- id: source
232+
type: topoexec.boundary.Input
233+
boundary: {role: input}
234+
event_sources: [{type: manual}]
235+
trigger_policy: {type: manual}
236+
execution: {lane: main}
237+
- id: sink
238+
type: topoexec.boundary.Output
239+
boundary: {role: output}
240+
event_sources: [{type: message, inputs: [in]}]
241+
trigger_policy: {type: any_input, inputs: [in]}
242+
execution: {lane: main}
243+
edges:
244+
- id: source_sink
245+
kind: immediate
246+
from: source.out
247+
to: sink.in
248+
policy: {mode: latest, copy_policy: shared_view}
249+
```
250+
251+
## Invalid Examples
252+
253+
Missing edge kind:
254+
255+
```yaml
256+
edges:
257+
- id: source_sink
258+
from: source.out
259+
to: sink.in
260+
```
261+
262+
Immediate cycle without a matching CompositeLoop:
263+
264+
```yaml
265+
edges:
266+
- {id: ab, kind: immediate, from: a.out, to: b.in}
267+
- {id: ba, kind: immediate, from: b.out, to: a.in}
268+
```
269+
270+
Both cases are covered by CLI validation fixtures under `examples/invalid_*.yaml`.
271+
272+
## Versioning
12273

13-
- `edges[].kind` is required and must be `immediate`, `delay`, `state`, or `async`.
14-
- Immediate cycles are invalid unless they exactly match a `composite_loops[]` component set.
15-
- `immediate` edges participate in compile-time SCC analysis; `delay`, `state`, and `async` edges do not create immediate SCCs.
16-
- Runtime visibility rules for edge kinds, epochs, transactions, commits, triggers, and CompositeLoop ownership are defined in [runtime-semantics.md](runtime-semantics.md).
17-
- `boundary.role` is generic and may be `processing`, `input`, `output`, or `input_output`.
18-
- Unknown root and section fields are rejected.
274+
Schema v1 is strict and compatibility-preserving. Additive fields require a schema update only when v1 validation or runtime meaning would change. Breaking semantic changes should bump the schema version rather than silently changing v1 behavior.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
schema_version: 1
2+
graph:
3+
name: invalid_immediate_cycle
4+
kind: internal_test
5+
lanes:
6+
main:
7+
type: event_loop
8+
components:
9+
- id: a
10+
type: topoexec.test.A
11+
event_sources: [{type: message, inputs: [in]}]
12+
trigger_policy: {type: any_input, inputs: [in]}
13+
execution: {lane: main}
14+
- id: b
15+
type: topoexec.test.B
16+
event_sources: [{type: message, inputs: [in]}]
17+
trigger_policy: {type: any_input, inputs: [in]}
18+
execution: {lane: main}
19+
edges:
20+
- id: ab
21+
kind: immediate
22+
from: a.out
23+
to: b.in
24+
policy: {mode: latest, copy_policy: shared_view}
25+
- id: ba
26+
kind: immediate
27+
from: b.out
28+
to: a.in
29+
policy: {mode: latest, copy_policy: shared_view}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
schema_version: 1
2+
graph:
3+
name: invalid_missing_edge_kind
4+
kind: internal_test
5+
lanes:
6+
main:
7+
type: event_loop
8+
components:
9+
- id: source
10+
type: topoexec.boundary.Input
11+
event_sources: [{type: manual}]
12+
trigger_policy: {type: manual}
13+
execution: {lane: main}
14+
- id: sink
15+
type: topoexec.boundary.Output
16+
event_sources: [{type: message, inputs: [in]}]
17+
trigger_policy: {type: any_input, inputs: [in]}
18+
execution: {lane: main}
19+
edges:
20+
- id: source_sink
21+
from: source.out
22+
to: sink.in
23+
policy: {mode: latest, copy_policy: shared_view}

0 commit comments

Comments
 (0)