File size: 67,903 Bytes
f8dbde1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
# deg contract v0.1

Status: contract and schema compiler implemented (`src/deg/schema.py`, `response.py`,
`request.py`, `parsers.py`); model, trainer and in-process runtime implemented
(`model.py`, `train.py`, `infer.py`, `evaluate.py`, design in §12); HTTP server and Python
SDK implemented (`api.py`, `server.py`, `client.py`, §9).

## 1. Model

- Base model: **Qwen3.5-0.8B** (default; LFM2.5-1.2B-Instruct is the alternative
  backbone, §12.8), with three kinds of heads on one trunk:
  - decision heads: `choice`, `score`, `noul` (as in jev v1);
  - a span pointer (start/end over context tokens, plus a null outcome);
  - the LM head, used only for `x-generate` fields.
- Model id: `deg-qwen3.5-0.8b-0.1` (alias `deg-latest`).

## 2. The one flag (and two annotations)

`x-generate: true` is the only extension that changes a field's mode.

- It is allowed only on `type: string` or on `type: array` whose `items` is a plain
  `type: string` (no `enum`, no `format`). On an array, it applies to every item.
- `x-generate: false` means the same as leaving it out.

Two annotations refine a field without changing its mode:

- `x-max-tokens: N` (integer >= 1, only where `x-generate` applies; on an array it applies
  to every item): that field's own decode budget, overriding `options.max_new_tokens`, so
  one request can mix budgets.
- `x-levels: {"<level>": "label", ...}` (only on an integer score field, keys among its
  admissible levels, labels non-empty strings): each labeled level's option line becomes
  `<level>: <label>` (the layout the score head was trained on from jev level criteria);
  unlabeled levels keep the bare number. Use it instead of listing level meanings in the
  description.

Any other `x-*` keyword is an error, so a typo like `x-genrate` fails loudly.

There is **no router head**. A field's mode follows only from its JSON type and keywords
(§3). The model never chooses between deciding, extracting and generating.

## 3. Type → mechanism

| Schema (no `x-generate`) | Mode | Output |
|---|---|---|
| `enum` (any scalar types, 2..255 values; `null` allowed as a value) | `decide.choice` | the value, its probability, the full distribution, confidence |
| `oneOf`/`anyOf` whose branches are all `const`/`enum` | `decide.choice` | the same; branch `description`s become option descriptions |
| `boolean` | `decide.noul` | `p(true)`; the value is `p >= 0.5` |
| `["boolean","null"]` | `decide.choice` over `[true, false, null]` | as choice |
| `integer` whose bounds admit 2..11 values (after `multipleOf`) | `decide.score` | the most probable level, the distribution, the expected level |
| every other number: any `type: number`, or an `integer` with a missing bound or more than 11 admissible values | `extract.number` | a span plus a deterministic number parser; bounds only validate |
| `string` with `format: date` / `date-time` | `extract.date` | a span plus a deterministic date parser, giving an ISO 8601 / RFC 3339 value |
| `string` with `format: email` / `uri` / `uuid` | `extract.span` | a verbatim span that must pass the format check |
| `string` | `extract.span` | a verbatim span |
| `array` | per item | the length is a runtime count; each item uses the item schema's mode |
| `object` | recursive | the properties, in schema order |

| Schema with `x-generate: true` | Mode |
|---|---|
| `string` (optional `minLength`, `maxLength`, `pattern`) | `generate` |
| `array` of `string` | `generate` per item |

Rules of thumb that follow from the table:

- **Number rule.** Only a small integer scale is a judgment: an `integer` whose bounds give
  at most 11 possible values (`1..5`, `0..10`, `0..100` step 25) is decided. Every other
  number is a quantity copied from the context. Bounds (`minimum`, `maximum`, exclusive
  bounds, `multipleOf`) never turn a quantity into a decision; they only validate the
  extracted value. There is no expected-value path for numbers: deg never computes a
  number that is not written in the context. To get a judgment on a finer or non-integer
  scale, use an integer scale or an `enum`.
- Extracted values are always copied from the context. Parsers only normalize them; they
  never invent digits or dates.
- **x-generate fields are the only source of new tokens.**

## 4. Null, optional and constraints

- Nullable means `type: [X, "null"]`, `anyOf`/`oneOf: [X, {type: null}]`, or an `enum`
  that lists `null`. Nullable objects and arrays are not supported.
- Decide fields are always filled, whether optional or not. Nullable score fields get an
  extra null outcome, and nullable booleans become a 3-way choice.
- When an extract field finds nothing, or its span fails the parser or a constraint, the
  plan's `on_null` decides what happens:
  - `null`: the field is nullable, so the value is `null`;
  - `omit`: the field is optional and not nullable, so the key is left out;
  - `forbid`: the field is required and not nullable, so the extractor must return its
    best valid span. If it can't, that is a runtime error (`FillError`, HTTP 500), never a
    made-up value. This is deliberate: authors who want a fallback mark the field
    nullable (`type: [X, "null"]`) and get `null` with a `null_reason` instead.
    **Exception: inside an array item** (anchored or index-addressed, §12.6) a `forbid`
    leaf with no valid value means the item does not exist: the item is dropped (not an
    error), the array's meta says `"dropped": n`, and a `minItems` the drops break is
    handled as in §12.6 (drop the enclosing item, else a `FillError` at the top level).
- `meta.null_reason` is one of `not_found`, `unparsable`, or the name of the violated
  keyword.
- Constraints are enforced as follows:
  - Spans are filtered by `minLength`, `maxLength`, `pattern` (searched, as in JSON
    Schema) and `format`.
  - Numbers are filtered by `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
    and `multipleOf`.
  - Generated text must satisfy `minLength`, `maxLength` and `pattern`. Output that
    violates them is a runtime error (`FillError`), never a silent truncation or repair.
    The generator uses **constrained decoding** (a length budget plus a regex/grammar
    automaton for `pattern`; EOS masked until `minLength`), so a valid output exists by
    construction and this error should not occur in practice; it is a guard, not a code
    path.
- Arrays: the length is bounded by `minItems` and `maxItems`. When `maxItems` is absent,
  the cap is `options.max_items` (default `null` = no cap: an anchored array has as many
  items as the context has, §12.6; an index-addressed array stops at 32, the count head's
  trained range). `uniqueItems` is checked when the response is validated.
- Objects: `additionalProperties` may be absent, `true` or `false`. deg never emits
  undeclared keys. A schema-valued `additionalProperties` is an error, because the key
  names would be unknown.

## 5. Validation errors (HTTP 422, every problem listed as `{loc, msg}`)

- `x-generate` on anything other than `string` or `array` of `string`. This includes
  `enum`, `boolean`, `integer`, `number`, `object`, arrays of non-strings, and strings
  that have `enum` or `format`.
- **Unsupported keywords:** `allOf`, `not`, `if`/`then`/`else`, `const` (a single value
  has nothing to decide), `dependentSchemas`, `dependentRequired`, `dependencies`,
  `patternProperties`, `propertyNames`, `unevaluatedProperties`, `minProperties`,
  `maxProperties`, `unevaluatedItems`, `prefixItems`, `additionalItems`, tuple `items:
  [...]`, `contains`, `minContains`, `maxContains`, `$dynamicRef`, `$dynamicAnchor`,
  `$recursiveRef`, `$recursiveAnchor`, `contentEncoding`, `contentMediaType`,
  `contentSchema`.
- **Limited keywords:**
  - `anyOf`/`oneOf` are allowed only in the nullable form or the const/enum form.
  - `$ref` must be local (`#` or `#/...`) and not recursive. Its siblings may only be
    annotations, which override the target's.
  - Union types are allowed only as `[X, "null"]`.
  - `format` must be one of `date`, `date-time`, `email`, `uri` or `uuid`.
- Also errors:
  - unknown keywords, and keywords that don't apply to the type (e.g. `maxLength` on
    `integer`);
  - an `enum` with a duplicate value, a value of the wrong type, or fewer than 2 or more
    than 255 values;
  - integer bounds that admit 0 or 1 values;
  - number bounds that admit no value (e.g. `minimum` > `maximum`);
  - an invalid `pattern`;
  - a `required` name that isn't in `properties`;
  - a root that isn't `type: object`;
  - any document that isn't a valid JSON Schema (draft 2020-12 by default, or the draft
    named in `$schema`).
