observer-rust-lib 0.1.1

MIT-licensed Rust integration library for Observer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# Observer Rust HOWTO

This manual explains how to use the Rust provider micro-library in `lib/rust` as a real end-to-end Observer integration.

It is not just a macro reference.

It is a user manual for the whole process:

- how to author tests in Rust
- how deterministic lowering works
- why proc-macro discovery is not the contract
- how provider hosts expose `list` and `run`
- how Observer derives inventory from the host
- how suites run against that inventory
- how to interpret the passing and failing starter examples

If you are new to this surface, read this file before reading the individual Rust examples.

## Quick Start: First 5 Commands

If you want the fastest path to the Rust integration model, start in `lib/rust/starter/` and run:

```sh
make list
make inventory
cat tests.inv
make host-run TARGET='ledger/rejects-overdraft'
make verify
```

That sequence shows the whole contract in order:

- raw provider `list`
- derived canonical inventory
- the exact exported names Observer will use
- one direct provider-target execution
- full snapshot verification of the end-to-end flow

## Choose A Rust Path

Choose the standalone host path when:

- you want a dedicated provider binary
- the application does not already own a CLI surface
- `./host list` and `./host observe ...` are acceptable developer entrypoints

Choose the embedded path when:

- the application already owns `main()`
- you want `myapp observe list` and `myapp observe --target ...`
- Observer should integrate through an app-owned CLI namespace instead of replacing it

Use `lib/rust/starter/` to learn the standalone path first.

Use `lib/rust/starter-embedded/` immediately after that if the real product already has its own CLI.

## 1. What This Library Is

`lib/rust` is a Rust-facing provider micro-library for Observer.

Its job is to let you:

- write tests in ordinary Rust
- collect those tests through a human-first surface
- lower them deterministically into explicit registrations
- expose those registrations through the standard Observer provider protocol
- run Observer suites against the resulting canonical inventory

The important boundary is this:

- authoring is Rust-native
- execution contract is Observer-native

You do not write Observer suites in Rust.

You write Rust tests, then Observer discovers them through the provider host and runs suites against canonical inventory names.

## 2. The Core Mental Model

There are three layers:

1. your Rust code under test
2. your Rust-authored Observer tests
3. the Observer provider host boundary

The normal flow is:

1. write Rust functions
2. write `describe!(...)`, `test!(...)`, or `it!(...)` registrations
3. call `collect_tests(...)`
4. let the library lower those authored tests into explicit registrations
5. run the host with `list` to expose canonical test names and targets
6. let `observer derive-inventory` lower that list into `tests.inv`
7. run `observer run --inventory ... --suite ... --config ...`

This is why the examples are split into:

- direct Rust snippets in `lib/rust/examples/*.rs`
- real end-to-end starter projects in `lib/rust/starter/` and `lib/rust/starter-failure/`

## 3. The Determinism Gate

This library is intentionally human-first, but the determinism gate still wins.

That means:

- authored tests may use `describe!(...)`, `test!(...)`, and `expect(...)`
- the library may derive an identity from explicit suite path plus title
- optional explicit `id = ...` may override that derived identity
- the host boundary still publishes only explicit resolved registrations

What is not allowed as the contract:

- module scanning
- function-name discovery
- source-location-derived external identity
- compiler-order-dependent implicit registration
- proc macros that hide or change the explicit published inventory contract

The normative rule is simple:

- human-first authoring is fine
- heuristic discovery is not

## 4. The Four Practical Operating Modes

There are four practical ways to use this library.

### 4.1 Mode A: Plain collection and direct execution

This is the smallest local smoke path.

Representative file:

- `lib/rust/examples/example_smoke.rs`

This mode demonstrates:

- collection with `collect_tests(...)`
- deterministic lowering and sorting
- direct `run_test(...)`

It does not demonstrate the full Observer CLI flow.

### 4.2 Mode B: Standalone provider host mode

This is the canonical provider-host path.

Representative file:

- `lib/rust/examples/host_example.rs`

This mode exposes:

- `list`
- `run --target <target> --timeout-ms <u32>`
- `observe --target <target> --timeout-ms <u32>`

Use this when:

- you want a dedicated provider host binary
- you are integrating Rust tests into Observer as a standalone provider

