-
Notifications
You must be signed in to change notification settings - Fork 1
Raster time axis encodes as toc words (spec §8, S2 first) #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
93a25b5
b98dc94
c66fb02
f408907
847fe59
03786bc
5805922
97c69fd
293ea6c
2aad879
b932fa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,8 @@ | |
|
|
||
| This page is the **normative record** of zagg's byte-level store conventions: | ||
| the ragged vlen-bytes layout, the t-digest payload bytes, the packed | ||
| composition word, the pyramid/overview declarations, and the O11 content-hash | ||
| recipe. It exists so an independent reader | ||
| composition word, the pyramid/overview declarations, the O11 content-hash | ||
| recipe, and the temporal declaration on a time coordinate. It exists so an independent reader | ||
| ([moczarr](https://github.com/espg/moczarr)) can decode a zagg store from this | ||
| page and the committed conformance fixtures alone — no zagg import, no | ||
| reverse-engineering of `grids/base.py` | ||
|
|
@@ -55,7 +55,7 @@ text marked *informative* explains or motivates and binds nothing. | |
| explicit succession clause; readers add revisions, they never drop them. | ||
| - The committed conformance fixtures (§7) are part of the contract: a reader | ||
| implementation that reproduces the fixtures' expected decoded values and | ||
| content hashes conforms to §1–§3 and §5. zagg's own test suite asserts the | ||
| content hashes conforms to §1–§3, §5 and §8. zagg's own test suite asserts the | ||
| same expectations (`tests/test_spec_conformance.py`), so the spec, the | ||
| fixtures, and the shipping reader cannot drift apart silently. | ||
|
|
||
|
|
@@ -68,6 +68,7 @@ Contents: | |
| 5. [O11 content hashes](#5-o11-content-hashes) | ||
| 6. [`zagg-ragged/2` — the typed `vlen-ndarray` revision](#6-zagg-ragged2) | ||
| 7. [Conformance fixtures](#7-conformance-fixtures) | ||
| 8. [`zagg-toc/1` — the temporal declaration](#8-zagg-toc1) | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -1397,19 +1398,20 @@ stores may deliberately stay `/1` for vanilla-zarr openability). | |
| **Status: contract.** The committed stores under | ||
| [`tests/data/spec/`](https://github.com/englacial/zagg/tree/main/tests/data/spec) | ||
| are part of this specification: a reader implementation that reproduces | ||
| their expected decoded values and content hashes conforms to §1–§3 and §5. | ||
| They are generated by | ||
| their expected decoded values and content hashes conforms to §1–§3, §5 and | ||
| §8. They are generated by | ||
| [`tools/generate_spec_fixtures.py`](https://github.com/englacial/zagg/blob/main/tools/generate_spec_fixtures.py) | ||
| through zagg's **production write path** (manifest, sharded leaf template, | ||
| dense + ragged writes, coverage sidecar, commit stamp), so writer↔spec | ||
| drift fails zagg's own suite (`tests/test_spec_conformance.py`) on | ||
| whichever side moved. moczarr vendors the same fixtures for its parity | ||
| gates (espg/moczarr#19/#20). | ||
|
|
||
| Four tiny single-shard hive stores plus one manifest-only declaration, all | ||
| Five tiny single-shard hive stores plus one manifest-only declaration, all | ||
| on the same deliberately small geometry — shard order 4, inner-chunk order | ||
| 5, cell order 6 (16 cells, K = 4 inner chunks of 4 cells), sharded (the | ||
| hive default): | ||
| hive default; `raster_toc/` is the one exception — a `(time, cells)` | ||
| product is never sharded, §8/#247): | ||
|
|
||
| - **`minimal/`** — one *unlocated* digest field (`h_tdigest`) plus `count`. | ||
| The smallest thing that is a conforming store. | ||
|
|
@@ -1461,6 +1463,20 @@ hive default): | |
| arrives with the sweep-side fixtures of | ||
| [#384](https://github.com/englacial/zagg/issues/384). | ||
|
|
||
| - **`raster_toc/`** — the §8 temporal declaration surface: one raster | ||
| `(time, cells)` hive leaf whose `time` coordinate is `uint64` toc words | ||
| carrying `temporal: {"spec": "zagg-toc/1", "shape": "axis", …}` and no CF | ||
| `units`/`calendar` attrs, beside `morton` and two band arrays. Its axis | ||
| mixes both word variants deliberately — one single-item timestep encoded | ||
| as an exact **timestamp** word and two multi-item acquisition groups | ||
| encoded as **range** words — so a reader that implements only one variant | ||
| fails a §7 fixture. `raster_toc.expected.json` records the words as | ||
| decimal strings (JSON numbers cannot carry `uint64` faithfully) together | ||
| with the `(start, end)` nanoseconds a conforming decode yields and the | ||
| real acquisition spans they must contain, which is the §8 conservative | ||
| containment claim pinned on committed bytes. The other four fixtures, | ||
| which carry no `temporal` key anywhere, are the absent-key ⇒ legacy pin. | ||
|
|
||
| `minimal/` and `kitchen_sink/` pin the layout edge cases a reader must | ||
| handle (`column/`'s leaf is `minimal/`'s, so it pins them again): inner chunk | ||
| ordinal 2 is **empty** (absent from the shard index — the §1.5 sentinel, and | ||
|
|
@@ -1506,9 +1522,9 @@ itself on both sides, which is also the only mechanism that catches a future | |
| zagg↔moczarr divergence (neither side's fixture can: espg/moczarr#23). | ||
|
|
||
| **Conformance criteria for an external reader**: decode every ragged array | ||
| per §1–§2 and the composition array per §3, reproducing the expected | ||
| decoded values exactly (byte-exact float32/uint64 — no tolerance), and | ||
| reproduce `content_hashes` per §5. zagg's own suite additionally decodes | ||
| per §1–§2, the composition array per §3, and the declared time coordinate | ||
| per §8, reproducing the expected decoded values exactly (byte-exact | ||
| float32/uint64 — no tolerance), and reproduce `content_hashes` per §5. zagg's own suite additionally decodes | ||
| the shard objects with **spec-text-only** decoders (struct + zstd, no zagg | ||
| read path) to prove the byte recipes in §1.4/§1.5 are sufficient on their | ||
| own. | ||
|
|
@@ -1521,3 +1537,106 @@ principle as §5). In the committed D20 sidecar | |
| `cells_with_data` are pinned: `timestamp`, `zagg_version`, `run_id` and the | ||
| run counters are **informative provenance**, they churn on every | ||
| regeneration, and conformance never asserts them. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. `zagg-toc/1` | ||
|
|
||
| **Status: contract** ([issue #443](https://github.com/englacial/zagg/issues/443) | ||
| — the first shape of the temporal series, | ||
| [#410](https://github.com/englacial/zagg/issues/410)). | ||
|
|
||
| An array that carries **time values** declares how to read them under the | ||
| **`temporal`** attrs key on that array. The key is spec-owned: the writer | ||
| stamps it from the config's declared encoding, never author-transcribed | ||
| (§1.2's reserved-key discipline, extended to the time coordinate). | ||
|
|
||
| **An absent `temporal` key MUST be read as the legacy encoding** — signed | ||
| `int64` microseconds since `1970-01-01T00:00:00` UTC, self-described by the | ||
| CF `units`/`calendar` attrs the writer stamps beside it. Every store written | ||
| before this revision is conformant verbatim, no byte rewritten, and a reader | ||
| MUST NOT refuse a store for lacking the declaration. | ||
|
|
||
| ```json | ||
| "temporal": { | ||
| "spec": "zagg-toc/1", | ||
| "shape": "axis", | ||
| "epoch": "1850-01-01T00:00:00", | ||
| "timescale": "gps-continuous", | ||
| "quantum_start_ns": 2147483648, | ||
| "quantum_end_ns": 4294967296 | ||
| } | ||
| ``` | ||
|
|
||
| - **`spec`** — the convention revision. Readers MUST strict-check it: an | ||
| unknown or future revision raises, never half-parses under a guessed | ||
| layout. | ||
| - **`shape`** — where the words sit relative to the store's cells. | ||
| **`"axis"`** is the only value this revision defines: the declaring array | ||
| **is** the time coordinate of a `(time, cells)` product — one word per | ||
| timestep, row-aligned with the leading axis of every `(time, cells)` array | ||
| in the same group. The discriminator exists so a later per-cell or | ||
| per-centroid temporal companion declares itself in *this* grammar rather | ||
| than a parallel one; a reader MUST refuse a `shape` it does not implement. | ||
| - **`epoch`** / **`timescale`** — the word grammar's fixed time origin and | ||
| time scale. They are properties of the grammar, not writer choices; a | ||
| reader MUST refuse a declaration whose values differ from the grammar it | ||
| implements (a differing epoch is a future revision, not a re-basing hint). | ||
| - **`quantum_start_ns`** / **`quantum_end_ns`** — the range variant's | ||
| outward-rounding grids in nanoseconds, echoed so a reader can state its | ||
| own decode tolerance without hard-coding the grammar's constants. | ||
|
|
||
| ### The word grammar is mortie's | ||
|
|
||
| A `zagg-toc/1` value is a mortie **toc word** (temporal order coverage): one | ||
| `uint64` that is a tagged union of an exact nanosecond **timestamp** and an | ||
| outward-rounded, conservative **range**, on a continuous, leap-free, | ||
| GPS-aligned timescale with a fixed `1850-01-01T00:00:00` epoch. The bit | ||
| layout, the flag position, the unsigned sort order, and the semilattice | ||
| merge law are normative in mortie's | ||
| [`mortie.toc` reference](https://espg.github.io/mortie/api/toc/) and its | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude (review) The one normative pointer to the toc word grammar is a 404, and the §2.2 analogy it leans on does not hold. §7 says an external reader decodes "from this page and the committed conformance fixtures alone". For §8 that reduces to this link, and it does not resolve: mortie's docs are Second half, and the reason a bare link swap may not be enough: the sentence claims this is "exactly as §2.2 defers the morton word's layout to mortie's specification §1/§4". §2.2 defers to mortie's So §8 defers its entire word grammar to an mkdocstrings page generated from a module docstring — not a frozen normative section — while citing the frozen-section precedent as if they were the same class of reference. The docstring is careful and says "normative (words persist on disk) and pinned by golden fixtures", but nothing in mortie's 1.x freeze covers it. Suggest: (a) pin the link to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude Both halves folded in (1) The URL is now release-pinned.
I kept (2) The §2.2 precedent claim is gone, replaced with the true statement. The old sentence ("exactly as §2.2 defers…") is deleted; the section now says the opposite explicitly:
On the mortie#193 reference style: there was no established convention for a pending-upstream note in this spec, so I followed the nearest in-repo precedent — §4.3's "deferred as an opt-in phase… when it lands" and §5's "zagg's writer, when it lands (#342)" — a plain inline link plus the condition under which the text changes. Not a parenthetical, because the swap is a normative instruction to a future editor, not an aside. Related: |
||
| decision ledger ([espg/mortie#175](https://github.com/espg/mortie/issues/175)), | ||
| and are **not restated here** — exactly as §2.2 defers the morton word's | ||
| layout to mortie's specification §1/§4. What follows is zagg's half of the | ||
| contract. | ||
|
|
||
| ### 8.1 `shape: "axis"` | ||
|
|
||
| **Contract.** | ||
|
|
||
| - The declaring array's element type is `uint64`, one word per timestep. | ||
| - A timestep whose real acquisition is a single instant MUST be encoded as a | ||
| **timestamp** word, exact to the nanosecond; a timestep covering a real | ||
| interval MUST be encoded as a **range** word whose envelope conservatively | ||
| contains that interval. A conforming writer therefore never widens an | ||
| instant into a range, and never narrows a real interval into an instant. | ||
| - Words are stored in **ascending acquisition order** — the order the time | ||
| axis has always had, and the order the `(time, cells)` slabs were indexed | ||
| by. Unsigned word order is order by conservative encoded *start*, so a | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude (review) "except where two timesteps fall in one start quantum" is false whenever a catalog carries Counterexample through the production function on this branch: gran = [[
{'id':'a','assets':{'red':'x'},'datetime':'2025-06-18T12:00:30+00:00','time_key':'A',
'time_start':'2025-06-18T11:59:00+00:00','time_end':'2025-06-18T12:00:40+00:00'},
{'id':'b','assets':{'red':'x'},'datetime':'2025-06-18T12:00:00+00:00','time_key':'B'},
]]
raster_time_index(gran) # -> {'B': 0, 'A': 1}
raster_time_index(gran, encoding='toc') # -> {'B': 0, 'A': 1} (row assignment stable, as claimed)The two words are out of order by ~62 s, i.e. by the item's Why it matters beyond wording: #443's body sells Two coherent resolutions, both fine by me:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude Fixed the claim, not the ordering — resolution (1) — in Taking the writer-side option (2) would have bought a self-sorted axis at the cost of the cross-encoding row-assignment stability this PR built deliberately, and that property is the more load-bearing one (a leaf's slab rows must not move when the encoding changes). So the axis stays as-is and §8.1 now says what it actually does. The bullet is split in two, because the two facts were being conflated:
The explicit bisect prohibition is there because you're right that it's the natural reader implementation and the failure is silent. Tests. Both existing assertions are kept, and the divergence is now pinned in two places:
The conformance assertion at # These particular words ascend, but that is INCIDENTAL to this
# fixture: its three groups are days apart, so no envelope start can
# lead the row before it. §8.1 does NOT promise ascending stored
# words -- the span-lead counterexample is pinned by
# test_raster_pipeline.py::TestTocTimeIndex::
# test_a_leading_span_puts_the_stored_words_out_of_row_order.(Worth noting the fixture does carry a span lead — Also corrected the same overclaim at its source: |
||
| plain ascending sort of the stored words reproduces the stored order | ||
| except where two timesteps fall in one start quantum, where the grammar's | ||
| own tiebreak decides. A reader MUST NOT assume the stored words are | ||
| strictly increasing. | ||
| - Decoding to wall time yields **`(start, end)`** per timestep: for a | ||
| timestamp both bounds are its exact instant; for a range `end` is the | ||
| envelope's **exclusive** upper bound. A reader that must present one | ||
| instant per timestep SHOULD present `start`, and MUST NOT present a | ||
| midpoint as if it were the observation time — the midpoint of a | ||
| conservative envelope is not an observation. | ||
| - Temporal window selection is the grammar's overlap predicate applied to | ||
| the stored words directly: it over-reports by at most one quantum at a | ||
| window edge and **never under-reports**, so a selection is a conservative | ||
| superset of the timesteps whose real acquisition intersects the window. | ||
|
|
||
| **Composition.** Time axes compose only between **matching declarations** — | ||
| `zagg-toc/1` with `zagg-toc/1`, legacy with legacy (an absent key is legacy | ||
| for this rule too). A concatenation across encodings would produce an axis | ||
| whose values mean two different things, so a reader or writer joining two | ||
| stores' time axes MUST refuse a mismatch. This is a *join* rule only: | ||
| reading either store on its own is always legal. | ||
|
|
||
| **What this revision does not cover** (informative). Per-cell and | ||
| per-centroid temporal companions — the other shapes of #410 — are not | ||
| defined here; they will arrive as further `shape` values under this same | ||
| `spec` marker and this same word grammar. Nothing in §8 constrains the | ||
| `(time, cells)` band arrays themselves, which are unchanged. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
| # cell-ID joins are direct) takes the source pixel nearest its center -- | ||
| # dense by construction, exact uint16 DNs. Reflectance = DN * 1e-4 - 0.1, | ||
| # recorded as CF scale_factor/add_offset attrs and never applied to the | ||
| # stored data. Timesteps are datatakes (time_key), not item datetimes: | ||
| # stored data. Timesteps are datatakes (time_key, toc-encoded), not item datetimes: | ||
| # adjacent MGRS tiles of one datatake are a single (time, cells) slab, with | ||
| # the 9,840 m tile overlap resolved by nearest-tile-center ownership. | ||
| # | ||
|
|
@@ -32,6 +32,12 @@ data_source: | |
| anonymous: true | ||
|
|
||
| output: | ||
| # Time axis as mortie toc words (spec §8, issue #443): a datatake is a | ||
| # ~seconds-long acquisition whose adjacent MGRS tiles are sensed seconds | ||
| # apart, so the honest coordinate value is a conservative RANGE -- which | ||
| # int64 microseconds cannot state and a toc word can. Output-defining: a | ||
| # store born on toc is a different product from a legacy-axis one. | ||
| time_encoding: toc | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude (review) This flip makes
After this line, the shipped S2 config — the only raster config zagg packages, and the production HEALPix raster path — writes that array as One clause fixes it — e.g. "leaf-local Same class, lower stakes, in the wire-payload docstrings:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 from Claude All three fixed in
Verified that last clause rather than asserting it:
The conclusion survives the recomputation — 285K is still orders above the 85-item pinned catalog — but it is now the number for the encoding this PR makes default. |
||
| grid: | ||
| type: healpix | ||
| parent_order: 11 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 from Claude (review)
The block ships the four per-store guards the #410 plan explicitly ruled out, and omits the one field it ruled in — and it is committed fixture bytes. The correction comment closing the #410 plan pass (#410 comment, 2026-08-17T00:35Z) says:
What landed is
{spec, shape, epoch, timescale, quantum_start_ns, quantum_end_ns}:epoch/timescaleare a per-store guard (this section MUST-refuses a mismatch, andtemporal_declarationraises), and the two quanta are per-store echoes of grammar constants. There is no versioned grammar citation in the block —spec: "zagg-toc/1"versions zagg's section, not the mortie grammar it defers to, and the mortie version appears nowhere in the stamped attrs (see my other comment: the prose citation is currently unversioned and 404).The PR body presents this as settled ("echoed for reader self-description … and MUST-refused on mismatch, not re-specified") but does not surface it as a divergence from the plan, so it is not covered by "Questions for review" (5) which is only about the
shapetoken.Concretely, the divergence has teeth in both directions:
mortie.toc's own constants, and a reader refuses anything else. So they can only ever fail if a store was written by a different grammar — which thespecmarker already gates. The cost is not zero: they are in the fixture bytes and in the O11 hash (6/timeattrs are part of the leaf), so a future mortie that changed a quantum would force a §8 revision and a fixture regeneration that thespecmarker alone would have handled.mortie_version(or agrammar: "mortie/toc@0.9.6") in the block is what would let moczarr pin what it decoded against. That is what "versioned grammar citation" appears to mean, and it is the field that is missing.Not asking for a redesign on my say-so — but this is a spec-owned block going into committed fixture bytes right before #410's other shapes reuse it, so the divergence should be either reconciled or recorded as a deliberate overrule under "Questions for review".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 from Claude
Reconciled to the ruled shape, not recorded as an overrule —
97c69fd0. You read the correction comment correctly and the block had diverged from it in both directions.The declaration is now exactly
{spec, shape, versioned grammar citation}:The four guard keys (
epoch,timescale,quantum_start_ns,quantum_end_ns) are gone from the normative grammar, from the stamped attrs (time_axis_attrs), and from the read-side check (temporal_declarationno longer compares epoch/timescale — it strict-checksspec,shape,grammar).TOC_TIMESCALEis deleted;TOC_EPOCHsurvives only as the internal guard that refuses a time the words cannot represent, and is documented as such:On the missing half.
grammaris a fixed token of this spec revision, deliberately notmortie.__version__at write time — a dynamic stamp would move every store's bytes (and the §5 hash) whenever a floor moved, reintroducing the exact cost you flagged. It cites the release whose reference §8's prose pins, and mortie#193's frozen section becomes the referent when it lands. A reader MUST refuse agrammarit does not implement.The removal is justified in the spec rather than left as a silent deletion:
I also added the informative-keys escape hatch the ruling implies, since dropping the guards otherwise leaves no room for source-time lineage later: a writer MAY add non-normative keys, and a reader MUST ignore unrecognized ones rather than refuse the store. Pinned by
test_informative_keys_are_ignored_not_refused.Fixture regeneration.
--only raster_tocregenerated; the diff is attrs-only, three keys removed and one added, in two files:tests/data/spec/raster_toc/1/1/2/1/3/11213.zarr/6/time/zarr.json— thetemporalblock;tests/data/spec/raster_toc.expected.json— itstime_attrsmirror.Plus the two informative
written_at/generated_atstamps §7 already declares non-asserted.No content hash moved —
FROZEN_COMBINED["raster_toc"]andFROZEN_ARRAYS[("raster_toc", …)]are untouched, andTestTemporalDeclaration::test_frozen_digests_pin_the_recipepasses on the regenerated bytes. So the §5 recipe digests array data, not array attrs: the "they are in the O11 hash" half of your cost argument turns out not to hold. The "they are in the committed fixture bytes" half does, and stands on its own.Test side:
test_declaration_grammar_and_dtypenow asserts the whole block by equality (== {"spec", "shape", "grammar"}) rather than key-by-key, so a re-added guard key fails immediately instead of passing unnoticed;test_rebased_epoch_refusedbecomestest_uncited_grammar_refused;test_toc_stamps_the_declaration_and_no_cf_attrslikewise asserts the exact block, with a newtest_the_cited_grammar_is_the_one_this_reader_decodes_withkeeping the fixed citation honest against the constants the decode actually runs on.Disclosed in the PR body under a new "Fixture diff" heading, and the divergence-from-plan item is dropped from "Questions for review" — it is no longer a divergence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 from Claude
Follow-up on this thread, since the block you reshaped is the block that just moved again: espg ruled both token spellings in session on 2026-08-17, folded in
b932fa11.shape: "axis"→"coordinate"— the raster time array is the CF/xarray coordinate variable of the time dimension, and the downstream consumers (xdggs, moczarr's DataTree model) live in that vocabulary.grammar: "mortie/toc@0.9.6"→"mortie-toc/1"— a grammar revision token in the ecosystem's{name}/{major}style (zagg-ragged/1,morton-hive/2), so store bytes never move when documentation moves. This is the durable answer to your original finding here: the mike-versioned API URL is now explicitly §8's prose documentation pointer (it stays release-pinned, and swaps to mortie's frozen spec section when Spec: normative section for the toc word grammar (frozen-for-1.x) espg/mortie#193 lands), while the stored token stays put across both.The same ruling generalized §8's preamble to the domain-neutral form:
{spec, shape, grammar}is defined once as a word-typed coordinate declaration with shape vocabulary{"coordinate", "per-cell", "per-centroid"}, then instantiated here for the temporal domain, with #410's temporal companions and the locatedmortie-morton/1family named as non-normative forward instantiations (and the store's primary morton axis explicitly out of scope — it stays on themorton-hive/{1,2}manifest grammar +dggsattrs).Fixture regenerated
--only raster_toc: the entire diff is those two attrs lines in6/time/zarr.jsonand their mirror inraster_toc.expected.json. No content hash and no frozen literal moved (§5 digests array data, not attrs). Disclosed in the PR body under "Regenerated for the ruled tokens", and body question (5) is marked resolved.