- Allowed annotations: `title`, `description`, `default`, `examples`, `$comment`,
  `$schema`, `$id`, `$anchor`, `$defs`, `definitions`, `readOnly`, `writeOnly`,
  `deprecated`. A leaf's `title` and `description` are its instructions to the model.

## 6. Fill plan

`compile_schema(schema) -> FillPlan` is a pure function. The plan holds:

- `fields`: the leaves in schema order (depth-first). Each leaf has:
  - its path: property names, with `*` meaning each item;
  - its mode and JSON type;
  - `options`/`option_descriptions` (choice) or `levels` (score);
  - `null_outcome`, `format`, `constraints`;
  - `required`, `nullable`, `on_null`, `title`, `description`.
- `arrays`: the array nodes with `min_items`, `max_items` and `unique_items`.
- `FieldPlan.fingerprint`: a sha256 of the leaf's canonical spec. It is identical wherever
  that leaf appears, whatever its siblings or their order.
- `FillPlan.fingerprint`: a hash of the **set** of parallel-phase fingerprints plus the
  **ordered list** of generate fingerprints.
  - Reordering decide/extract properties keeps the hash.
  - Reordering generate fields changes it.

## 7. Execution order

1. **Read once.** The context is rendered once and encoded once:
   - strings are used verbatim;
   - JSON is serialized with `", "`/`": "` separators, the given key order and
     `ensure_ascii=false`.
2. **Parallel phase: all decide.\* and extract.\* fields.** Each field's outcome is a
   function of exactly the context encoding, the field's key path, the field's own
   annotations, and its options/levels/constraints. It is never a function of other fields
   or their order.
   - So outcomes are **bitwise-reproducible** when fields are reordered, added or removed,
     or when the schema is split into several requests or merged into a bigger one.
   - Array items are first enumerated as item anchors (the count). Each item's leaves are
     then conditioned on the context plus that item's anchor.
3. **Generate phase.** The `generate` fields run after the parallel phase. Each one is
   conditioned on the context and all decided/extracted values (one shared KNOWN VALUES
   block), never on other generate fields: generate fields are independent of each other
   and decode together.
   - Decoding is greedy by default.
   - `options.temperature > 0` requires `options.seed`; each field samples from its own
     stream seeded by `(seed, pointer)`.
   - Decoding is bounded by `maxLength`, `pattern` and the field's `x-max-tokens`, else
     `options.max_new_tokens` (default 256 per field).
   - Not implemented: generate-to-generate dependencies. If added, they must be declared
     (e.g. `"x-generate": {"after": "/summary"}`) and the dependent field must continue
     from the parent row's final state after its generated tokens (forked), never
     re-prefill the parent's text; unannotated schemas keep the independence above.

## 8. Response

```json
{
  "model": "deg-qwen3.5-0.8b-0.1",
  "value": {"department": "billing", "urgent": true, "reply": "..."},
  "meta": {
    "/department": {"mode": "decide.choice", "probability": 0.8, "confidence": 0.7,
                     "probabilities": [0.8, 0.15, 0.05]},
    "/urgent":     {"mode": "decide.noul", "probability": 0.91},
    "/anger":      {"mode": "decide.score", "probability": 0.6, "confidence": 0.5,
                     "probabilities": [0, 0.1, 0.6, 0.3, 0], "expected": 2.2},
    "/order_id":   {"mode": "extract.span", "confidence": 0.88, "span": [32, 39]},
    "/due":        {"mode": "extract.date", "confidence": 0.4, "span": null,
                     "null_reason": "not_found"},
    "/line_items": {"mode": "array", "items": 2},
    "/reply":      {"mode": "generate", "generated_tokens": 11}
  },
  "usage": {"prompt_tokens": 42, "generated_tokens": 11},
  "plan": "<FillPlan.fingerprint>"
}
```

- `value` is always valid against the input schema. `assemble` checks this with
  `jsonschema` and deg's own format checks, and raises `FillError` (HTTP 500) otherwise.
- `meta` is keyed by concrete RFC 6901 pointer, with integer array indices. An array
  that dropped items whose required values were not in the context (§12.6) has
  `"dropped": n` beside `"items"`.
- `probabilities` follow the schema's option or level order; for score, the null outcome
  comes last.
- `span` gives `[start, end)` code-point offsets into the rendered context.
  `assumed_offset: true` marks a date-time whose offset came from
  `options.default_offset`.
- `confidence = (count * peak - 1) / (count - 1)` is the jev v1 formula. Ties go to the
  first maximum.
- `usage.generated_tokens` is the sum of the per-field `generated_tokens`. It is 0 for a
  schema with no `x-generate`.
- On a multimodal context an extract field whose pointer lands in a media item has
  `"span": null` and says where instead: `"media": "m0"` plus `"box": [x0, y0, x1, y1]`
  (pixels of the source image, `x1`/`y1` exclusive) or `"time": [s, e]` (seconds of the
  source audio), `"tokens": [i, j]` (the item's soft tokens the pointer chose) and
  `"read": "crop"` (the value was read from that region, §12.10). No schema keyword asks
  for it, as none asks for `span`: the value and the schema are unchanged, and the
  location is meta. A located value that cannot be read is null with
  `null_reason: "media_unread"`.

## 9. API

`POST /v1/fill`

```json
{"context": "string | object | array",
 "schema": {"type": "object", "...": "..."},
 "model": "deg-latest",
 "options": {"max_new_tokens": 256, "max_items": null, "date_order": null,
             "default_offset": "Z", "temperature": 0, "seed": null,
             "array_decode": "count_peaks", "null_margin": null}}
```

- `array_decode` (`"threshold"` | `"count_peaks"` | `"hybrid"`) picks how anchored
  arrays find their items (§12.6); also accepted in the decision APIs' `options`.
- `null_margin` (finite number, nats, or `null`) biases nullable/optional extract leaves
  toward null: such a leaf is null when its best valid span's log-prob is below the null
  log-prob + `null_margin` (§12.1). `null` = the server's `--null-margin` (default 0 =
  plain argmax). > 0 trades made-up answers for omissions (deg-2b held-out choice: 2.6,
  deg-0.8b: 1.4; `deg.bench.null_margin`, results/deg/null-margin-v1). It touches no
  decide field, no `forbid` (required non-nullable) leaf and no count/anchor decision; it
  applies to leaves inside index-addressed and anchored items the same way (a nullable
  item leaf becomes null, an optional one is omitted). Also accepted in the decision APIs'
  `options`.

- `date_order` is `"MDY"`, `"DMY"` or `null`. With `null`, a numeric date like
  `03/05/2026` parses only if exactly one reading is a valid date; otherwise the field is
  null or omitted.
- Unknown request fields and unknown options are 422 errors.

`GET /v1/models` lists the accepted model names; `GET /health` reports the batcher.

### 9.1 Decision APIs (`src/deg/api.py`, `server.py`)

`python -m deg.server --ckpt DIR --port 8790` serves three routes on one model. Each
request becomes **one** fill: every question is a property of one schema (key = question
name / id), so the input is read once, decisions/extractions are independent fields, and
`generate` questions run last, conditioned on the other answers.

| Question (OpenAI / Jev) | Field schema | Mode |
|---|---|---|
| `predicate` / `noul` | `boolean`; Jev `criteria.true/false` appended to the description as `Yes means:` / `No means:` lines (the jev training format) | decide.noul |
| `choice` | `oneOf` of `const` + `description` (or `enum` when no option has a description); OpenAI values are typed (`"true"` != `true`) | decide.choice |
| `score` | `integer`, `minimum 0`, `maximum L-1`, description = the instructions; level labels (and descriptions) as `x-levels: {"i": "label[: description]"}`, so each is the option line `i: label` the score head was trained on | decide.score |
| `extract` (deg) | `[string\|number\|integer, null]` + `format` from `value_type`, `pattern`, `minLength`, `maxLength` | extract.* (nullable: a miss is `null`) |
| `generate` (deg) | `string`, `x-generate: true`, `minLength`, `maxLength`, `pattern`; `max_tokens` -> that field's `x-max-tokens` (questions in one request may use different budgets) | generate |

A one-option choice or one-level score (accepted by live Jev) is answered with probability 1
without the model.