### 4.3 Mode C: Embedded host mode

This is the preferred path when your application already has its own CLI.

Representative file:

- `lib/rust/examples/host_embed_example.rs`

In this mode:

- your app keeps its own `main()`
- you route `observe ...` to Observer host dispatch
- normal app behavior remains intact outside the `observe` command

### 4.4 Mode D: Full Observer CLI workflow

This is the mode real users care about most.

Representative directories:

- `lib/rust/starter/`
- `lib/rust/starter-embedded/`
- `lib/rust/starter-embedded-failure/`
- `lib/rust/starter-failure/`

In this mode you do the whole chain:

- compile the provider host with Cargo
- inspect raw host `list` output
- derive inventory with `observer derive-inventory`
- write and run an Observer suite
- verify canonical report snapshots

This is the mode to learn first if your goal is real adoption.

## 5. The Authoring Surface

The intended authored surface is:

```rust
describe!("ledger", {
    test!("rejects overdraft", |ctx| {
        expect(true).to_be_truthy();
        ctx.stdout("denied overdraft\n");
    });
});
```

The common path is:

- `describe!(...)` for grouping
- `test!(...)` or `it!(...)` for human titles
- `expect(...)` for assertions
- `ctx.observe()` for bounded observation

## 6. Derived Identity vs Explicit `id`

By default, the library derives identity mechanically from explicit registration data:

- suite path
- test title
- duplicate occurrence order within the registration stream

That means a test like:

```rust
describe!("ledger", {
    test!("rejects overdraft", |_ctx| {});
});
```

resolves to the canonical inventory name:

```text
ledger :: rejects overdraft
```

If you want a refactor-stable external identity, give the test an explicit `id`:

```rust
test!("rejects overdraft", id = "ledger/rejects-overdraft", |_ctx| {});
```

In this first cut, the resolved identity is used for both:

- canonical inventory name
- provider execution target

The runnable starters use explicit ids deliberately so the end-to-end contract is mechanically obvious.

## 7. `expect(...)` And Observation

`expect(...)` is for authored behavior assertions.

Representative forms currently include:

- `to_be(...)`
- `to_equal(...)`
- `to_contain(...)`
- `to_match(...)`
- `to_be_truthy()`
- `to_be_falsy()`
- `.not()` inversion before the final matcher call

Observation is explicit and bounded:

```rust
let mut observe = ctx.observe();
assert!(observe.metric("wall_time_ns", 104233.0));
assert!(observe.vector("request_latency_ns", &[1000.0, 1100.0, 980.0]));
assert!(observe.tag("resource_path", "fixtures/config.json"));
```

Observation does not change:

- canonical identity
- inventory bytes
- suite hash
- report semantics other than adding observational records

## 8. What `collect_tests(...)` Actually Does

`collect_tests(...)` is not runtime discovery.

It is deterministic lowering.

The library records authored registrations in the order the authoring API defines, resolves identities, validates duplicates, and sorts the materialized registration set before host exposure.

That is why the library satisfies the determinism gate while still feeling ergonomic.

## 9. Provider Host Commands

The Rust library owns the standard provider host transport too.

### 9.1 `list`

`list` emits one JSON object containing:

- provider id
- sorted tests
- canonical names and targets

Representative shape:

```json
{"provider":"rust","tests":[{"name":"ledger/rejects-overdraft","target":"ledger/rejects-overdraft"}]}
```

### 9.2 `run`

`run --target <target> --timeout-ms <u32>` executes one published target and emits one JSON object containing at least:

- provider id
- target
- exit
- stdout as base64
- stderr as base64

For developer-facing usage, prefer `observe`. `run` remains available for compatibility with the standardized provider boundary.

## 10. Standalone Host Example

The direct-host example is in:

- `lib/rust/examples/host_example.rs`

Its shape is intentionally small:

```rust
let tests = collect_tests(|| {
    describe!("pkg", {
        test!("smoke test", id = "pkg::smoke", |ctx| {
            ctx.stdout("ok\n");
            expect(true).to_be_truthy();
        });
    });
})
.expect("collection should validate");

let exit_code = match observer_host_main("rust", &tests) {
    Ok(()) => 0,
    Err(error) => {
        eprintln!("{error}");
        2
    }
};
```

