Task: Write the exporter from the same tables
Table of Contents
This page documents a task in the Decide the XML strategy for ORE documents story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Write the document back from the same tables. The write direction need not mirror the read direction; it has to be valid, cheap and lossless. Three rules carry it: schema order, present fields only, and a decimal form that never exponentiates.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Decide the XML strategy for ORE documents |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-17 |
3. Acceptance
- Output validates against
input.xsdfor every document in the corpus. - A decimal writes as fixed point at the column's scale and never as an exponent.
- Only text that needs escaping is escaped, decided by one scan.
- Reading the output yields the same values as reading the original.
4. Plan
(Implementation strategy. Written when work starts; key decisions
are distilled into the parent story's * Decisions at close, but the
plan itself stays — it is the historical record of what we did.)
5. Plan
The direction is a walk of the table, not a replay of the reading. The emitter takes the same document_type the binder takes and walks the record in table order, which the generator emits in schema order. Nothing about the document that was read survives, so a record built by any other route writes the same way.
Three rules carry it.
- Present fields only. A field whose presence bit is clear writes nothing. A required field whose bit is clear is an error, not a silent omission: the writing would not validate, so it is not made.
- A decimal is plain text. The writer calls the decimal's own canonical
rendering, which never exponentiates and never invents a column scale. A
continuous quantity uses the shortest text that reads back to the same
double, and may exponentiate because
xs:floataccepts it. - Escape in one scan. A text node escapes
&,<and>; an attribute also escapes"and the whitespace a parser would normalize away. A value that needs none of them is copied whole.
The table grew one thing: a repeated member's operations. A repeated member
is a std::vector whose element type only the generated code knows, so
field now carries a vector_ops of three functions – append, count, at.
The reader appends; the writer measures and indexes. Before this, the row
carried only append, which is why the reading existed and the writing did
not.
The writing found three defects in the generator. None of them was visible from the reading, because the reading tests counted documents rather than comparing values:
- A repeated compositor repeats its members.
conventions.xsdstates the root asxs:choice minOccurs"0" maxOccurs="unbounded", so every member may appear any number of times. The generator read each member's own =maxOccursand modelled Deposits as one optional record, so a document with twelve Deposits bound twelve values into one record and the writer emitted a single<Deposit>with the values concatenated. The compositor's repetition now reaches its members. - A repeated leaf carries its element's own kind. Every repeated leaf row
said
scalar_kind::textregardless of the element, so a repeatedxs:integerwrotestd::stringvalues into astd::vector<std::int64_t>. It was silent heap corruption rather than a wrong answer, and the netting family'sCurrenciesis the case:vector<std::int16_t>over an enumeration, read as strings. - An enumeration index needs its spellings. A repeated enumerated element
now has
scalar_kind::enumeration: the reader validates the spelling and stores the index, the writer renders the spelling back.
A generated record states its own zeros. An int16_t member with no
initializer is indeterminate in a default-initialized record, so a record
built by hand carried garbage into a writing and failed on an enumeration
index that was never set. Every numeric member now states = 0=. The decimal
already defaulted to zero.
6. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
Emitted documents that validate against input.xsd |
246 of 246 | ./scripts/validate_ore_examples.sh build/output/linux-clang-release-ninja/xml-roundtrip/emitted external/ore/xsd |
pending |
| XML suites, all green | 537 cases, 63,635 assertions | publish/bin/ores.ore.core.tests "[ore][xml]" |
pending |
| Corpus documents written back and re-read stably | 246 | publish/bin/ores.ore.core.tests "[ore][xml][emitter]" |
pending |
7. Notes
8. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
9. PRs
| PR | Title |
|---|---|
| #3447 | [ore] Write the document back from the same tables |
10. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
11. Result
Merged. The writer walks the same table the binder fills and writes only the fields whose row says they appeared. Its fourth acceptance criterion, that a reading of the output yields the same values, is proved by the round-trip task rather than here, because that is where the harness lives.