- `POST /v1/decisions` mirrors the OpenAI Decisions API (openai-python 3.26.1
  `types/decision.py`, `types/decision_create_params.py`; guide
  https://developers.openai.com/api/docs/guides/decisions): request `model`, `input`
  (string or user messages of `input_text` parts; `input_image` is refused with 400 because
  this model reads text only), `questions`, `safety_identifier`; response `{answers, model,
  usage}` with answers in question order: `predicate {type, name, probability}`, `choice
  {type, name, choice, probabilities: [{value, probability}], confidence}`, `score {type,
  name, score, probabilities: [{value, label, probability}], confidence}` (`score` = the
  expected level index), usage `{input_tokens, input_tokens_details{cache_write_tokens,
  cached_tokens}, output_tokens, output_tokens_details{reasoning_tokens}, total_tokens}`.
  `name` is optional (echoed, `null` when absent) and must be unique. Errors: 400
  `{"error": {message, type: "invalid_request_error", param, code}}`. deg never returns a
  `refusal` answer.
- `POST /v1/systemone` mirrors live TypeSafe Jev (`jev-1.13.0`, probed 2026-10-09;
  `tests/fixtures/jev_systemone_live.json`): answers keyed by id in request order, `noul
  {type, noul}`, `choice {type, choice, confidence, probabilities{option: p}}`, `score {type,
  score, confidence, legend{"i": level}, probabilities{"i": p}}`, usage `{input_tokens,
  output_tokens}`. Limits and errors as live Jev: 1..255 options (`{"detail": "Too many
  choices. Must have at most 255 choices."}`), 1..10 levels, unknown type / unknown
  top-level field -> 400 `{"detail": {"error_type": "api_usage_error", "message": "Invalid
  request."}}`, unknown model -> 400 `Unknown model: X`, missing field / empty questions ->
  422 `{"detail": [{type, loc, msg, input}]}`, missing key -> 403. Unknown question keys are
  ignored, as live Jev does. Unlike live Jev, probabilities are not rounded to 2 decimals.
- Extensions on both: `extract` -> `{type, value, span, confidence[, null_reason]}`,
  `generate` -> `{type, text, output_tokens}`; request `options` (`temperature`, `seed`,
  `date_order`, `default_offset`). Usage output tokens are the generated tokens (0 without
  `generate`).
- Model names: deg aliases, plus `gpt-6-luna` (decisions) and `jev-latest`,
  `jev-preview`, `clef-flash` (systemone) so existing clients work unchanged; responses
  carry deg's model id.
- Batching: one GPU worker; requests arriving within the batch window (default 5 ms) are
  taken together (default 16) and handed to `deg.infer.fill_many(model, jobs)` when the
  runtime defines it, else run back to back. A failing job fails only its own request;
  more than `--max-pending` queued jobs -> 429.

## 10. Determinism guarantees

- **Plan:** the same schema always gives the same plan and fingerprints. Compilation never
  mutates the input.
- **Parallel phase:** greedy and sampled requests behave identically here, since decide
  and extract involve no sampling. On the same build and hardware, outcomes are bitwise
  identical across:
  - repeated runs;
  - field reordering;
  - adding or removing other fields;
  - splitting or merging schemas.

  - batching with other requests (`fill_many`).

  The runtime batches per-field queries with batch-invariant kernels so that a field's
  numerics don't depend on the batch composition (§12.2).
- **Generate phase:** given the same parallel outcomes, a generate field's text is
  bitwise reproducible (greedy, or sampled with `seed`) and independent of the other
  generate fields in the schema and of other requests decoding with it.
- **Post-processing** (argmax, tie-breaks, parsing, assembly) is pure Python over
  the head outputs, with no clock, locale or randomness.

## 11. What is measured against the constrained-decoding baseline

The baseline is the same base model (Qwen3.5-0.8B, at the same quantization) emitting the
whole JSON object under grammar-constrained decoding (a JSON-Schema-derived grammar),
greedy, from the same prompt.

The same eval set of (context, schema, gold JSON) is measured on both:

- **Tokens:** generated tokens per response, prompt tokens, and their total.
- **Latency:** end-to-end on one pinned CPU core (the deploy target), plus GPU throughput.
- **Accuracy per mode:**
  - choice: exact match;
  - score: MAE;
  - noul: accuracy;
  - span: exact match and token F1;
  - number and date: exact match after parsing;
  - null: precision and recall.
- **Calibration:** the ECE of decide probabilities and of extract confidences.
- **Validity:** the rate of schema-valid responses. deg must hit 100%, by construction.
- **Determinism:** bitwise equality across 10 repeats, 5 field permutations, and
  split-vs-merged schemas.
- **jev gates:** the jev v1 eval gates on `/v1/systemone`.

## 12. Model (v1 implementation)

Code: `src/deg/model.py` (trunk, forks, heads), `infer.py` (`fill`), `train.py`,
`evaluate.py`. Tests: `tests/test_model.py`.

### 12.1 Encoding: one trunk, one fork per question

- **Trunk** = a fixed system/user prefix, the rendered context, `"\n"`. It contains the
  context only, never the schema, so it is shared by every field and every request on
  the same context.
- **Fork** = one continuation of a *copy* of the trunk state. Qwen3.5 is a hybrid: Gated
  DeltaNet layers carry a recurrent state plus a 3-token causal-conv tail, attention layers
  carry K/V. A fork starts from exactly that state, so it reads the whole context and
  nothing else. Fork kinds:
  - decide/extract leaf: `FIELD <concrete pointer>`, `ITEM i of <array>` per enclosing
    array (index mode; anchored items use `ITEM <array> AT: "..."`, §12.6), `TITLE`, `DESCRIPTION`, `KIND`, extract
    `CONSTRAINTS`, then `OPTIONS:` with one `- <option>` line per outcome, then `ANSWER:`.
    Outcome text follows the plan order (choice: value + option description; score:
    level, plus a null line for nullable scales; noul: no option lines).
  - array count: `ARRAY <pointer>`, item anchors of enclosing arrays, the array's
    title/description and item-field pointers, `ANSWER:`.
  - generate: `KNOWN VALUES:` (every decided/extracted value, one `pointer = json` line
    each, in leaf order; generate values are never included), `WRITE <pointer>`,
    annotations, `LIMITS`, `VALUE:`, then the text and `<|im_end|>`. At inference the
    KNOWN VALUES block is prefilled ONCE on the trunk and every generate field forks that
    state with only its own header (no per-field restatement of the values).
- **Why forks and not an attention mask:** DeltaNet layers are recurrent, so a mask cannot
  hide one field's tokens from the next; separate continuations can. The backbone is a
  functional re-implementation of the HF Qwen3.5 stack over explicit state
  (`Backbone.forward(ids, lengths, state)`); it matches the HF forward (fp32 relative error
  < 1e-3 on GPU, `assert_close` on CPU) and lets gradients flow from forks into the trunk.
- **Padding is exact**: padded positions get `beta = 0` and `g = 0` in the delta rule (no
  write, no decay), their K/V are masked, and the conv tail is gathered at each row's real
  length, so a right-padded row gives the same state as an unpadded one.
- **Long contexts stream; there is no context cap.** Inputs up to `DENSE_PREFILL_MAX`
  (8192) tokens are read in one shot (dense causal mask). Longer inputs prefill in
  `PREFILL_CHUNK` (4096) token chunks, carrying each DeltaNet layer's recurrent state and
  conv tail and appending each attention layer's K/V; on CUDA the chunk attends the K/V
  buffer through the flash-style fork kernel (no mask tensor at all), elsewhere through
  SDPA with a `[B, chunk, P+chunk]` mask. Nothing is O(L^2): peak memory is the K/V
  (6 attention layers x 2 KV heads x 256 x bf16 = 12 KiB/token) + hidden states
  (2 KiB/token) + one chunk's work (~0.46 GiB at 4096, independent of L). Span-pointer
  keys are projected in 8192-row blocks (bitwise equal to one `bi_linear`), and span
  candidates come from an `[T, MAX_SPAN_TOKENS]` band, never a T x T grid. Chunked
  outputs equal the one-shot read within bf16 noise; at most 8192 tokens they are bitwise
  the one-shot path. A context whose K/V does not fit the device fails with a CUDA OOM
  (reported, never truncated); serving those needs K/V offload to host memory.
- **Nothing scales with fields x context on the device.** Fork batches are sized from the
  memory free right now (`fork_token_budget`: `FORK_MEM_FRACTION` of free + cached-unused
  memory at `FORK_BYTES_PER_TOKEN` ~128 KiB per padded fork token, capped at
  `FORK_TOKENS`; OOM split stays as a backstop); after a fork pass only the rows the heads
  read (answer + option tokens) are kept. Span logits for a long context or a big schema
  are computed in answer-row micro-batches of at most `HEAD_LOGIT_BYTES` and context-row
  blocks of `HEAD_ROWS` (keys re-projected per micro-batch, no `[T, 256]` copy), then wait
  in host memory until decoding (bitwise the one-shot values). After field decoding the
  raw head outputs and the trunk hidden states are released, and the KNOWN VALUES block
  extends the context state in place (`forward(consume=True)`), so a context's K/V is
  never held twice. Peak device memory = weights + context K/V + hidden + one fork batch.

### 12.2 Independence (bitwise) and the one-pass runtime

Inference runs every query of a request (array counts, array items speculated up to 8
per array, every decide/extract field) as fork rows of ONE batch
(`DegModel.answer_many`; chunked only above 16k padded tokens, for memory), and every
x-generate field of a request as rows of one decode batch. `fill_many` puts the rows of
several requests in the same batches. A row's numerics never depend on the other rows
because every op in the fork path is batch-invariant (`src/deg/kernels.py`):

- **Prefixes by reference.** A fork row reads its prefix (context trunk, or trunk +
  KNOWN VALUES) in place: attention reads the prefix K/V at per-row offsets, the
  recurrent kernel starts from the prefix DeltaNet state by index. No per-row copy of
  the context exists; per-row state is the fixed-size recurrent state, conv tail and the
  row's own K/V.
- **GEMMs**: cuBLAS picks kernels (tiles, split-K) by shape, so a row's result changes
  with the number of rows (measured on the L4: rows differ for many M). `bi_linear` is a
  Triton GEMM with one fixed tile config and no split-K; `bi_linear_small` (decode steps
  and the LM head) uses 16-row tiles and a split-K factor fixed by the weight shape, with
  partials summed in a fixed order. Each call site always uses the same config, so a
  row's output depends only on that row. Projections sharing an input are fused into one
  GEMM (weights concatenated once; parameters become views).
- **Attention** (`fork_attention`): one program per (row, head, query block), prefix
  keys then own causal keys in a fixed block order; fully masked blocks are exact no-ops
  of the online softmax.
- **DeltaNet** (`fork_delta_rule`): FLA's fused recurrent gated delta rule, one program
  per (row, head, value block), tokens in order; padded positions have `beta = g = 0`.
- **Norms**: fixed-config Triton row kernels; elementwise ops run eagerly.
  `torch.compile` is not used in the fork path: it specializes size-1 dims, so one row
  alone and the same row in a batch would run different generated kernels.
- **Heads** run batched in fp32 through `bi_linear` (IEEE fp32 dot).

`tests/test_model.py` checks with `torch.equal` that every field's raw head outputs are
identical alone, reordered, inside bigger schemas and in split vs merged schemas; that a
generate field's text is identical alone and next to another generate field; and that
`fill_many` equals `fill` per request, on CPU (tiny random Qwen3.5; the CPU reference runs
each fork row alone at a 64-token width grid because torch's CPU SIMD/scalar-tail split
depends on tensor size) and on GPU (the real model). Caveat: the trunk prefill (batch 1)
still uses the FLA chunk kernel and torch.compile'd ops with autotuning, so equality is
guaranteed within one process; across processes it holds when the autotune cache
matches (same build, hardware and cache).

**Decode step.** Each generate decode step (all rows x 1 token, LM head, argmax) is
captured once as a CUDA graph over static buffers (`DecodeGraph`: prefixes up to a
power-of-2 token capacity, rows bucketed 1/2/4/..., own-token capacity) and replayed.
Greedy takes the argmax when it is admissible and only otherwise walks the top 256 in
logit order (the same rule as the constraint walk). Measured on one L4 (bf16): ~7.3-7.9 ms
per step at batch 1 vs 36 ms before; the floor is weight bandwidth (~1.5 GB/step incl. the
LM head).

### 12.3 Heads (fp32)

`a` = hidden state at `ANSWER:`, `o_i` = hidden state at the end of option line `i`,
`c_t` = trunk hidden state at context token `t`.

- `choice`: `q(a) · k(o_i) / sqrt(256) + u · o_i`, softmax over options (the jev v1
  pointer head). The option is encoded as text in the fork, so any enum works zero-shot.
  Training shuffles option order (target permuted) with p = 0.5.
- `score`: a separate pointer head of the same form over the level lines.
- `noul`: `w · a + b` → logits `[l, 0]`, i.e. `[p_true, p_false]`.
- `extract`: start and end pointers `qs(a) · ks(c_t)`, `qe(a) · ke(c_t)` over the context
  tokens only (prefix, suffix and query tokens are not addressable), plus a null logit from
  `a` as one extra outcome. Decoding:
  - numbers, dates, and email/uri/uuid strings: every candidate span the deterministic
    grammar finds in the context (the same candidate regexes datagen used to locate gold
    spans) is scored by `log p_start + log p_end` of its covering tokens; the best one
    that parses and meets the constraints wins, unless null scores higher.
  - plain strings: the top 32 token spans (`i <= j < i + 128`), mapped to code points
    and trimmed of whitespace and boundary punctuation a token carried in (`trim_span`;
    98.7% of gold spans are recovered exactly by trimming the covering tokens).
  - `options.null_margin` m (default 0) shifts that comparison: null wins when
    `log p(best valid span) < log p(null) + m`. Decoding stays deterministic and
    batch-invariant (the margin is per request, applied after the shared head pass).
  - `forbid` fields ignore null and take the best valid span; none at all is a
    `FillError` (inside an array item: the item is dropped, §12.6).
  - confidence = `exp(log p_start + log p_end)` of the chosen outcome.
- `count`: linear over 0..64 items, masked to `[minItems, min(maxItems or
  options.max_items or 32, 64)]`. Index-addressed arrays only (§12.6). Arrays resolve
  level by level (a nested array's count fork needs its parent's item anchor).
- `anchor`: per context token `t`, an independent logit `qa(a) · ka(c_t) / sqrt(256) +
  ua · c_t + b` that an item of the array starts at `t` (sigmoid; `b` starts at -6),
  masked to the array's region (§12.6).
- anchored `extract`: inside an anchored item, the start/end logits of context token
  `t` get a learned bias `B[start|end, inside, bucket(t - anchor)]`, where `inside` means
  `anchor <= t < region end` and the buckets edge at -256, -64, -16, -4, 0, 1, 2, 4, ...,
  256 tokens (zero-initialized).
- `generate`: the base LM head (tied embeddings, frozen in training), greedy by default,
  seeded multinomial (one stream per field) when `temperature > 0`. Constrained decoding: candidates are tried in
  logit (or sampled) order among the top 256; a token is admissible if the decoded text
  stays within `maxLength` and is a partial match of `(?s:.*?)(?:pattern)(?s:.*)`
  (`regex` partial matching, i.e. JSON Schema search semantics); EOS is admissible only
  when `minLength` and the pattern are met. `generated_tokens` counts EOS.
- Calibration: one temperature per head kind (`choice, score, noul, extract, count`), fit
  by NLL on datagen `val` only (`deg.evaluate calibrate --source datagen-val`, default;
  jev dev is reported separately with `--report-jev`, or pooled with `--source
  datagen-val+jev-dev`), stored in `heads.safetensors`; `count` is not fit. Decide heads
  fit the one-hot filled value (`--target hard`, default), the label accuracy and ECE
  are judged against; the soft training targets (`--target soft`) are less confident
  than the gold and over-soften (results/deg/bench-v2/calibration-v2.md).

### 12.4 Training (`deg.train`)

- Data: datagen records (`split == "train"`; val/test, including the held-out test
  domains, are never trained on) plus jev v1 `train.jsonl` decisions (noul/choice/score
  with teacher soft labels), mapped to one-field forks (`FIELD /answer`, the instructions
  and criteria as text, score levels as `i: criterion` options). A micro-batch draws
  records at random (jev with probability `--jev-frac`, default 0.3) up to a token budget.
  `--reload-every N` re-reads the growing datagen files.
- Sources: `--records PATH[:WEIGHT]` is repeatable (default: the v2 `full-off` file,
  weight 1). Each source reads its own `split == "train"` records (its own val/test labels
  are kept out) into its own pool, and every reload re-reads all sources. Each deg
  micro-batch picks one source with probability proportional to its weight among the
  **active** sources, those with at least `--source-min-train` (default 50, summed over
  ranks) train records, so a small, slowly growing source (e.g.
  `/data/deg/datagen/v3-long/records.jsonl:0.25` next to v2 at 1.0 → 20%) gets its fixed
  share as soon as it reaches the threshold. The jev mix stays per record inside every
  micro-batch. End-of-run calibration fits on the val records of every source (contexts
  within `--max-context-tokens`); the end-of-run eval uses the first source.
- Long contexts: a record whose context exceeds `--max-context-tokens` (default 6000)
  is kept whole and trains a **span-aware window** at every draw (inference is unchanged:
  it streams the whole context). The window is `max-context-tokens` minus the template
  tokens, placed uniformly among the positions that contain one gold span picked at
  random (spans and alternatives), so windows differ across epochs and cover the gold.
  A cropped window trains only what its text shows: extract fields with a gold span (or
  alternative) inside the window, and anchored arrays with at least one gold item start
  inside it (the items starting outside are left out with their subtrees). Absent-gold
  (null) extract fields, decide and generate fields (their evidence has no span) and
  count heads train only on a whole context; nothing is ever trained as null or empty
  because of cropping. Train logs report `windows`, `window_targets`, `window_dropped`.
- Decide cap per field (`--decide-forks-per-field K`, default 0 = off; `cap_decide_forks`,
  applied first in every draw): at most K decide forks (choice/score/noul) per schema field
  (array indices folded to `*`), a fresh random subset each draw. v3-long filings carry
  ~135 choice forks per record, 99.8% one repeated in-array enum per table cell (`unit`,
  `scale`, `statement_type`; 68% of their booleans are false); uncapped they are most of
  the decide forks of every step that draws one. run-2b, which trained them uncapped, lost
  decide accuracy on short inputs (datagen val top-level choice .939 -> .873 at step 4500;
  JevBench choice 94 -> 74-92 of 139 across steps 500-6000, noisy; results/deg/regress-2b).
- Fork cap per draw (`cap_forks`): every drawn example (whole context, window, filler)
  trains a fresh random subset of its forks when they exceed its room: `--record-fork-tokens`
  when set, else the token budget beside its trunk and at least half the budget however
  long the context. Whole-array forks (anchor, and anchored arrays' count) are always
  kept; the other forks are taken in random order while they fit, the first one whatever
  its length (so a generate fork longer than the room still trains). A record therefore
  costs one trunk pass plus about the room in fork tokens per step, whatever its field
  count: unsubsampled, a long filing (1,153 field rows, 176 generate forks each repeating
  its ~16k-token known-value block) was 2.9M fork tokens and 260 micro-batches, a 20+ min
  step. Loss terms stay means over the forks drawn (a subsample; forks longer than the
  room are drawn less often). Train logs report `subsampled_records`, `subsampled_forks`.
- Training trunks have no 8192 cap: a trunk longer than `DENSE_PREFILL_MAX` is read in
  `PREFILL_CHUNK` chunks with every layer of every chunk gradient-checkpointed
  (`Backbone._forward_chunked_ckpt`; the mask is built inside the checkpoint, earlier
  chunks' K/V are passed in as they are), so nothing O(L^2) is saved and one layer's
  recompute is O(chunk x L). It equals the dense checkpointed read in hidden states,
  state, fork outputs and gradients (`tests/test_train_long.py`). The memory planner
  models the chunked trunk separately (per-chunk recurrent state, the concatenated K/V
  copy, no saved mask, chunk-sized recompute).
- Row chunks share one trunk pass: a record too big for one micro-batch is split by the
  memory planner into field-row chunks (`MemPlanner.split_rows`); `run_step` runs them on
  ONE trunk forward (`run_chunks`): the trunk outputs (final hidden, recurrent state, conv
  tails, K/V) are detached into leaves, each chunk's forks + heads backpropagate into the
  leaves, and the trunk backpropagates once from their accumulated gradient (equal to the
  unsplit record's gradient, dense and chunked trunks alike: `tests/test_train_mem.py`).
  An out-of-memory error in such a run drops its gradients and reruns the chunks one by
  one with their own trunk passes (`--no-share-trunk` always does). Logs: `trunk_shared`.
- Losses, each a mean over its items:
  - `decide`: KL(t ‖ q). `t` is the record's `decide_probs` (Clef probabilities where
    Clef's argmax matched the filled value), else one-hot of the filled value; jev uses
    `probs`, else `hard`.
  - `extract`: start and end CE where every `span_alternatives` entry is a positive:
    with `span_probs` the target is that soft distribution; otherwise the loss is the
    marginal `-log Σ_pos p`. A field absent from the filled value targets null.
  - `count`: CE of the array length (index-addressed arrays).
  - `anchor`: per anchored array instance, BCE of every in-region token against the gold
    item starts, summed and divided by 1 + the number of items (§12.6).
  - `lm`: next-token CE on x-generate text + EOS, teacher-forced on gold values; the
    prompt is the inference layout (KNOWN VALUES = decided/extracted values only).
  - `anchor_count` (`--anchor-count-weight W`, default 0 = off): with W > 0 every
    anchored array of a whole context (never a cropped window: a count needs the whole
    array) also gets the index-mode count fork, CE of its gold item count over
    `[minItems, min(maxItems | MAX_COUNT, MAX_COUNT)]` with the target capped at the top
    class (which then means "at least that many"), so `count_peaks` decoding has a
    trained count.
  - total = `decide + extract + 0.5 count + anchor + W anchor_count + L lm`
    (`--anchor-weight`, `--anchor-count-weight`, `--lm-weight L`, default 1).
- Filler augmentation (`--filler-frac P`, `--filler-corpus`, default
  `/data/deg/external/v3/docs.jsonl`, `--filler-max-tokens N`, default 16384): the
  anchor head under-fires on long documents when it has only seen short ones. With
  probability P a drawn short datagen record is rebuilt with its document at a random
  offset inside unrelated text (`pre + "\n\n" + doc + "\n\n" + post`), total length
  log-uniform from its own length + 64 to `min(N, --max-context-tokens)`; every gold
  span shifts with it and every target is the plain example's (the filler is unrelated
  text). A `--filler-negative-frac` share (default 0.25) is filler only: the record's
  top-level anchored arrays with minItems 0 train their anchor fork with no item start
  (every token negative) and count 0; nothing else trains on it. Filler comes only from
  v3 **train-split** documents (`datagen.v3.split_of`), never the record's own document
  and never ExtractBench (refused by path). Draws are placeholders sorted at their planned
  length and materialized when the packing reaches them, like long-document windows, so
  the packing and the memory planner see real examples; a rebuilt example that would
  exceed `--max-context-tokens` falls back to the plain one. Logs: `filler`,
  `filler_negative`, `filler_tokens`.
- Optimization: bf16 weights with fp32 master copies (AdamW, lr 2e-5 trunk / 1e-3 heads,
  warmup then cosine to 10%), embeddings and the tied LM head frozen, per-layer gradient
  checkpointing, elementwise ops fused with `torch.compile`. Forks are length-grouped
  (≤25% padding). Multi-GPU: `torchrun`, one data shard per rank, explicit gradient
  all-reduce. Logs: losses, tokens/s (real and padded), GPU utilization and memory.

### 12.5 Smoke run and full-run launch

Smoke (2026-10-08, one L4, `/data/deg/runs/smoke-1`, 300 steps, budget 14k tokens,
≤32 records/step, jev 20k-record subset at 30%): total loss 13.0 → ~1.5 (decide 1.54 →
0.15, extract 8.2 → ~0.5, lm 1.83 → ~0.5, count 2.7 → ~0.8); ~4.0k real tokens/s
(4.7k padded), 85-92% GPU utilization, 14.9 GiB peak. Eval through the full `fill` path
after calibration:

| split | records | valid | decide acc / ECE | extract EM | count acc | generated / full-JSON tokens |
|---|---|---|---|---|---|---|
| val | 29 | 100% | 0.93 / 0.097 | 0.79 | 0.73 | 0.33 |
| test (held-out domains) | 60 | 100% | 0.90 / 0.071 | 0.85 | 0.70 | 0.29 |
| jev dev (1312 questions) | | | 0.81 / 0.024 | | | |

Full run (4 GPUs; not launched by default — the orchestrator allocates GPUs):

```bash
systemd-run --user --unit deg-train-full --property=MemoryMax=80G \
  --setenv=CUDA_VISIBLE_DEVICES=0,1,2,3 --setenv=PYTHONUNBUFFERED=1 \
  --setenv=PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
  --working-directory=/shared/rwkv/packages/deg \
  /bin/bash -c 'exec /shared/rwkv/.venv/bin/torchrun --standalone --nproc-per-node 4 \
    -m deg.train --out /data/deg/runs/full-1 --steps 20000 --budget 14000 \
    --max-records 32 --jev-frac 0.3 --reload-every 500 --save-every 1000 \
    --eval-records 200 --eval-jev 1000 > /data/deg/runs/full-1.log 2>&1'
```

### 12.6 Position-anchored array items (`array_mode = "anchor"`, run-2 default)

Run-1 addressed array item `i` by index ("ITEM i of /arr") and sized arrays with a count
head over 0..64. On real documents that failed: array-item leaves were 10% correct vs 45%
for scalars, forcing the gold counts did not help, and nested counts copied the parent's
index (results/deg/extractbench-v1/DIAGNOSIS.md, cause d). Anchor mode addresses an
item by **where it is**, not by its ordinal:

- **Which arrays.** An array whose item subtree has an extract leaf is anchored. Arrays of
  only decide/generate leaves (no span can say where an item is) keep the count head and
  index addressing. The mode is static per schema and checkpoint
  (`model.meta["array_mode"]`; a checkpoint without anchor heads, i.e. run-1, loads as
  `index` and behaves exactly as before; `deg.train --array-mode anchor|index`, default
  `anchor`).
- **Routing by document length** (`model.meta["anchor_min_tokens"]`, default 0 = off;
  `deg.train --anchor-min-tokens N`, saved in `deg.json`). An anchor-mode model fills a
  document of fewer than N context tokens exactly as index mode (count fork + `ITEM i`
  forks), and trains its whole-context records the same way; documents of N or more
  tokens, filler-augmented records that reach N, and every cropped window (its document is
  longer than any window) anchor. Why: on short datagen records run-2b step-4500 in index
  mode beats its own anchor mode (test array-item EM .844 vs .769 count_peaks, extract EM
  .896 vs .866; run-1 index .891 / .922), while anchors win on long documents
  (ExtractBench; results/deg/regress-2b).
- **Anchor fork.** One fork per array instance: `ARRAY <ptr>`, the enclosing items'
  locators, title/description, `ITEM FIELDS`, `KIND: mark where each item starts in the
  context`, `ANSWER:`. The anchor head scores every context token of the array's
  **region**; tokens with p > 0.5 are marked, each run of adjacent marked tokens is one
  item (its best token). Fewer than `minItems`: the best remaining non-adjacent in-region
  tokens are added (none left: `FillError`); more than `maxItems` (or `options.max_items`
  when given): the highest are kept. **No other cap**: the count is the number of
  anchors, and item forks run in the usual memory-bounded fork micro-batches.
- **Decode modes** (`options.array_decode`, default `count_peaks`). `threshold`: as above.
  `count_peaks`: the array also runs its count fork (the index-mode count query, trained
  on anchored arrays by `anchor_count`, §12.4); N = its argmax over `[minItems,
  min(maxItems | max_items | MAX_COUNT, MAX_COUNT)]` (at the saturated top class, the
  threshold count when larger), capped by maxItems; the items are the N best local
  maxima of the anchor logits with non-maximum suppression (`PEAK_RADIUS` = 4 tokens:
  one peak per item), then the best remaining tokens outside every radius if too few
  peaks exist. `hybrid`: threshold, unless no token passes p > 0.5 and the count says
  N > 0, then count_peaks. The count fork is one more independent fork and peak picking
  is a pure function of the logits, so every mode is deterministic and batch-invariant.
  Default chosen on datagen val only (`results/deg/array-decode-v1`, run-2b step-3000,
  131 records / 153 anchored arrays, paired): aligned item-leaf EM threshold .609,
  count_peaks .553, hybrid .659 (count exact .575 / .784 / .641); with 4k tokens of
  filler before the record: .428 / .482 / .572 (count .342 / .704 / .513).
  `PEAK_RADIUS` 2/4/8 changes count_peaks/hybrid leaf EM by at most .03.
  Default re-chosen at run-2b step-4500 (count head trained by `anchor_count` since step
  4000; `results/deg/regress-2b`): count_peaks vs hybrid vs threshold, datagen val count
  .846 / .577 / .558, extract EM .828 / .818 / .816; datagen test (200) count .850 / .740 /
  .690, extract EM .866 / .847 / .842; ExtractBench normalized 239 vs 221 (hybrid),
  aligned 339 vs 338, count_ok 132 vs 111, valid docs 28 vs 27.
- **Regions.** A top-level array's region is the whole context. Item `k`'s region runs
  from its anchor to the next sibling anchor (the last item's to its parent's region
  end). A nested array searches anchors only inside its parent item's region, so a child
  count cannot copy the parent's count or index.
- **Item forks.** Every fork inside an anchored item renders anchored indices as `*`
  (`FIELD /results/*/time`) and one locator line per enclosing anchored item:
  `ITEM /results AT: "<context text from the anchor, at most 24 tokens, cut at the region
  end>"`. Extract forks also get the anchored span bias (§12.3) from the innermost
  anchor and region. There is no ordinal anywhere in an anchored item's forks.
- **Items that do not exist.** An anchor only says where an item would start; the item
  exists only if every required non-nullable (`on_null: forbid`) extract leaf in it has a
  valid value. When one has none (`forbid` already ignores null and searches every valid
  candidate in the context, so this means no span parses/meets the constraints at all —
  e.g. a `format: email` string item anchored in a résumé with no address), the
  innermost anchored item holding that leaf is **dropped**: never a `FillError`, never a
  made-up value. Survivors keep their anchors, regions and fork outputs (no fork sees an
  anchored item's index, so nothing is re-run and their values are bitwise unchanged)
  and are renumbered in document order; the array's meta gets `"dropped": n`; generate
  fields of a dropped item are not decoded and its values are not in KNOWN VALUES. If the
  drops leave an array below `minItems`, its enclosing anchored item cannot be
  represented and is dropped in turn; a top-level array left below `minItems` is a
  `FillError` (`minItems N: only k items have their required values`), like an anchor
  fork with too few positions. There is no "replacement anchor" fallback: for typed
  leaves (numbers, dates, email/uri/uuid) the valid candidate set is the whole context
  and does not depend on the anchor, so another anchor cannot succeed where this one
  failed. Dropping is a pure function of each item's own fork outputs, so it is
  deterministic and batch-invariant. It does not depend on `array_decode` (which only
  picks the anchors). **Index-addressed items** (index-mode models, documents routed to
  index mode below `anchor_min_tokens`, arrays without extract leaves) follow the same
  rule: the innermost enclosing item — anchored or index-addressed — is dropped and the
  survivors renumbered with their fork outputs unchanged (their `ITEM i of` forks are
  not re-run); generate forks of survivors render the new index. A required
  non-nullable value outside every array item keeps the §4 `FillError`.
- **Rounds.** Anchored arrays add one fork round per nesting level (anchors first, then
  the items' forks and nested anchor forks); index arrays keep the speculative one-pass
  layout.
- **Order.** Items come out in document order (anchor order). `deg.evaluate` reports
  positional `extract_em` and order-insensitive `extract_em_aligned`;
  `deg.extractbench.score_record(..., align=True)` and the ExtractBench eval report
  `normalized_aligned_rate` next to the positional rates.
- **Independence.** An anchor fork depends only on the context and its own query (array
  pointer, enclosing locators, annotations); an item's forks depend only on the context,
  their own query, and the item's anchor and region. Both are bitwise identical alone,
  inside a bigger schema and reordered (`tests/test_anchors.py`).
- **Training gold.** From the datagen spans (`spans`, else the first `span_alternatives`
  entry): an item's anchor is the first token of its earliest span that no sibling item
  shares (a shared span such as a table column header does not locate an item), else of
  its earliest span. An anchored array instance whose items do not all get distinct
  anchors inside the region is left out of training (counted as
  `anchor_skipped_arrays` in the data log); an empty gold array trains an anchor fork with
  no positives (the hard negative for nested arrays).

### 12.7 Who writes x-generate text: hybrid and LoRA (deg-suite v1 follow-up)

deg-suite v1 found the untrained base Qwen3.5-0.8B writes better x-generate text than
every fine-tuned checkpoint (judge faithful 0.94 vs 0.27 on DialogSum, 1.59 vs 0.43 on
mixed, out of 2; lengths similar): full fine-tuning made generated text unfaithful.
Three ways to keep the base model's generation, all deterministic and batch-invariant:

* **Two-copy hybrid** (`load_model(..., generate="base")`, `--generate base` on the server
  and the suite): a frozen base body sits beside the fine-tuned one (token embeddings =
  tied LM head shared when equal, +~1 GB bf16). Decide/extract are unchanged; for
  x-generate the base body re-reads trunk + KNOWN VALUES in one pass (one more context
  read) and decodes the header of :func:`deg.infer.base_generate_header`, which closes
  the user turn with a plain instruction and opens the assistant turn (thinking off).
* **LoRA** (`deg.train --lora-rank R`): the backbone stays the base; named low-rank
  adapters on every attention / linear-attention / MLP projection (`lora_A` [n*r, in],
  `lora_B` [out, n*r], B = 0 at start) train with the heads. A per-row selector picks
  each fork row's adapter (`--adapters shared` or `decide,extract`: by fork kind; choice,
  score, noul -> decide; extract, count, anchor -> extract), so rows with different
  adapters share one batch and stay bitwise independent (the delta goes through the same
  bi_linear GEMMs). x-generate rows run adapter-off = the base model, never trained.
  `--lora-context base` (default): the context is read ONCE by the base weights and
  every fork (adapted decide/extract rows, adapter-off generate rows) continues it, so the
  one-pass structure holds and the trunk has no gradient in training.
  `--lora-context <adapter>`: that adapter also reads the context; generation re-reads it
  adapter-off (2x prefill for requests with x-generate fields).
  Checkpoints keep the base `backbone.safetensors` and add `adapters.safetensors` +
  `deg.json["lora"]`.
* **GENERATE LoRA on the hybrid's base copy** (`--generate base+lora:<dir>`;
  `load_model(..., generate="base+lora:<dir>")`): the two-copy hybrid, with one low-rank
  adapter trained by `deg.gentrain` merged into the frozen base copy at load
  (`W += alpha/r * B @ A`, fp32 on the CPU, cast to the body dtype; deterministic).
  Deg's own backbone, heads and the one-pass flow are unchanged; generate rows stay
  batch-invariant (the merged copy is an ordinary frozen body); a zero adapter is the
  plain hybrid. A generator dir holds `generator.json` (`format`
  `deg-generator-lora-v1`, rank, alpha, training meta) + `lora.safetensors`
  (`<module>.lora_A` [r, in], `<module>.lora_B` [out, r], named as in the HF
  `Qwen3_5ForCausalLM`). Training (`python -m deg.gentrain`): SFT on x-generate fields
  in exactly the hybrid's layout (context + KNOWN VALUES + `base_generate_header`, target
  = value + EOS; `deg_example(..., base_layout=True)`), the adapter on every
  `LORA_TARGETS` projection reading the whole sequence; data = gen-v1 `train` (suite
  guard; open-weight writer + judge only, `check_open_teacher`; fields the judge kept at
  faithful 2 / responsive 2; targets over the field's decode budget dropped, never cut);
  `val` token NLL selects the checkpoint (early stopping); `--kl-base W` adds
  W * KL(base || adapted) on the target tokens.
* **Anti-forgetting for full fine-tunes** (`--lm-kl-base W`): KL(frozen base || deg) on
  the x-generate target tokens, computed by the frozen base on the identical trunk + fork
  tokens (no grad).

### 12.8 Pluggable backbones (`--backbone`, `deg.json["backbone"]`)

The trunk/fork runtime, the heads, training and the memory planner do not depend on the
architecture beyond one `Family` of layer functions (`model.py`): every supported base is
a hybrid of **state layers** and **attention layers**. `State.lin[i]` = (recurrent state,
conv tail) per state layer, `State.kv[i]` = K/V per attention layer; `ForkState` reads the
prefix K/V by reference and copies only the fixed-size per-row state, so prefill once,
fork-by-reference, chunked prefill (inference and checkpointed training), the CUDA-graph
decode step and the two-copy hybrid are shared code. A family supplies: the trunk/training
state layer (`rec_layer`), attention projections and output (`attn_qkv`, `attn_out`), the
batch-invariant fork versions of both, per-row state shapes, the final norm, the rotary
call and the LoRA targets.

| `--backbone` | layers | per state layer | per attention layer | tokens |
|---|---|---|---|---|
| `qwen3.5-0.8b` (default) | 18 Gated DeltaNet + 6 attention, hidden 1024 | recurrent [16,128,128] fp32 + 3-token conv tail | K/V 2 heads x 256 | ChatML, `<think></think>` gen turn |
| `qwen3.5-2b` (`--base-model Qwen/Qwen3.5-2B`) | same 18 + 6 layout, hidden 2048, MLP 6144 | as the 0.8B | as the 0.8B | as the 0.8B |
| `lfm2.5-1.2b` (LiquidAI LFM2.5-1.2B-Instruct) | 10 gated short-conv + 6 GQA attention (32 q / 8 kv heads x 64), hidden 2048, MLP 8192, tied embeddings, vocab 65,536 | none (empty `[B,0]`) + 2-token tail of `B*x` | K/V 8 heads x 64 | `<\|startoftext\|>` BOS + ChatML, pad `<\|pad\|>`, gen turn without a thinking block |

LFM2 conv layer: `B, C, x = in_proj(norm(h))`, `y = C * conv3(B * x)` (depthwise, no
activation), `h += out_proj(y)`; attention has per-head q/k RMSNorm and plain 1-D RoPE;
norms are `w * norm(x)`. Both families have K/V = 12 KiB/token (6 layers x 2 KiB).

- **Determinism (LFM).** The fork path uses the same kernels as Qwen3.5: `bi_linear` /
  `bi_linear_small` for every projection (fused q/k/v and w1/w3), `fork_attention` (head
  dim 64, GQA group 4), a fixed-config Triton row kernel for the LFM norm
  (`kernels.rms_rows_w`), and an eager elementwise short conv accumulated in fp32 in a
  fixed tap order (per element, so batch-invariant; no conv kernel is needed). No fallback
  path was required. `tests/test_lfm_backbone.py` checks (tiny random LFM2 + the real
  tokenizer, CPU) the functional stack against HF `Lfm2ForCausalLM`, every fork row
  against an independent full forward of context + its query and bitwise alone vs
  batched, keep/decode continuation, chunked prefill = one-shot (inference, padded rows,
  prefix state, checkpointed training incl. gradients), and runs the `test_model` /
  `test_hybrid_generate` independence and hybrid-generate contracts against the LFM
  fixtures; on CUDA (real model) bitwise alone / reordered / bigger schema / fill_many,
  generate rows next to other generate rows, HF match, chunked prefill match.
- **Checkpoints** record `backbone`; a checkpoint loads its own base (pre-§12.8
  checkpoints are Qwen3.5-0.8B) and refuses another `--backbone`; `--generate base`
  attaches that backbone's frozen base. Token budgets (`--max-context-tokens`,
  `--anchor-min-tokens`, `--budget`) count the backbone's own tokens.
- **Memory.** `MemCoef.for_backbone` gives analytic coefficients per backbone (calibrated
  online by the measured scale as before). LFM2.5-1.2B full fine-tuning keeps ~15.7 GiB
  resident (bf16 weights 2.2 GiB + fp32 master/AdamW 12 B and bf16 grads 2 B per trainable
  parameter, 1.04B trainable with the embeddings frozen) vs ~6.8 GiB for Qwen3.5-0.8B, so
  it needs a higher `--mem-target-gb` on a 24 GB L4.
- **Qwen3.5 is size-generic.** `--backbone` / `--base-model` accept `qwen3.5-<size>` or
  the HF id (`Qwen/Qwen3.5-2B`); every Qwen3.5 size is the one `QWEN35` family with the
  same tokenizer, chat layout and gen turn, and the backbone and heads read layer types,
  hidden/MLP widths and head counts from the snapshot's config. `qwen3.5-2b` has its own
  memory coefficients (2x saved hidden per token, ~1.9x wider layer recompute; same
  recurrent state, K/V and vocabulary). `tests/test_qwen2b_backbone.py` runs the model,
  hybrid-generate and long-read contracts on a tiny 2B-shaped Qwen3.5 (hidden 2x, MLP
  3x hidden), checks the real 2B config sizes backbone and heads (meta device), that a
  2B checkpoint refuses other backbones and `--generate base` loads the frozen 2B, and on
  CUDA the real 2B's bitwise independence and HF match.

### 12.9 Data-parallel training: ZeRO-2 over several GPUs (`torchrun`, `deg.zero`)

`torchrun --nproc-per-node N -m deg.train ...` trains one model on N GPUs. Each rank
reads its own uid shard of every source (`zlib.crc32(uid) % N`), draws its own
micro-batches with the per-rank `--budget` and memory plan, and the step's gradient is the
gradient of the **global** step loss: each loss term's item counts are all-reduced before
the forward (`global_counts`), each rank's term is its share of the global mean, and the
ranks' gradients are **summed** (not averaged; a rank with more decide items carries more
of the decide gradient, exactly as one batch of all ranks' records). One global batch is N
per-rank micro-batch sets (N x `--budget` tokens). Gradient clipping uses the global norm.
A rank that cannot finish its step (minimal unit OOM) makes all ranks redraw
(`all_ranks_ok`), so every optimizer step is complete on every rank.

**ZeRO-2** (`deg.zero.ZeroMaster`, default with N > 1; `--no-zero` = whole masters per
rank). bf16 compute weights stay whole on every rank (the functional backbone calls its
layers' weights directly). The fp32 master weights, both AdamW moments and the fp32
gradient are sharded: the trainable parameters form one flat vector cut into 32M-element
buckets; rank r owns the r-th of N pieces of each bucket. A step's bf16 gradients stay on
the parameters until the step's last part (several-part steps sum them in a pinned host
fp32 vector, so the device never holds two gradient copies), then a bucketed fp32
reduce-scatter (SUM) gives each rank its shard's gradient; AdamW steps the shard (one flat
parameter: element-wise the same arithmetic and decay as the unsharded trainer); each
rank rounds its shard to bf16 and an all-gather refreshes every rank's compute weights
(bitwise `p.copy_(master)`). Per trainable parameter a rank keeps 2 B (bf16 grad) +
16/N B instead of 18 B: Qwen3.5-2B (1.37B trainable, embeddings frozen) on 3 L4s holds
~3.8 GiB weights + 2.6 GiB grads + 6.8 GiB shard state = ~13 GiB static vs ~26 GiB
unsharded. The memory planner uses this static (`sharded_static_bytes`), then the
measured resident memory as before.

- **Start/resume.** Heads (fresh, per-rank seeds) and backbone are broadcast from rank 0
  at start. Every rank keeps the whole bf16 weights, so rank 0 saves an ordinary deg
  checkpoint (`step-N/`, same format as one GPU; no sharded files, no conversion).
  `--init <run>/step-N --start-step N` resumes: weights from the checkpoint, the cosine
  schedule continues at step N, optimizer moments start fresh with a `--warmup` re-warm
  (as on one GPU), the data order is re-drawn (the rng mixes in N). The process group
  timeout is 4 h (rank 0 alone calibrates, evaluates and saves at the end).
- **Smoke on one GPU.** `--zero-sim-world N` shards as rank 0 of N ranks with no
  collectives: the real per-rank memory and compute time on one GPU (other shards are not
  updated; never a real run).
- `tests/test_zero.py` (CPU, gloo, 3 processes): sharded masters + AdamW + global clip
  equal one process stepping whole fp32 masters on the summed gradient, for one-part
  (device bf16 grads) and two-part (host fp32 sum) steps, replicas bitwise equal; the
  flat layout covers every element once; global counts make two ranks' gradients and
  losses sum to the one-batch ones.


### 12.10 Grounded extraction on media (`deg.ground`)

A text extract field copies a span and `meta.span` says where it came from. On a media
context the same extract field may point into an image or audio item; then meta gives a
box or a time window (§8). There is no annotation: the mode follows from the field's type
(§3), and text vs media follows from where the pointer lands.

- **Pointer.** The extract fork and span head are unchanged; they already score every
  context token, the media **soft tokens** included. `landing`: when the start pointer's
  probability mass on the soft tokens beats both the text tokens' and null's, the field
  is located in media (`decode_ground`: the best pair inside one item, logits
  renormalized over soft tokens + null); otherwise the text path runs exactly as before
  with the soft tokens masked. No parameters are added and a context without media never
  takes this path, so text checkpoints load unchanged and text fills are bitwise
  identical.
- **Image geometry.** Gemma 4 resizes an image (aspect-preserving) to multiples of 48 px
  with at most `max_soft_tokens * 9` 16-px patches and average-pools 3x3 patches: soft
  token `k` is cell `(k mod gw, k div gw)` of a `gw x gh` grid in raster order
  (`ground.image_grid`, tested against HF's resize and the tower's position ids).
  `start` = the box's top-left cell, `end` = its bottom-right cell: `start <= end` in
  raster order, the span head's own order. Valid pairs have `col(end) >= col(start)` and
  `row(end) >= row(start)`; cells map back to source pixels (`cells_to_box`).
- **Audio geometry.** One soft token per 40 ms (`audio_ms_per_token`): `start`/`end` are
  the window's first and last token, `[start * 0.04, (end + 1) * 0.04]` s, clipped.
- **Value from the region: crop re-read.** A media item has no text to copy, so the
  located region (padded 15 % per side, at least 48 px; audio: the window +-0.5 s) is cut
  from the source and filled as the only media item of a one-field x-generate schema
  (`ground.read_schema`: the leaf's key, title/description, maxLength); the text is then
  parsed by the leaf's mode like a copied span (`response.extract`: number/date parsing,
  constraints). All reads of a `fill_many` batch run as one nested batch, before the
  KNOWN VALUES block needs them. Chosen over a region-masked attention bias because (1) at
  a receipt's 280-token budget one soft token covers ~48x48 resized px, often more than a
  printed word; a crop at the same budget reads the region at several times the
  resolution, which masking cannot add; (2) it reuses the trunk's media path unchanged: no
  change to the fork attention kernels, the CUDA decode graphs or the text path; (3) its
  training data are ordinary media records. Media given as embeddings only (`Media` parts,
  no source file) can be located but not read (`null_reason: "media_unread"`).
- **Training.** A media record's `grounds` (concrete pointer -> `{"media", "box"}` /
  `{"media", "time"}`, or null = not in the media) gives an extract leaf's pointer target:
  the gold corner soft tokens (`ground_target`; null -> the null slot), in the normal
  `extract` term (`extract_media` logs its share). A present value with neither a text
  span nor a ground entry trains no pointer (never null). Crop-read records
  (`records.read.jsonl`, family `mm-read-v1`) are the gold region cropped exactly as
  inference crops (same `crop_sha` key, so the media cache serves both) with the leaf's
  read schema and gold value.
- **Gold.** `python -m deg.datagen.mm_ground --set cord-v2`: CORD-v2 word boxes
  (`ocr_boxes`, with their line category) matched to the gold values (items
  name/quantity/price, totals); a matched field turns from x-generate into a plain string
  (an extract field) in `records.ground.jsonl` and the `.ground` eval files. A value with
  no exact same-category word run gets no entry. invoices-donut (no boxes),
  RICO-Screen2Words (no view hierarchy, no localized field), MINDS-14 and AMI (no word
  timings in these sources) stay value-only.
- **Eval.** `python -m deg.bench.mm_eval run --ground` fills `<set>.<split>.ground.jsonl`
  and reports, beside (never in) the set score: box IoU, IoU >= .5 rate, containment
  (share of the gold box inside the predicted one), gold-centre hit, time IoU and
  containment, and null accuracy for gold-null entries. A field not located scores 0.
- `tests/test_ground.py` (CPU, tiny Gemma 4 hub): geometry against HF and the towers,
  corner decoding, landing and null, CORD matching and crop-read records, a training step
  with image and audio pointer targets, fills that locate and read values (and the text
  path when the pointer lands in text), and the metrics.