## 11. Embedded Host Example

The embedded-host example is in:

- `lib/rust/examples/host_embed_example.rs`

This is the preferred path when the app already owns its CLI and you want:

```text
myapp observe list
myapp observe --target pkg::embedded-smoke --timeout-ms 1000
```

## 12. The Real Observer CLI Flow

The real end-to-end flow is:

1. build the Rust host
2. inspect raw `list`
3. derive inventory
4. write or inspect `tests.obs`
5. run the suite
6. compare hashes and report snapshots

That is what the starters are for.

## 13. The Passing Starter

`lib/rust/starter/` is the passing reference project.

It shows:

- ordinary Rust code under test in `src/lib.rs`
- Rust-authored Observer tests in `src/bin/ledger-observer-host.rs`
- a standalone provider host binary
- `observer.toml`
- `tests.obs`
- checked-in inventory and report snapshots

## 14. The Embedded Starter

`lib/rust/starter-embedded/` is the app-owned CLI companion.

It shows the same provider contract, but with one important difference:

- the built binary is an application first
- the Observer provider path is routed only when the app is invoked as `observe ...`

This is the project-shaped reference for teams that already own their CLI surface and do not want a dedicated provider-host binary.

## 15. The Embedded Failing Starter

`lib/rust/starter-embedded-failure/` is the failing companion for the app-owned CLI path.

It keeps the same `observe` routing model as `starter-embedded/`, but adds one intentionally wrong exported test so the failing path is as obvious as the passing path.

## 16. The Failing Starter

`lib/rust/starter-failure/` is the failing companion.

It keeps the same real provider flow but adds one intentionally wrong exported test:

- `ledger/broken-running-total`

That makes the whole chain easier to understand because you can compare the passing and failing starters side by side.

## 17. `observer.toml` For Rust Providers

The starter config shape is:

```toml
version = "0"

[providers.rust]
command = "./build/target/debug/ledger-observer-host"
cwd = "."
inherit_env = false
```

That tells Observer exactly which host binary to invoke.

The embedded starter uses the same config model, but adds provider args so Observer calls the app through its routed command namespace:

```toml
version = "0"

[providers.rust]
command = "./build/target/debug/ledger-app"
args = ["observe"]
cwd = "."
inherit_env = false
```

## 18. Writing `tests.obs` For Rust Providers

The starters use the simple suite surface.

Representative shape:

```observer
test prefix: "ledger/" timeoutMs: 1000: expect exit = 0.

test "ledger/rejects-overdraft" timeoutMs: 1000: [
	expect exit = 0.
	expect out contains "denied overdraft".
].
```

The suite talks only about canonical inventory names, not Rust module names or function symbols.

## 19. What A Raw `list` Response Looks Like

From `lib/rust/starter/`, `make list` yields a provider response shaped like:

```json
{"provider":"rust","tests":[
  {"name":"format/renders-balance-line","target":"format/renders-balance-line"},
  {"name":"ledger/applies-ordered-postings","target":"ledger/applies-ordered-postings"},
  {"name":"ledger/rejects-overdraft","target":"ledger/rejects-overdraft"}
]}
```

Observer then lowers that into inventory lines shaped like:

```text
#format/renders-balance-line provider: "rust" target: "format/renders-balance-line"
#ledger/applies-ordered-postings provider: "rust" target: "ledger/applies-ordered-postings"
#ledger/rejects-overdraft provider: "rust" target: "ledger/rejects-overdraft"
```

## 20. What A Raw `run` Response Looks Like

A passing target looks like this shape:

```json
{"provider":"rust","target":"ledger/rejects-overdraft","exit":0,"out_b64":"ZGVuaWVkIG92ZXJkcmFmdAo=","err_b64":""}
```

A failing target looks like this shape:

```json
{"provider":"rust","target":"ledger/broken-running-total","exit":1,"out_b64":"","err_b64":"Li4u"}
```

That means:

- the provider call itself succeeded structurally
- the target ran
- the test outcome failed

This distinction matters when the suite later says `expect exit = 0`.

### 20.1 Passing Walkthrough: Raw Host To Suite Verdict

Use `lib/rust/starter/` for this walkthrough.

The flow is:

1. build the host
2. inspect `list`
3. derive inventory
4. run one target directly
5. run the suite

At the raw host boundary, `make list` gives you the published canonical names and targets.

Observer then derives inventory entries for those names.

Now the suite talks only about those canonical names.

For example, this suite item:

```observer
test "ledger/rejects-overdraft" timeoutMs: 1000: [
	expect exit = 0.
	expect out contains "denied overdraft".
].
```

drives Observer to:

1. resolve `ledger/rejects-overdraft` in inventory
2. call the provider host with `run --target ledger/rejects-overdraft --timeout-ms 1000`
3. decode the returned `out_b64`
4. assert over the canonical run result

### 20.2 Failing Walkthrough: Raw Host Failure To Suite Failure

Use `lib/rust/starter-failure/` for this walkthrough.

The key target is:

```text
ledger/broken-running-total
```

At the host boundary, a direct run returns a normal structured response with a failing exit code.

That means:

- the provider host did its job
- the target really ran
- the Rust test itself failed

When Observer later runs the suite, it records a normal `run` action for that case with `exit = 1`.

The suite then fails because its assertion contract says `expect exit = 0`.

## 21. How To Start A New Rust Integration

1. Write ordinary Rust code under test.
2. Add authored tests using `describe!(...)`, `test!(...)`, and `expect(...)`.
3. Decide whether derived identity is enough or whether you need explicit `id`.
4. Call `collect_tests(...)` and validate the resulting set.
5. Expose a standalone host or embedded `observe` subcommand.
6. Confirm raw provider behavior with `list` and direct target runs.
7. Add `observer.toml`.
8. Derive inventory.
9. Write suites against canonical inventory names.
10. Freeze hashes and report snapshots.

## 22. Common Mistakes

### Mistake 1: treating macros as discovery magic

The macros are only the authored surface.

The contract is the lowered explicit registration set.

### Mistake 2: treating Rust symbols as the external contract

Observer runs canonical published names and targets.

It does not care what your internal function names were.

### Mistake 3: skipping the raw host check

If you have not checked raw `list` and raw direct target execution, you are debugging too high in the stack.

### Mistake 4: using derived identity when you really need a stable external id

If renaming suite labels or titles should not change the published contract, use explicit `id`.

### Mistake 5: hiding the provider path inside app-specific CLI behavior

Make the `observe` routing point explicit if the application owns the outer CLI.

### Mistake 6: not snapshotting the failing path

A failing example is not second-class.

That is why `starter-failure/` exists.

### Troubleshooting Checklist

If the integration is not working, check the layers in this order.

1. Collection: confirm `collect_tests(...)` succeeds and the host binary builds.
2. Raw host list: run `./build/target/debug/ledger-observer-host list` and confirm tests appear in sorted order.
3. Raw host run: run `./build/target/debug/ledger-observer-host observe --target <target> --timeout-ms 1000` and confirm you get valid JSON.
4. Inventory derivation: run `observer derive-inventory --config observer.toml --provider rust > tests.inv` and inspect the resulting names.
5. Suite contract: confirm `tests.obs` refers to canonical inventory names, not Rust module or function names you remember informally.
6. Failure category: decide whether the problem is host failure, target failure, or suite assertion failure before changing code.
7. Snapshot drift: regenerate inventory hash, suite hash, and report snapshots only after the behavior is understood and accepted.

## 23. Quick Reference

### Authoring forms

- `collect_tests(...)`
- `describe!(...)`
- `test!(...)`
- `it!(...)`
- `expect(...)`

### Observation helpers

- `ctx.observe().metric(...)`
- `ctx.observe().vector(...)`
- `ctx.observe().tag(...)`

### Host helpers

- `observer_host_main(...)`
- `observer_host_main_from(...)`
- `observer_host_dispatch(...)`
- `observer_host_dispatch_embedded(...)`
- `observer_host_handles_command(...)`

## 24. What To Read Next

- `lib/rust/README.md`
- `lib/rust/starter/README.md`
- `lib/rust/starter-embedded/README.md`
- `lib/rust/starter-embedded-failure/README.md`
- `lib/rust/starter-failure/README.md`
- `specs/12-rust-provider-determinism.md`
- `specs/13-provider-authoring.md`