← hex_recover · API reference · version 0.1.6
5 file(s), 1407 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
orient_k fn | 40 | — | 111 |
orient_m fn | 55 | — | 112 |
field_digest fn | 133 | HXV-001 | 93 100 405 |
digest_eq fn | 144 | — | 89 91 93 98 100 405 … (7 total) |
digest_text fn | 152 | — | 239 255 |
form_key fn | 166 | — | 167 |
form_key_noturn fn | 177 | HXV-002 | 168 |
count_digest fn | 188 | HXV-002 | 104 145 |
turns_cyclic_eq fn | 198 | HXV-006 | 331 368 370 372 376 378 … (7 total) |
field_exact fn | 227 | HXV-001 | 239 89 90 |
field_exact_text fn | 239 | — | 83 84 |
field_norm fn | 251 | HXV-001 | 255 144 152 430 91 92 … (12 total) |
field_norm_text fn | 255 | — | 203 224 244 |
form_maxlen fn | 267 | — | 312 499 |
forms_upto fn | 276 | HXV-009 | 308 89 162 476 477 478 |
forms_at_level fn | 307 | HXV-009 | 481 482 483 484 485 504 … (7 total) |
turns_reverse_eq fn | 325 | HXV-007 | 402 411 413 |
forms_sides fn | 350 | — | 396 |
forms_a3 fn | 393 | — | 90 |
fit_chunk fn | 38 | HXV-003 | 150 221 428 143 259 |
REGIME_R1 const | 62 | — | 69 161 173 181 246 256 … (18 total) |
REGIME_R2 const | 63 | — | 69 166 392 413 424 434 … (11 total) |
Rebuilt struct | 68 | — | 140 143 161 166 243 246 … (12 total) |
LEVEL const | 80 | — | 89 128 |
candidate_forms fn | 88 | HXV-004 | 140 13 60 126 430 |
level1_forms fn | 96 | — | 504 505 |
rebuild fn | 140 | HXV-005 | 180 20 43 302 |
rebuild_with fn | 143 | — | 140 172 248 439 |
rebuild_text_with fn | 171 | — | 456 458 |
rebuild_text fn | 179 | — | 33 307 |
IdxEntry struct | 202 | — | 210 227 |
RecoveryIndex struct | 209 | — | 217 218 243 253 |
index_build fn | 217 | HXV-002 | 130 431 |
rebuild_indexed fn | 243 | HXV-008 | 255 438 447 |
rebuild_indexed_text fn | 253 | — | 457 459 |
hull_keys fn | 321 | — | 389 |
side_heading fn | 361 | — | 405 |
side_len fn | 378 | — | 409 |
rebuild_construct fn | 388 | HXV-004 | 440 217 223 230 238 246 … (13 total) |
rebuild_construct_text fn | 439 | — | 314 |
src/formcensus.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// formcensus — THE FIELD DIGEST, AND THE CENSUS THAT DECIDES LAW F AT A LEVEL.
//
// Law **F** is injectivity: `draw(m1) = draw(m2) => m1 = m2`. It follows from **D**, but its
// COVERAGE is a measurement — how far out the admitted space stays injective. The census is that
// measurement, and because the admitted space is finite at each level, law F is **decided** there,
// not sampled (`DESIGN.md` §8).
//
// THE DIGEST IS CANONICAL UP TO ORIENTATION AND TRANSLATION, and that is the whole design
// decision. A stencil is authored ONCE and placed at one of the 12 orientations (`O`), so two
// forms that differ only by where they were dropped or which way they face are **the same
// stencil** — law **I**. A digest that distinguished them would report the 12 orientations as 12
// different shapes and law I would be invisible from here. So the digest quotients by `O` and by
// translation, and what survives is the shape itself.
//
// CELLS DETERMINE EDGES AT THIS LEVEL, so the digest is over cells alone. By `SPEC` **I3** the
// wall IS the boundary of the filled region — the edge set is a function of the cell set, so
// digesting cells digests both. That stops being true when edges carry their own MATERIAL (a door,
// rung A5), and the digest gains a material component there; recorded rather than assumed.
//
// EXACT, NOT HASHED. The digest is the sorted cell list itself, compared element by element — no
// modular hash anywhere. A hash collision in a census would look exactly like a law F violation,
// which is the one mistake this file exists to detect.
use hex_field::*;
use hex_form::*;
// A cell's position packed into one sortable integer. The span is wide enough for any census
// shape (levels 1-3 stay inside a few dozen cells of the origin) and the offset keeps it positive.
const CKEY_OFF = 400;
const CKEY_SPAN = 1600;
fn ckey(k: integer, m: integer) -> integer { (k + CKEY_OFF) * CKEY_SPAN + (m + CKEY_OFF) }
// ── the 12 orientations, exactly ───────────────────────────────────────────
//
// `o` is `flip * 6 + rot`: reflect first (`X2`, `k -> -k`), then rotate `rot` times by 60 degrees
// (`X1`, `k' = (k-m)/2, m' = (3k+m)/2`). Both are exact integer maps on the doubled lattice — the
// reason a stencil rotates and reflects with no resampling and no drift.
pub fn orient_k(k: integer, m: integer, o: integer) -> integer {
ok2 = k;
om2 = m;
if o >= 6 { ok2 = refl_k(k); om2 = refl_m(m); }
for _ in 0..(o % 6) {
nk = ((ok2 - om2) / 2) ?? 0;
nm = ((3 * ok2 + om2) / 2) ?? 0;
ok2 = nk;
om2 = nm;
}
ok2
}
// The `m` of a step under orientation `o` — the companion of `orient_k`. Walking both is
// how a shape is compared against all 12 orientations without rebuilding it.
pub fn orient_m(k: integer, m: integer, o: integer) -> integer {
ok3 = k;
om3 = m;
if o >= 6 { ok3 = refl_k(k); om3 = refl_m(m); }
for _ in 0..(o % 6) {
nk2 = ((ok3 - om3) / 2) ?? 0;
nm2 = ((3 * ok3 + om3) / 2) ?? 0;
ok3 = nk2;
om3 = nm2;
}
om3
}
// Insertion sort on integers — the cell counts here are tens, and a sort that is obviously
// correct beats a fast one. (Scalars only: loft's #338 in-place struct swap does not apply.)
fn sort_keys(v: vector<integer>) -> vector<integer> {
out: vector<integer> = [];
for si in 0..len(v) {
x = v[si] ?? 0;
out += [x];
pos = len(out) - 1;
while pos > 0 && (out[pos - 1] ?? 0) > (out[pos] ?? 0) {
tmp = out[pos - 1] ?? 0;
out[pos - 1] = out[pos] ?? 0;
out[pos] = tmp;
pos = pos - 1;
}
}
out
}
// Lexicographic order on two sorted key vectors — shorter is smaller on a common prefix.
fn vec_less(a: vector<integer>, b: vector<integer>) -> boolean {
na = len(a);
nb2 = len(b);
lim = if na < nb2 { na } else { nb2 };
for li in 0..lim {
av = a[li] ?? 0;
bv = b[li] ?? 0;
if av < bv { return true; }
if av > bv { return false; }
}
na < nb2
}
// The cell set under orientation `o`, translated so its bounding-box corner sits at the origin,
// as a SORTED key vector. Translation normalisation makes the digest position-free; sorting makes
// it independent of the scan order the cells happened to be found in.
fn oriented_keys(cells: HexSet, o: integer) -> vector<integer> {
ks: vector<integer> = [];
ms: vector<integer> = [];
for gr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for gq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, gq, gr) {
rk = lattice_k(gq, gr);
rm = lattice_m(gq, gr);
ks += [orient_k(rk, rm, o)];
ms += [orient_m(rk, rm, o)];
}
}
}
n = len(ks);
if n == 0 { return ks; }
mink = ks[0] ?? 0;
minm = ms[0] ?? 0;
for mi in 0..n {
if (ks[mi] ?? 0) < mink { mink = ks[mi] ?? 0; }
if (ms[mi] ?? 0) < minm { minm = ms[mi] ?? 0; }
}
raw: vector<integer> = [];
for ri in 0..n { raw += [ckey((ks[ri] ?? 0) - mink, (ms[ri] ?? 0) - minm)]; }
sort_keys(raw)
}
// THE DIGEST: the lexicographically smallest of the 12 oriented, translated, sorted key vectors.
// Two cell sets have the same digest **iff** one is an orientation-image of the other — which is
// exactly "the same stencil", and makes law I visible from the census side.
// Example: @HXV-001
pub fn field_digest(cells: HexSet) -> vector<integer> {
best = oriented_keys(cells, 0);
for di in 1..12 {
cand = oriented_keys(cells, di);
if vec_less(cand, best) { best = cand; }
}
best
}
// Are two digests the same? Compares the shape rather than its spelling, which is what
// makes a digest usable as an identity.
pub fn digest_eq(a: vector<integer>, b: vector<integer>) -> boolean {
if len(a) != len(b) { return false; }
for ei in 0..len(a) { if (a[ei] ?? 0) != (b[ei] ?? 0) { return false; } }
true
}
// A digest as text, for a key, a log line or a diff. The rendering is stable, so two runs
// that found the same shape write the same string.
pub fn digest_text(d: vector<integer>) -> text {
s = "";
for ti in 0..len(d) {
if ti > 0 { s = s + "."; }
s = s + "{d[ti] ?? 0}";
}
s
}
// ── the form's own identity, and the DEGRADED key the control needs ────────
//
// A collision is two forms with DIFFERENT identities that draw the SAME field. The identity is the
// form as written; `form_key_noturn` drops the turn sequence from it, which is the control
// `DESIGN.md` §9 names — a key that cannot see corners makes distinct shapes look like one form.
pub fn form_key(f: Form) -> text {
s = "h{f.fm_h0}|";
lens = f.fm_len;
turns = f.fm_turn;
for ki in 0..len(lens) { s = s + "{lens[ki] ?? 0},"; }
s = s + "|";
for kj in 0..len(turns) { s = s + "{turns[kj] ?? 0},"; }
s
}
// Example: @HXV-002
pub fn form_key_noturn(f: Form) -> text {
s = "h{f.fm_h0}|";
lens = f.fm_len;
for kl in 0..len(lens) { s = s + "{lens[kl] ?? 0},"; }
s
}
// The DEGRADED digest for the second control: cell COUNT alone. A census keyed on a summary
// instead of the shape collides immediately — both level-1 triangle classes have 3 cells — which
// is what proves the real digest is carrying the weight.
// Example: @HXV-002
pub fn count_digest(cells: HexSet) -> integer { hexset_count(cells) }
// Are two turn sequences the SAME CLOSED CYCLE, written from a different corner?
//
// A cycle has no distinguished first corner: `[2,5,5]`, `[5,5,2]` and `[5,2,5]` are one shape
// walked from three different starting points. The enumeration cannot know that — it sweeps
// (h0, turns) — so the census MUST, or it reports a stencil as several and its collisions as law F
// failures. This is the census discovering a requirement on the canonical text (S6): fixing the
// winding (C3) is not enough, the starting CORNER has to be fixed too.
// Example: @HXV-006
pub fn turns_cyclic_eq(a: vector<integer>, b: vector<integer>) -> boolean {
n = len(a);
if len(b) != n { return false; }
if n == 0 { return true; }
for cs in 0..n {
same = true;
for ci in 0..n {
if (a[(ci + cs) % n] ?? 0) != (b[ci] ?? 0) { same = false; }
}
if same { return true; }
}
false
}
// ── THE FAITHFUL DIGEST — a different question, and it must be a different function ─
//
// `field_digest` above quotients by orientation, because the CENSUS asks "how many distinct
// SHAPES does this level hold?" and an orientation-image is the same stencil (law I).
//
// LAW F ASKS SOMETHING ELSE: is `draw` injective — do two distinct models produce two distinct
// FIELDS? That is about the cells actually written, not about the shape class. A stencil at h0=0
// and the same stencil at h0=6 draw genuinely different cells; the shape digest calls them equal,
// and law F would look violated when nothing is wrong.
//
// So the corpus `.f` uses THIS one: the field exactly as drawn, no orientation quotient and no
// translation quotient (every entry is drawn at the same anchor, so position is part of the
// answer). The census digest here would report a stencil and its turned copy as a law F
// failure when nothing is wrong.
// Example: @HXV-001
pub fn field_exact(cells: HexSet) -> vector<integer> {
raw: vector<integer> = [];
for xr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for xq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, xq, xr) { raw += [ckey(lattice_k(xq, xr), lattice_m(xq, xr))]; }
}
}
sort_keys(raw)
}
// The cell set's EXACT digest as text: this shape at this position and orientation.
// Use it when position matters — two placements of one shape differ here.
pub fn field_exact_text(cells: HexSet) -> text { digest_text(field_exact(cells)) }
// The TRANSLATION-normalised, orientation-SENSITIVE digest — orientation 0 only.
//
// This is the one `rebuild` matches on: a recovered stencil must be the same shape at the same
// heading (h0 is authored, X39), but WHERE it was drawn is placement's business, so position is
// quotiented and orientation is not. Three digests now, three questions:
//
// field_digest quotient orientation + translation "how many SHAPES?" (census, law I)
// field_exact quotient nothing "is draw INJECTIVE?" (corpus, law F)
// field_norm quotient translation only "which STENCIL is this?" (rebuild, R1)
// Example: @HXV-001
pub fn field_norm(cells: HexSet) -> vector<integer> { oriented_keys(cells, 0) }
// The cell set's NORMALISED digest as text: the same shape wherever it sits and however it
// is turned. Use it to recognise a shape you have seen before.
pub fn field_norm_text(cells: HexSet) -> text { digest_text(field_norm(cells)) }
// ── THE LEVEL LADDER — the enumeration, parameterised ──────────────────────
//
// A1 fixed every side at length 1. A2 grows `len` on the same shape, and asks the question the
// rung ladder poses: **does length alone ever collide?** The enumeration is otherwise identical —
// 3 sides, all 12 starting headings, every turn triple summing to 12, filtered by law J — so the
// levels nest: `forms_upto(1)` is a subset of `forms_upto(2)`.
//
// A form's LEVEL is its longest side, so level `n` is exactly what `forms_upto(n)` adds to
// `forms_upto(n-1)`. That keeps `corpus/a1` untouched forever (the never-regenerate rule) while
// `corpus/a2` holds only what is new.
pub fn form_maxlen(f: Form) -> integer {
lens = f.fm_len;
mx = 0;
for mi in 0..len(lens) { if (lens[mi] ?? 0) > mx { mx = lens[mi] ?? 0; } }
mx
}
// Every admitted 3-sided form with all side lengths in 1..maxlen, canonicalised and deduplicated.
// Example: @HXV-009
pub fn forms_upto(maxlen: integer) -> vector<Form> {
out: vector<Form> = [];
texts: vector<text> = [];
for la in 1..maxlen + 1 {
for lb in 1..maxlen + 1 {
for lc in 1..maxlen + 1 {
for eh in 0..12 {
for ea in 1..12 {
for eb in 1..12 {
ec = 12 - ea - eb;
if ec >= 1 && ec <= 11 {
ef = form_new(eh, [la, lb, lc], [ea, eb, ec]);
if form_admissible(ef) {
cf = form_canon(ef);
ct = form_write(cf, "x");
seen = false;
for si in 0..len(texts) { if (texts[si] ?? "") == ct { seen = true; } }
if !seen { texts += [ct]; out += [cf]; }
}
}
}
}
}
}
}
}
out
}
// Only the forms NEW at this level — longest side exactly `n`.
// Example: @HXV-009
pub fn forms_at_level(n: integer) -> vector<Form> {
all = forms_upto(n);
out: vector<Form> = [];
for fi in 0..len(all) {
ff = all[fi] ?? form_new(0, [], []);
if form_maxlen(ff) == n { out += [ff]; }
}
out
}
// Is `b` a cyclic shift of `a` REVERSED — the signature of a MIRROR pair?
//
// The flip is one of the 12 orientations (`O` = 6 rotations x {id, flip}), and mirroring a turtle
// walk reverses its turn sequence. At level 1 this never shows: every admitted form has equal
// sides, so it is achiral and its mirror is just a rotation. Unequal sides make a form and its
// mirror genuinely different texts — `turn 3,5,4` against `turn 3,4,5` — that draw mirror-image
// fields and therefore share a SHAPE digest. That is law **I**, not a law F failure.
// Example: @HXV-007
pub fn turns_reverse_eq(a: vector<integer>, b: vector<integer>) -> boolean {
n = len(a);
if len(b) != n { return false; }
if n == 0 { return true; }
rev: vector<integer> = [];
for ri in 0..n { rev += [a[n - 1 - ri] ?? 0]; }
turns_cyclic_eq(rev, b)
}
// ── THE SECOND GROWTH AXIS: SIDE COUNT (rung A3) ───────────────────────────
//
// A2 grew `len`; A3 grows the number of SIDES — 4 (a house's side count), 5, 6. The two axes are
// orthogonal and they MULTIPLY, which is the practical frontier this rung found: see §10.20.
//
// The turn tuple is enumerated with a mixed-radix odometer rather than nested loops, so the side
// count is a parameter instead of a shape baked into the code. `sides - 1` digits in 1..11, the
// last determined by `sum = 12` (law J's turn condition, imposed by construction).
fn ipow(b: integer, e: integer) -> integer {
r = 1;
for _ in 0..e { r = r * b; }
r
}
// Every form with this many sides up to this side length — the candidate set a recovery
// searches. Generated rather than stored, so a bigger search is a bigger argument.
pub fn forms_sides(sides: integer, maxlen: integer) -> vector<Form> {
out: vector<Form> = [];
texts: vector<text> = [];
ncomb = ipow(11, sides - 1);
nlen = ipow(maxlen, sides);
for ci in 0..ncomb {
turns: vector<integer> = [];
rest = ci;
tot = 0;
for _ in 0..sides - 1 {
dg = ((rest % 11) ?? 0) + 1;
rest = (rest / 11) ?? 0;
turns += [dg];
tot = tot + dg;
}
last = 12 - tot;
if last >= 1 && last <= 11 {
turns += [last];
for li in 0..nlen {
lens: vector<integer> = [];
lrest = li;
for _ in 0..sides {
lens += [((lrest % maxlen) ?? 0) + 1];
lrest = (lrest / maxlen) ?? 0;
}
for eh in 0..12 {
ef = form_new(eh, lens, turns);
if form_admissible(ef) {
cf = form_canon(ef);
ct = form_write(cf, "x");
seen = false;
for si in 0..len(texts) { if (texts[si] ?? "") == ct { seen = true; } }
if !seen { texts += [ct]; out += [cf]; }
}
}
}
}
}
out
}
// The A3 set: side counts 4..6 at unit length — the rung's own axis, held at `len 1` so it stays
// orthogonal to A2's. Growing BOTH at once is what does not fit (§10.20).
pub fn forms_a3() -> vector<Form> {
out: vector<Form> = [];
for sd in 4..7 {
fs = forms_sides(sd, 1);
for fi in 0..len(fs) { out += [fs[fi] ?? form_new(0, [], [])]; }
}
out
}
src/formfit.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// formfit — `rebuild` : 𝔽 → 𝕄*, LEVEL 1. REGIME R1, SO IT IS A MATCH AND NOT A FIT.
//
// `ROUNDTRIP` §6: for a stencil WE AUTHORED the grammar is the prior, and it constrains the answer
// to a FINITE set. Recovery is therefore an **exact integer match against that set** — `ρ = 0`, no
// tolerance anywhere. Reaching for R2's machinery here (fitting a line to a shape whose complete
// description we hold) would throw away an exact answer and reintroduce a knob nothing needs.
//
// WHAT MAKES THE MATCH LEGITIMATE is the S5 census, not an assumption: it DECIDED that level 1 is
// finite and that law F holds over it, so an exact match is unique when it exists. `rebuild` does
// not take that on trust either — it counts its matches and refuses to answer if more than one
// candidate fits.
//
// WHEN NOTHING MATCHES, IT SAYS SO. A footprint that no grammar form draws is **R2** — arbitrary
// cell-authored content, no `𝕋` behind it — and the one thing `rebuild` must never do is return a
// confident R1 answer for it. It reports the regime and a residual instead.
use hex_field::*;
use hex_form::*;
use formcensus::*;
fn fdiv(a: integer, b: integer) -> integer {
q = (a / b) ?? 0;
if a < 0 && q * b != a { return q - 1; }
q
}
// ── THE WINDOW EVERY REDRAW GOES INTO — SIZED FROM THE FORM, NOT FROM A GUESS ─
//
// Recovery draws a candidate and compares it with the field, so the redraw needs a chunk big
// enough for the whole form: `form_fill` clips to its chunk silently, and a clipped redraw would
// report an exact answer as R2. The window is therefore derived from the form, never a constant.
// `form_fill` sets exactly the cells whose lattice centre `(2q + (r&1), 3r)` lies on or inside the
// turtle polygon, and `form_poly_k`/`form_poly_m` give that polygon's corners, so their bounding
// box bounds the drawing. Every `head_step` moves `m` by a multiple of 3, so `r` is bounded
// exactly; `k` gives `q` within one, and a one-cell pad on every side absorbs that.
// Example: @HXV-003
pub fn fit_chunk(f: Form) -> HexSet {
vk = form_poly_k(f);
vm = form_poly_m(f);
n = len(vk);
if n == 0 { return hexset_chunk(0, 0, 1, 1); }
mink = vk[0] ?? 0;
maxk = mink;
minm = vm[0] ?? 0;
maxm = minm;
for bi in 0..n {
kk = vk[bi] ?? 0;
mm = vm[bi] ?? 0;
if kk < mink { mink = kk; }
if kk > maxk { maxk = kk; }
if mm < minm { minm = mm; }
if mm > maxm { maxm = mm; }
}
r0 = fdiv(minm, 3) - 1;
r1 = fdiv(maxm, 3) + 1;
q0 = fdiv(mink - 1, 2) - 1;
q1 = fdiv(maxk, 2) + 1;
hexset_chunk(q0, r0, q1 - q0 + 1, r1 - r0 + 1)
}
pub const REGIME_R1 = 1;
pub const REGIME_R2 = 2;
// What `rebuild` recovered from a drawn field: the regime (R1 exact and unique, R2 no form
// draws it), the recovered form (meaningful in R1), the unexplained-cell count and how many
// candidates matched.
pub struct Rebuilt {
pub rb_regime: integer, // REGIME_R1 (exact, unique) or REGIME_R2 (no grammar form draws this)
pub rb_form: Form, // the recovered model — meaningful only in R1
pub rb_rho: integer, // 0 in R1; the smallest unexplained-cell count in R2
pub rb_matches: integer, // how many candidates matched exactly; >1 means the level is ambiguous
}
// ── the admitted set — the candidate space `rebuild` matches against ───────
//
// `LEVEL` is how far up the ladder the enumeration reaches. A field drawn from a form above it
// finds NO candidate and `rebuild` reports it as R2 — "nothing in the admitted set draws this",
// not "no stencil draws this". `rebuild_construct` is the route for those (@HXV-004).
pub const LEVEL = 2;
// The candidate space: A1/A2 (3 sides, len <= LEVEL) plus A3 (side counts 4..6 at len 1).
//
// ENUMERATING THIS IS THE EXPENSIVE PART, so callers that recover many fields must build it ONCE
// and pass it to `rebuild_with`. `rebuild` re-enumerates on every call, which is fine for a single
// lookup and quadratic waste over a corpus.
// Example: @HXV-004
pub fn candidate_forms() -> vector<Form> {
out = forms_upto(LEVEL);
a3 = forms_a3();
for ai in 0..len(a3) { out += [a3[ai] ?? form_new(0, [], [])]; }
out
}
// Retained for the A1 gates: the level-1 enumeration on its own.
pub fn level1_forms() -> vector<Form> {
out: vector<Form> = [];
texts: vector<text> = [];
for eh in 0..12 {
for ea in 1..12 {
for eb in 1..12 {
ec = 12 - ea - eb;
if ec >= 1 && ec <= 11 {
ef = form_new(eh, [1, 1, 1], [ea, eb, ec]);
if form_closes(ef) {
cf = form_canon(ef);
ct = form_write(cf, "a1");
seen = false;
for si in 0..len(texts) { if (texts[si] ?? "") == ct { seen = true; } }
if !seen { texts += [ct]; out += [cf]; }
}
}
}
}
}
out
}
// How many keys are in exactly one of the two sorted lists — the symmetric difference. This is
// the R2 residual: cells the best grammar form cannot account for. It is a COUNT, not a distance,
// so no tolerance is implied by it.
fn sym_diff(a: vector<integer>, b: vector<integer>) -> integer {
n = 0;
for ai in 0..len(a) {
found = false;
for aj in 0..len(b) { if (a[ai] ?? 0) == (b[aj] ?? 0) { found = true; } }
if !found { n = n + 1; }
}
for bi in 0..len(b) {
found2 = false;
for bj in 0..len(a) { if (b[bi] ?? 0) == (a[bj] ?? 0) { found2 = true; } }
if !found2 { n = n + 1; }
}
n
}
// THE ROUTINE. Match the field against every admitted form, translation-normalised — WHERE it was
// drawn is placement's business (the `place` element), not the stencil's.
// Example: @HXV-005
pub fn rebuild(cells: HexSet) -> Rebuilt { rebuild_with(candidate_forms(), cells) }
// The core, with the candidate set supplied — build it once per gate run, not once per lookup.
pub fn rebuild_with(cand: vector<Form>, cells: HexSet) -> Rebuilt {
want = field_norm(cells);
nmatch = 0;
best = form_new(0, [], []);
rho = -1;
for ci in 0..len(cand) {
cf = cand[ci] ?? form_new(0, [], []);
cc = fit_chunk(cf);
form_fill(cf, 0, 0, cc);
got = field_norm(cc);
d = sym_diff(want, got);
if d == 0 {
nmatch = nmatch + 1;
best = cf;
}
if rho < 0 || d < rho { rho = d; }
}
if nmatch == 1 {
return Rebuilt { rb_regime: REGIME_R1, rb_form: best, rb_rho: 0, rb_matches: 1 };
}
// No unique grammar form draws this field: it is R2 content, and the residual is reported
// rather than swallowed. `rb_matches > 1` would mean law F failed at this level — the census
// says it does not, so the gate treats that as a hard error rather than an answer.
Rebuilt { rb_regime: REGIME_R2, rb_form: best, rb_rho: rho, rb_matches: nmatch }
}
// The round trip in one call: field -> model -> canonical text. Empty when recovery is not R1,
// so a caller cannot accidentally spell an R2 guess as if it were an authored stencil.
pub fn rebuild_text_with(cand: vector<Form>, cells: HexSet, name: text) -> text {
r = rebuild_with(cand, cells);
if r.rb_regime != REGIME_R1 { return ""; }
form_write(r.rb_form, name)
}
// Recover a form from cells and write it as canonical text, or "" when the cells are not a
// single clean form. The empty answer is the refusal — check it before using the text.
pub fn rebuild_text(cells: HexSet, name: text) -> text {
r = rebuild(cells);
if r.rb_regime != REGIME_R1 { return ""; }
form_write(r.rb_form, name)
}
// ── INDEXED RECOVERY — draw the candidates ONCE, then look up ──────────────
//
// `rebuild_with` re-DRAWS every candidate on every lookup: O(N) fills per field, O(N^2) over a
// corpus. A candidate's field never changes, so its digest can be computed once and put in a map
// keyed by the digest; recovery then becomes a single probe.
//
// WHAT THIS DOES AND DOES NOT BUY:
//
// IT FIXES the per-lookup cost — O(N) fills to build the index, O(1) per recovery afterwards.
// IT DOES NOT FIX the cost of ENUMERATING the candidate space. Building the index still walks
// every admissible form once, so a form class too large to enumerate (the
// 4-sided house, ~1.2M proposals) stays out of reach. Reaching it needs
// CONSTRUCTIVE recovery — `rebuild_construct`, below.
//
// LAW F IS CHECKED AT BUILD TIME, ONCE, OVER THE WHOLE SPACE. Two candidates sharing a digest
// would silently overwrite each other in the map, so the build COUNTS such clashes in
// `ri_collisions`, and zero is the answer law F promises (@HXV-002).
pub struct IdxEntry {
pub ix_digest: text, // `field_norm_text` of the form's drawn field — NOT named `key`
pub ix_form: Form,
}
// The admitted forms keyed by the digest of the field each draws, so `rebuild_indexed` finds
// a match by lookup. `index_build` builds it and records what that cost.
pub struct RecoveryIndex {
pub ri_map: hash<IdxEntry[ix_digest]>,
pub ri_count: integer, // entries indexed
pub ri_collisions: integer, // candidates whose digest was already taken — a law F failure
pub ri_fills: integer, // draws performed to build it (the cost, reported not guessed)
}
// Example: @HXV-002
pub fn index_build(cand: vector<Form>) -> RecoveryIndex {
idx = RecoveryIndex { ri_map: [], ri_count: 0, ri_collisions: 0, ri_fills: 0 };
for ii in 0..len(cand) {
cf = cand[ii] ?? form_new(0, [], []);
cc = fit_chunk(cf);
form_fill(cf, 0, 0, cc);
idx.ri_fills = idx.ri_fills + 1;
dg = field_norm_text(cc);
prior = idx.ri_map[dg];
if prior == null {
idx.ri_map += [IdxEntry { ix_digest: dg, ix_form: cf }];
idx.ri_count = idx.ri_count + 1;
} else {
idx.ri_collisions = idx.ri_collisions + 1;
}
}
idx
}
// THE FAST PATH: one probe. A hit is an exact R1 answer — the digest IS the field, compared in
// full, so this is the same exactness the scan gave, not a heuristic narrowing.
//
// A MISS falls back to the scan, and only then, purely to compute the R2 residual. R2 is the
// exceptional path (content no grammar form draws), so paying O(N) there costs nothing in
// practice and keeps `rho > 0` a real measurement rather than a placeholder.
// Example: @HXV-008
pub fn rebuild_indexed(idx: RecoveryIndex, cand: vector<Form>, cells: HexSet) -> Rebuilt {
hit = idx.ri_map[field_norm_text(cells)];
if hit != null {
return Rebuilt { rb_regime: REGIME_R1, rb_form: hit.ix_form, rb_rho: 0, rb_matches: 1 };
}
rebuild_with(cand, cells)
}
// The same, searching a prepared index and candidate set instead of enumerating: the shape
// to use when many cells sets are recovered against one library.
pub fn rebuild_indexed_text(idx: RecoveryIndex, cand: vector<Form>,
cells: HexSet, name: text) -> text {
r = rebuild_indexed(idx, cand, cells);
if r.rb_regime != REGIME_R1 { return ""; }
form_write(r.rb_form, name)
}
// ── CONSTRUCTIVE RECOVERY — read the form off the field, no enumeration ────
//
// Indexed recovery is BUILT BY the enumeration, so it reaches no further than `rebuild`. This
// does not enumerate at all: it reads `(h0, lens, turns)` off the field's own geometry, in
// O(cells), independent of how large the admissible space is.
//
// THE CONSTRUCTION, and why it is exact rather than a fit:
//
// 1. every admitted form is CONVEX. Law J requires the turns to be positive and to sum to 12 —
// all exterior angles positive, exactly one revolution — which is the definition of a convex
// polygon. (A4's non-convex forms need a negative turn, so they fall outside this and want
// boundary TRACING instead; stated as a limit, not discovered later.)
// 2. the turtle walks CENTRE TO CENTRE, so every polygon vertex IS a hex centre (S3), and
// `form_fill` takes every hex on or inside. For a convex polygon whose vertices are lattice
// points, the CONVEX HULL of the contained lattice points is the polygon itself — its corners
// are extreme points and cannot be hulled away.
// 3. so: hull the filled cells in exact integer (k,m); the hull vertices ARE the turtle's
// corners, in order. Collinear cells along a long side are interior to an edge and drop out,
// which is exactly right — they are not corners.
// 4. each side vector is an exact integer multiple of one of the 12 heading steps, found by
// trying all 12. No gcd reduction: `head_step(0)` is `(2,0)`, not `(1,0)`, so a primitive
// reduction would land off the table.
//
// THE HULL IS TAKEN IN (k,m), NOT IN WORLD COORDINATES, and that is safe: `(k,m) -> (k*sqrt3/2,
// m/2)` is linear with determinant `sqrt3/4 > 0`, so it preserves convexity AND orientation. The
// integer arithmetic is therefore exact — no floats enter the recovery at any point.
//
// AND IT STILL VERIFIES. The construction PROPOSES a form; one re-draw CONFIRMS it. That keeps
// `ρ = 0` a measured fact rather than a claim, and sends anything that does not reproduce its own
// field to R2 — where a hand-drawn blob belongs.
const PK_OFF = 400;
const PK_SPAN = 1600;
fn pk(k: integer, m: integer) -> integer { (k + PK_OFF) * PK_SPAN + (m + PK_OFF) }
fn pk_k(v: integer) -> integer { ((v / PK_SPAN) ?? 0) - PK_OFF }
fn pk_m(v: integer) -> integer { ((v % PK_SPAN) ?? 0) - PK_OFF }
fn sort_pk(v: vector<integer>) -> vector<integer> {
out: vector<integer> = [];
for si in 0..len(v) {
out += [v[si] ?? 0];
ps = len(out) - 1;
while ps > 0 && (out[ps - 1] ?? 0) > (out[ps] ?? 0) {
tmp = out[ps - 1] ?? 0;
out[ps - 1] = out[ps] ?? 0;
out[ps] = tmp;
ps = ps - 1;
}
}
out
}
// Twice the signed area of the triangle o->a->b. Positive when b is left of o->a (CCW).
fn cross3(ok2: integer, om: integer, ak: integer, am: integer,
bk: integer, bm: integer) -> integer {
(ak - ok2) * (bm - om) - (am - om) * (bk - ok2)
}
// The convex hull of the filled cells, CCW, corners only — monotone chain in exact integers.
// Collinear points are dropped (`<= 0` pops them), which is what makes long sides come back as one
// side instead of a run of tiny ones.
pub fn hull_keys(cells: HexSet) -> vector<integer> {
pts: vector<integer> = [];
for hr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for hq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, hq, hr) { pts += [pk(lattice_k(hq, hr), lattice_m(hq, hr))]; }
}
}
sp = sort_pk(pts);
n = len(sp);
out: vector<integer> = [];
if n < 3 { return sp; }
// lower hull, then upper hull
st: vector<integer> = [];
top = 0;
for li in 0..n {
p = sp[li] ?? 0;
while top >= 2
&& cross3(pk_k(st[top - 2] ?? 0), pk_m(st[top - 2] ?? 0),
pk_k(st[top - 1] ?? 0), pk_m(st[top - 1] ?? 0),
pk_k(p), pk_m(p)) <= 0 { top = top - 1; }
if top < len(st) { st[top] = p; } else { st += [p]; }
top = top + 1;
}
lower = top - 1;
for ui in 0..n {
p2 = sp[n - 1 - ui] ?? 0;
while top >= lower + 2
&& cross3(pk_k(st[top - 2] ?? 0), pk_m(st[top - 2] ?? 0),
pk_k(st[top - 1] ?? 0), pk_m(st[top - 1] ?? 0),
pk_k(p2), pk_m(p2)) <= 0 { top = top - 1; }
if top < len(st) { st[top] = p2; } else { st += [p2]; }
top = top + 1;
}
for oi in 0..top - 1 { out += [st[oi] ?? 0]; }
out
}
// Which heading, and how many steps, is the vector (dk, dm)? Tries all 12 — a constant — because
// the heading table's steps are not primitive vectors and a gcd reduction would miss them.
// Returns the heading, or -1 when the vector is not a whole number of steps in any heading.
pub fn side_heading(dk: integer, dm: integer) -> integer {
for sh in 0..HEAD_N {
st = head_step(sh);
if st.0 == 0 {
if dk == 0 && st.1 != 0 && dm % st.1 == 0 && ((dm / st.1) ?? 0) > 0 { return sh; }
} else {
if dk % st.0 == 0 {
q = (dk / st.0) ?? 0;
if q > 0 && q * st.1 == dm { return sh; }
}
}
}
-1
}
// How many steps of heading `h` the offset (dk, dm) covers. The measurement a rebuild
// makes at every corner to turn geometry back into a side length.
pub fn side_len(dk: integer, dm: integer, h: integer) -> integer {
st = head_step(h);
if st.0 != 0 { return (dk / st.0) ?? 0; }
if st.1 != 0 { return (dm / st.1) ?? 0; }
0
}
// THE ROUTINE: recover the form from the field, then verify by re-drawing it.
// Example: @HXV-003
// Example: @HXV-004
pub fn rebuild_construct(cells: HexSet) -> Rebuilt {
hk = hull_keys(cells);
nh = len(hk);
if nh < 3 {
return Rebuilt { rb_regime: REGIME_R2, rb_form: form_new(0, [], []),
rb_rho: hexset_count(cells), rb_matches: 0 };
}
lens: vector<integer> = [];
hs: vector<integer> = [];
bad = false;
for ci in 0..nh {
ak = pk_k(hk[ci] ?? 0);
am = pk_m(hk[ci] ?? 0);
// `% nh` types nullable (a zero divisor answers null); `nh >= 3` is checked above,
// so `?? 0` names an unreachable index rather than a chosen one.
bk = pk_k(hk[(ci + 1) % nh ?? 0] ?? 0);
bm = pk_m(hk[(ci + 1) % nh ?? 0] ?? 0);
sh = side_heading(bk - ak, bm - am);
if sh < 0 { bad = true; }
else {
hs += [sh];
lens += [side_len(bk - ak, bm - am, sh)];
}
}
if bad || len(hs) != nh {
return Rebuilt { rb_regime: REGIME_R2, rb_form: form_new(0, [], []),
rb_rho: hexset_count(cells), rb_matches: 0 };
}
turns: vector<integer> = [];
for ti in 0..nh {
d = head_norm((hs[(ti + 1) % nh ?? 0] ?? 0) - (hs[ti] ?? 0));
if d == 0 { d = 12; }
turns += [d];
}
cf = form_canon(form_new(hs[0] ?? 0, lens, turns));
if !form_closes(cf) {
return Rebuilt { rb_regime: REGIME_R2, rb_form: form_new(0, [], []),
rb_rho: hexset_count(cells), rb_matches: 0 };
}
// VERIFY by re-drawing. The construction proposes; this confirms, so `rho = 0` stays measured.
chk = fit_chunk(cf);
form_fill(cf, 0, 0, chk);
d2 = sym_diff(field_norm(cells), field_norm(chk));
if d2 == 0 {
return Rebuilt { rb_regime: REGIME_R1, rb_form: cf, rb_rho: 0, rb_matches: 1 };
}
Rebuilt { rb_regime: REGIME_R2, rb_form: cf, rb_rho: d2, rb_matches: 0 }
}
// Recover by CONSTRUCTION rather than by search, as text. Answers "" on refusal like its
// siblings, so all three recovery routes are read the same way.
pub fn rebuild_construct_text(cells: HexSet, name: text) -> text {
r = rebuild_construct(cells);
if r.rb_regime != REGIME_R1 { return ""; }
form_write(r.rb_form, name)
}
src/hex_recover.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_recover — rebuild a model from the field — exactly, or with a reported residual.
//
// The `rebuild` map. Constructive recovery reads the form off the field, enumerating nothing:
// every admitted form is convex, so the convex hull of the filled cells IS the turtle polygon.
// The census that decided injectivity is here too, with the three digests that answer three
// different questions and must never be confused.
//
// Entry point: re-exports `formfit`, `formcensus`.
pub use formfit::*;
pub use formcensus::*;
tests/01-hex-recover.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_recover — the conformance tests: rebuild a model from the field, exactly or with a residual.
//
// Contract only; the exhaustive measurement is hexbody's `tests/census.loft` and `tests/trip.loft`.
use hex_recover::*;
use hex_form::*;
use hex_field::*;
// REGIME_R1 / REGIME_R2 are exported by the library.
fn main() {
assert(len(candidate_forms()) > 0, "there is a candidate set to match against");
}
fn test_a_grammar_form_recovers_r1_with_zero_residual() {
f = form_new(0, [1, 1, 1], [4, 4, 4]);
cells = hexset_chunk(-20, -20, 41, 41);
form_fill(f, 0, 0, cells);
r = rebuild(cells);
assert(r.rb_regime == REGIME_R1, "a grammar form recovers in R1, got regime {r.rb_regime}");
assert(r.rb_rho == 0, "exactly, with rho 0, got {r.rb_rho}");
assert(r.rb_matches == 1, "and uniquely -- one match, got {r.rb_matches}");
}
fn test_the_recovered_form_re_draws_the_same_field() {
// The round trip an editor's undo/redo depends on: recover, redraw, compare.
// A form in the default candidate set (enumeration does not reach unequal-sided houses --
// that is `X43`, and constructive recovery is the route for those; see the README).
f = form_new(0, [1, 1, 1], [4, 4, 4]);
cells = hexset_chunk(-20, -20, 41, 41);
form_fill(f, 0, 0, cells);
t = rebuild_text(cells, "back");
assert(t.len() > 0, "R1 recovery yields a canonical text");
}
fn test_a_non_grammar_blob_lands_in_r2_not_a_false_r1() {
// The door that must stay shut: an arbitrary cell blob is NOT silently reported as a stencil.
cells = hexset_chunk(-20, -20, 41, 41);
hexset_set(cells, 0, 0, true);
hexset_set(cells, 1, 0, true);
hexset_set(cells, 5, 5, true); // a disconnected, non-grammar shape
r = rebuild(cells);
assert(r.rb_regime == REGIME_R2, "a non-grammar blob is R2, got {r.rb_regime}");
assert(r.rb_rho > 0, "with a positive residual, got {r.rb_rho}");
}
tests/02-worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `hex_recover` (@PLN141). Each `test_*` below is a REAL,
// run-every-CI demonstration of a contract the signature cannot state; the
// function it teaches CITES it with `// Example: @HXV-0NN`, and
// `check_doc_drift.sh examples` fails if a citation goes dangling. These are
// teaching examples, distinct from the conformance suite in `01-hex-recover.loft`.
//
// WHAT THIS PACKAGE ASKS OF A CALLER. Recovery is EXACT — integers throughout,
// no epsilon anywhere, and none is wanted (`SPEC` **P4**). What a caller gets
// wrong is never the arithmetic; it is one of three things:
//
// WHICH DIGEST three of them answer three different questions about the same
// field, and two of the three are `vector<integer>` with the same
// spelling. Reaching for the wrong one reported 17 law F failures
// on a corpus where nothing was wrong
// WHICH ROUTINE `rebuild` matches an ENUMERATED set and is bounded by `LEVEL`;
// `rebuild_construct` reads the form off the field and is not.
// A shape can be R2 to one and R1 to the other, and both answers
// are right
// WHAT `rho` IS a COUNT of cells, not a distance — so it does not shrink towards
// a match, and a residual of 1 is a refusal rather than a near miss
//
// The fixtures are written BY HAND and are as small as the claim allows: a
// three-cell triangle, a four-cell wedge, a ring of six. Where a claim needs a
// LARGE shape it is still hand-written — `form_new(h, [n,n,n], [4,4,4])` is three
// numbers, and its cell count is `(n+1)(n+2)/2`, checked in the test.
use hex_recover::*;
use hex_form::*;
use hex_field::*;
// A chunk big enough for anything this file draws, for the times a test wants to
// see a redraw WITHOUT the library's own window logic in the way.
fn wide() -> HexSet { hexset_chunk(-40, -40, 81, 81) }
// The equilateral triangle of side `n`, starting at heading `h`. Three numbers,
// and `(n+1)(n+2)/2` cells.
fn tri(h: integer, n: integer) -> Form { form_new(h, [n, n, n], [4, 4, 4]) }
fn drawn(f: Form) -> HexSet {
c = wide();
form_fill(f, 0, 0, c);
c
}
// How many of `a`'s keys are absent from `b`. Both come from `field_norm`, so this
// is asked after translation is quotiented away.
fn missing_from(a: vector<integer>, b: vector<integer>) -> integer {
n = 0;
for i in 0..len(a) {
found = false;
for j in 0..len(b) { if (a[i] ?? 0) == (b[j] ?? 0) { found = true; } }
if !found { n = n + 1; }
}
n
}
fn main() {
assert(len(candidate_forms()) > 0, "there is a candidate set to match against");
}
// @HXV-001 — THREE DIGESTS, ONE FIELD, AND THE NUMBER THAT TELLS THEM APART.
//
// `field_exact`, `field_norm` and `field_digest` all take a `HexSet` and all return
// a `vector<integer>`. Nothing in a call site says which question you asked, so
// this example asks all three of one field and shows where they disagree:
//
// field_exact quotients NOTHING "is `draw` injective?" (law F)
// field_norm quotients translation "which stencil is this?" (rebuild)
// field_digest translation AND orientation "how many SHAPES?" (law I)
//
// AND THE DIGEST IS THE CELL LIST, NOT A HASH. A hash collision in a census would
// look exactly like a law F violation, so there is no modular arithmetic anywhere:
// a cell at lattice `(k,m)` packs to `(k+400)*1600 + (m+400)` and the digest is
// those numbers, sorted. The unit triangle's three cells sit at `(0,0)`, `(2,0)`
// and `(1,3)`, so its digest is `640400.642003.643600` — written out here because
// a number you can derive by hand is the proof that nothing was hashed.
fn test_three_digests_of_one_field_and_where_they_disagree() {
t = tri(0, 1);
a = drawn(t);
assert(hexset_count(a) == 3, "the unit triangle is three cells, got {hexset_count(a)}");
assert(field_exact_text(a) == "640400.642003.643600",
"the digest IS the sorted cell list, got {field_exact_text(a)}");
// the SAME stencil, dropped somewhere else
moved = wide();
form_fill(t, 3, 2, moved);
assert(!digest_eq(field_exact(a), field_exact(moved)),
"field_exact quotients nothing, so moving the stencil changes it");
assert(digest_eq(field_norm(a), field_norm(moved)),
"field_norm quotients translation, so moving the stencil does not");
assert(digest_eq(field_digest(a), field_digest(moved)),
"and neither does the shape digest");
// the SAME stencil, turned 60 degrees — one of the twelve orientations
turned = drawn(tri(2, 1));
assert(!digest_eq(field_norm(a), field_norm(turned)),
"field_norm is orientation-SENSITIVE: a turned stencil is a different field");
assert(digest_eq(field_digest(a), field_digest(turned)),
"the shape digest quotients orientation too -- law I, one stencil");
// the summary that cannot tell any of them apart
assert(count_digest(a) == 3 && count_digest(turned) == 3,
"a cell COUNT is the same for both, which is why it is only a control");
}
// @HXV-002 — LAW F IS DECIDED OVER THE SPACE THAT SHIPS, AND IT COSTS MILLISECONDS.
//
// Law F is injectivity — `draw(m1) = draw(m2) => m1 = m2` — and its COVERAGE is a
// measurement, not a theorem. The measurement people picture is the open-ended one:
// push the level ladder up and see where injectivity stops. That is a long-running
// exploration and it is not what a caller needs. The question a caller has is
// narrower and finite: **is `draw` injective over the set THIS BUILD matches
// against?** Building the index answers it for the whole space at once — two
// candidates sharing a digest would overwrite each other in the map, so the build
// COUNTS the clashes — and the whole census is the 119 forms below.
//
// That is strictly stronger than the per-lookup `rb_matches` count, which only ever
// checks the corner of the space some field happened to land in.
//
// The numbers move when `LEVEL` moves, and they SHOULD: this is a measurement of the
// shipped candidate set, so a level raised without re-reading it is exactly the
// silent change the test exists to catch.
fn test_law_f_is_decided_over_the_whole_shipped_candidate_set() {
cand = candidate_forms();
assert(len(cand) == 119,
"LEVEL 2 (3 sides) plus A3 (4-6 sides at len 1) admits 119 forms, got {len(cand)}");
idx = index_build(cand);
assert(idx.ri_count == 119, "every one of them is indexed, got {idx.ri_count}");
assert(idx.ri_collisions == 0,
"and no two draw the same field -- law F holds here, got {idx.ri_collisions} clashes");
assert(idx.ri_fills == len(cand),
"the cost is reported, not guessed: one draw per candidate, got {idx.ri_fills}");
// CONTROL — the digest has to be doing work. Key the same 119 fields on the cell
// COUNT instead of the shape and the space collapses to 9 classes. If a summary
// were as discriminating as the field, "no collisions" above would be free.
counts: vector<integer> = [];
for ci in 0..len(cand) {
cf = cand[ci] ?? form_new(0, [], []);
cc = fit_chunk(cf);
form_fill(cf, 0, 0, cc);
counts += [count_digest(cc)];
}
classes = 0;
for i in 0..len(counts) {
fresh = true;
for j in 0..i { if (counts[j] ?? -1) == (counts[i] ?? -2) { fresh = false; } }
if fresh { classes = classes + 1; }
}
assert(classes == 9,
"CONTROL: keyed on cell count the 119 fields collapse to 9, got {classes}");
assert(classes < idx.ri_count,
"CONTROL did not fire -- a cell-count key is as discriminating as the field");
// CONTROL B — the IDENTITY has to see the corners. `form_key_noturn` drops the turn
// sequence, and at level 2 that merges four forms that differ only in where they bend.
// A census keyed on it would report those four as one and their four distinct fields
// as law F failures.
lvl2 = forms_upto(2);
keys: vector<text> = [];
blind: vector<text> = [];
for ki in 0..len(lvl2) {
kf = lvl2[ki] ?? form_new(0, [], []);
keys += [form_key(kf)];
blind += [form_key_noturn(kf)];
}
nfull = 0;
for i2 in 0..len(keys) {
f1 = true;
for j2 in 0..i2 { if (keys[j2] ?? "") == (keys[i2] ?? "") { f1 = false; } }
if f1 { nfull = nfull + 1; }
}
nblind = 0;
for i3 in 0..len(blind) {
f2 = true;
for j3 in 0..i3 { if (blind[j3] ?? "") == (blind[i3] ?? "") { f2 = false; } }
if f2 { nblind = nblind + 1; }
}
assert(nfull == 32, "level 2 holds 32 distinct identities, got {nfull}");
assert(nblind == 28,
"CONTROL: turn-blind they collapse to 28 -- four forms lost, got {nblind}");
}
// @HXV-003 — `rho` MEASURES THE FIELD, NOT THE WINDOW.
//
// `rebuild_construct` reads `(h0, lens, turns)` off the field's own convex hull and
// then VERIFIES by re-drawing: the construction proposes, the redraw confirms, and
// that is what keeps `rho = 0` a measured fact rather than a claim. The redraw
// needs a chunk to draw into, and the chunk used to be a constant 25x25 around the
// anchor — a window sized for the level-1 enumeration that the constructive path
// exists to replace. `form_fill` clips to its chunk silently, so:
//
// heading 0, side 12 91 cells R1, rho 0
// heading 0, side 13 105 cells R2, rho 2 <- correct answer, thrown away
// heading 2, side 13 105 cells R2, rho 196 <- more unexplained cells than cells
// heading 3, side 7 85 cells R2, rho 3 <- SMALLER shape, refused sooner
//
// Every one of those had the right form in `rb_form` already. The step that makes
// the answer trustworthy was the step that discarded it, and `rho` — documented as
// "the smallest unexplained-cell count" — was reporting the distance from the anchor
// to the edge of a constant. The 85-cell refusal beside the 105-cell pass is what
// says the limit was never a size: an odd (vertex-direction) heading covers two rows
// per unit of side and an even one covers one, so the same stencil turned reached the
// edge at half the length. A stencil and its orientation-image are ONE stencil
// (law I), so a recovery that depends on which way it faces is not a recovery.
//
// The window is now derived from the form itself (`fit_chunk`), and the invariant
// this pins is the one clipping broke: on the verified path the redraw CONTAINS the
// field, so `rho` counts only what the hull ADDS and never what the chunk dropped.
fn test_rho_measures_the_field_and_not_the_window() {
// the four cells of the table above
a = drawn(tri(0, 12));
assert(hexset_count(a) == 91, "side 12 is 13*14/2 cells, got {hexset_count(a)}");
ra = rebuild_construct(a);
assert(ra.rb_regime == REGIME_R1 && ra.rb_rho == 0,
"side 12 recovers exactly, got regime {ra.rb_regime} rho {ra.rb_rho}");
b = drawn(tri(0, 13));
assert(hexset_count(b) == 105, "side 13 is 14*15/2 cells, got {hexset_count(b)}");
rb = rebuild_construct(b);
assert(rb.rb_regime == REGIME_R1 && rb.rb_rho == 0,
"and so does side 13 -- one cell more is not a regime change, got regime"
+ " {rb.rb_regime} rho {rb.rb_rho}");
c = drawn(tri(2, 13));
assert(hexset_count(c) == 105, "the same shape turned 60 degrees, got {hexset_count(c)}");
rc = rebuild_construct(c);
assert(rc.rb_regime == REGIME_R1 && rc.rb_rho == 0,
"turning a stencil cannot change whether it recovers, got regime {rc.rb_regime}"
+ " rho {rc.rb_rho}");
d = drawn(tri(3, 7));
assert(hexset_count(d) == 85, "a vertex-heading triangle of side 7 is 85 cells,"
+ " got {hexset_count(d)}");
rd = rebuild_construct(d);
assert(rd.rb_regime == REGIME_R1 && rd.rb_rho == 0,
"85 cells is SMALLER than the 105 that passed -- size was never the limit,"
+ " got regime {rd.rb_regime} rho {rd.rb_rho}");
// ALL TWELVE HEADINGS, one side length. This is the property, not the four cells.
for h in 0..12 {
f = tri(h, 13);
r = rebuild_construct(drawn(f));
assert(r.rb_regime == REGIME_R1,
"heading {h} must recover like every other -- got regime {r.rb_regime}");
assert(form_write(r.rb_form, "x") == form_write(form_canon(f), "x"),
"and recover the same form it was drawn from, at heading {h}");
}
// THE INVARIANT. On the verified path the redraw is a SUPERSET of the field: no
// cell of the field is ever missing from it, so `rho` counts additions only.
for h2 in 0..12 {
f2 = tri(h2, 13);
cells = drawn(f2);
r2 = rebuild_construct(cells);
redraw = fit_chunk(r2.rb_form);
form_fill(r2.rb_form, 0, 0, redraw);
assert(missing_from(field_norm(cells), field_norm(redraw)) == 0,
"the redraw must never DROP a cell of the field, at heading {h2}");
}
// CONTROL — the check above can say no. The old constant window, written out here,
// makes the redraw drop cells of exactly these fields. Without this the containment
// assertion would pass on any window large enough for the fixtures it happened to use.
dropped = 0;
for h3 in 0..12 {
f3 = tri(h3, 13);
cells3 = drawn(f3);
r3 = rebuild_construct(cells3);
old = hexset_chunk(-12, -12, 25, 25); // the window this row removed
form_fill(r3.rb_form, 0, 0, old);
if missing_from(field_norm(cells3), field_norm(old)) > 0 { dropped = dropped + 1; }
}
assert(dropped == 12,
"CONTROL: a fixed 25x25 window drops cells at every heading, got {dropped} of 12");
}
// @HXV-004 — THE CONSTRUCTIVE PATH REACHES WHAT THE ENUMERATION CANNOT, AND BOTH
// ANSWERS ARE RIGHT.
//
// `rebuild` matches against `candidate_forms()`, which is enumerated: three sides up
// to `LEVEL`, plus four to six sides at length 1. An unequal-sided house is in
// neither, and there is no level you can raise to reach it cheaply — the 4-sided
// class is around 1.2M proposals (`X43`). `rebuild_construct` does not enumerate at
// all: it hulls the filled cells, reads each side's heading and length off the hull,
// and re-draws once to confirm.
//
// SO A FIELD CAN BE R2 TO ONE ROUTINE AND R1 TO THE OTHER, and neither is wrong.
// `rebuild`'s R2 means "nothing in the admitted set draws this"; it does NOT mean
// "no stencil draws this". Read it as the second and you will refuse content the
// library can recover exactly.
fn test_the_constructive_path_reaches_past_the_enumerated_level() {
// a 3-by-2 rectangle: four sides, two lengths, every turn a quarter of the circle
rect = form_new(0, [3, 2, 3, 2], [3, 3, 3, 3]);
assert(form_admissible(rect), "the rectangle is an admitted form");
cells = drawn(rect);
assert(hexset_count(cells) == 18, "3 by 2 is 18 cells, got {hexset_count(cells)}");
scan = rebuild(cells);
assert(scan.rb_regime == REGIME_R2,
"the enumeration does not hold it, got regime {scan.rb_regime}");
assert(scan.rb_matches == 0, "nothing matched, got {scan.rb_matches}");
assert(scan.rb_rho > 0, "so a residual is reported, got {scan.rb_rho}");
assert(rebuild_text(cells, "roof") == "",
"and no text is offered -- an R2 guess must not be spelled as a stencil");
built = rebuild_construct(cells);
assert(built.rb_regime == REGIME_R1,
"the construction reaches it, got regime {built.rb_regime}");
assert(built.rb_rho == 0, "exactly, got rho {built.rb_rho}");
assert(rebuild_construct_text(cells, "roof") == form_write(form_canon(rect), "roof"),
"and its text is the canonical spelling of the form it was drawn from");
}
// @HXV-005 — R2 IS A REFUSAL WITH A NUMBER, NEVER A FALSE R1 — AND `rho` IS NOT A
// DISTANCE.
//
// The one thing recovery must never do is return a confident R1 for content no
// grammar form draws. It does not, and the way it says no carries information — but
// `rho` is a COUNT of cells, so reading it as "how close was it" is the trap:
//
// a ring of six with a hole rho 1 the hull adds exactly the missing centre
// three scattered cells rho 3 no form was built at all; everything is unexplained
//
// A residual of ONE is the smallest positive value there is, and it is not a near
// miss: no grammar form has a hole, so the donut is as firmly outside the grammar as
// the scatter. Small `rho` means the hull is tight, nothing more.
fn test_r2_is_a_refusal_with_a_number_and_rho_is_not_a_distance() {
// a ring of six around an empty centre — the smallest field with a HOLE
donut = hexset_chunk(-3, -3, 7, 7);
hexdisk_into(donut, 0, 0, 1);
hexset_set(donut, 0, 0, false);
assert(hexset_count(donut) == 6, "six around an empty middle, got {hexset_count(donut)}");
rd = rebuild_construct(donut);
assert(rd.rb_regime == REGIME_R2, "a hole is not a grammar form, got {rd.rb_regime}");
assert(rd.rb_rho == 1,
"and the hull adds exactly the one cell that is missing, got {rd.rb_rho}");
// three cells, one of them far away
blob = hexset_chunk(-20, -20, 41, 41);
hexset_set(blob, 0, 0, true);
hexset_set(blob, 1, 0, true);
hexset_set(blob, 5, 5, true);
rb = rebuild_construct(blob);
assert(rb.rb_regime == REGIME_R2, "an arbitrary blob is R2, got {rb.rb_regime}");
assert(rb.rb_rho == 3,
"no form was built, so the whole field is unexplained, got {rb.rb_rho}");
// CONTROL — the door is not simply always shut
ok = rebuild_construct(drawn(tri(0, 3)));
assert(ok.rb_regime == REGIME_R1 && ok.rb_rho == 0,
"CONTROL: a real form still comes back R1, got regime {ok.rb_regime}");
}
// @HXV-006 — A CLOSED CYCLE HAS NO FIRST CORNER.
//
// `[2,5,5]`, `[5,5,2]` and `[5,2,5]` are one shape walked from three different
// starting points. The enumeration cannot know that — it sweeps `(h0, turns)` and
// hands back all three — so the census MUST, or it reports one stencil as three and
// its own re-spellings as law F failures.
//
// This is the census discovering a requirement on the canonical text (`S6`): fixing
// the WINDING (`C3`) is not enough, the starting CORNER has to be fixed too.
fn test_a_closed_cycle_has_no_first_corner() {
assert(turns_cyclic_eq([2, 5, 5], [5, 5, 2]),
"the same cycle read from the next corner");
assert(turns_cyclic_eq([2, 5, 5], [5, 2, 5]),
"and from the one after that");
assert(turns_cyclic_eq([2, 5, 5], [2, 5, 5]),
"a cycle is a rotation of itself");
// CONTROL — it has to be able to say no, or "every collision is explained" is free
assert(!turns_cyclic_eq([2, 5, 5], [2, 5, 4]),
"CONTROL: one turn different is a different cycle");
assert(!turns_cyclic_eq([2, 5, 5], [2, 5]),
"CONTROL: a different number of sides is a different cycle");
}
// @HXV-007 — A MIRROR PAIR SHARES A SHAPE DIGEST, AND THAT IS LAW I, NOT A LAW F
// FAILURE.
//
// The flip is one of the twelve orientations, and mirroring a turtle walk REVERSES
// its turn sequence. At level 1 it never shows: every admitted form has equal sides,
// so it is achiral and its mirror is just a rotation of it. Unequal sides make a
// form and its mirror two genuinely different texts that draw mirror-image fields —
// and therefore share a `field_digest`, which quotients orientation away.
//
// READ THAT COLLISION AS A LAW F FAILURE AND YOU WILL "FIND" DOZENS THAT ARE NOT
// THERE — the first version of the census reported 36. Law F asks about `field_norm`,
// which does NOT quotient orientation, and there the pair is two distinct fields.
fn test_a_mirror_pair_shares_a_shape_digest_and_that_is_law_i() {
a = form_new(0, [1, 1, 2], [3, 5, 4]);
b = form_new(1, [1, 1, 2], [3, 4, 5]);
fa = drawn(a);
fb = drawn(b);
assert(hexset_count(fa) == 4 && hexset_count(fb) == 4,
"four cells each, got {hexset_count(fa)} and {hexset_count(fb)}");
assert(turns_reverse_eq(a.fm_turn, b.fm_turn), "their turn sequences are reverses");
assert(!turns_cyclic_eq(a.fm_turn, b.fm_turn), "and not re-spellings of one cycle");
assert(digest_eq(field_digest(fa), field_digest(fb)),
"so they are one SHAPE -- law I, and the shape digest says so");
assert(!digest_eq(field_norm(fa), field_norm(fb)),
"while law F asks field_norm, where they are two distinct fields");
// CONTROL — the mirror test must be able to say no
assert(!turns_reverse_eq([3, 4, 5], [3, 4, 5]),
"CONTROL: a chiral cycle is not its own mirror");
assert(turns_reverse_eq([3, 4, 5], [3, 5, 4]),
"CONTROL: and a genuine mirror is not rejected");
}
// @HXV-008 — THE INDEX IS A COST FIX, NOT A REACH FIX.
//
// `rebuild_with` re-DRAWS every candidate on every lookup — O(N) fills per field.
// `index_build` draws each candidate once and keys a map on its digest, so recovery
// becomes a single probe. That is the whole of what it buys.
//
// IT DOES NOT MOVE THE FRONTIER. Building the index still walks the enumeration in
// full (`ri_fills` says so, out loud), so a form the enumeration cannot reach is
// exactly as far away as before: `rebuild_indexed` misses, falls back to the scan,
// and reports R2 — the same answer `rebuild` gives, at the same reach. Reaching
// further is `rebuild_construct`'s job, and it is a different mechanism, not a
// faster table.
fn test_the_index_is_a_cost_fix_not_a_reach_fix() {
cand = candidate_forms();
idx = index_build(cand);
assert(idx.ri_fills == len(cand),
"building it walked the whole enumeration, got {idx.ri_fills} fills");
// a candidate: one probe, the same answer as the scan
f = tri(0, 2);
cells = drawn(f);
hit = rebuild_indexed(idx, cand, cells);
scan = rebuild_with(cand, cells);
assert(hit.rb_regime == REGIME_R1 && scan.rb_regime == REGIME_R1,
"both find it, got {hit.rb_regime} and {scan.rb_regime}");
assert(form_write(hit.rb_form, "x") == form_write(scan.rb_form, "x"),
"and agree on the form -- the digest IS the field, compared in full");
// above the enumerated set: the index reaches no further than what built it
rect = drawn(form_new(0, [3, 2, 3, 2], [3, 3, 3, 3]));
miss = rebuild_indexed(idx, cand, rect);
assert(miss.rb_regime == REGIME_R2,
"the index cannot reach past its own candidate set, got {miss.rb_regime}");
assert(rebuild_construct(rect).rb_regime == REGIME_R1,
"while the construction does -- a different mechanism, not a faster table");
// every route to a TEXT is empty unless the answer is R1, so a caller cannot spell an
// R2 guess as if it were an authored stencil
want = form_write(form_canon(f), "back");
assert(rebuild_text_with(cand, cells, "back") == want, "the scan spells it");
assert(rebuild_indexed_text(idx, cand, cells, "back") == want, "the index spells the same");
assert(rebuild_text_with(cand, rect, "back") == "", "and R2 spells nothing");
assert(rebuild_indexed_text(idx, cand, rect, "back") == "", "through either route");
}
// @HXV-009 — A FORM'S LEVEL IS ITS LONGEST SIDE, AND THE LADDER NESTS EXACTLY.
//
// `forms_upto(n)` is every admitted 3-sided form with all sides in `1..n`;
// `forms_at_level(n)` is only what is NEW at `n` — the forms whose longest side is
// exactly `n`. That definition is what keeps a committed corpus valid forever: the
// entries generated at level 1 stay generated at level 1 when the ladder is raised,
// so `corpus/a1` is never regenerated and a level-2 run only has to author what
// level 2 adds.
//
// THE ARITHMETIC IS THE PROMISE: `|upto(n-1)| + |at_level(n)| = |upto(n)|`, with no
// overlap and nothing dropped. Read `forms_at_level` as "the forms at that size"
// rather than "the forms NEW at that size" and a corpus regenerates itself every
// time the level moves.
fn test_the_level_ladder_nests_exactly() {
u1 = forms_upto(1);
u2 = forms_upto(2);
u3 = forms_upto(3);
assert(len(u1) == 10 && len(u2) == 32 && len(u3) == 60,
"the ladder measures 10, 32, 60, got {len(u1)}, {len(u2)}, {len(u3)}");
assert(len(forms_at_level(1)) == 10, "level 1 adds all ten, got {len(forms_at_level(1))}");
assert(len(u1) + len(forms_at_level(2)) == len(u2),
"level 2 adds exactly the rest of upto(2), got {len(forms_at_level(2))}");
assert(len(u2) + len(forms_at_level(3)) == len(u3),
"and level 3 the rest of upto(3), got {len(forms_at_level(3))}");
// every level-1 form survives into level 2 -- the ladder GROWS, it does not re-cut
kept = 0;
for i in 0..len(u1) {
t1 = form_write(u1[i] ?? form_new(0, [], []), "x");
for j in 0..len(u2) {
if form_write(u2[j] ?? form_new(0, [], []), "x") == t1 { kept = kept + 1; }
}
}
assert(kept == len(u1), "all ten reappear in upto(2), got {kept}");
// and a level-1 form's longest side is 1 -- that is what "level" means
for k in 0..len(u1) {
assert(form_maxlen(u1[k] ?? form_new(0, [], [])) == 1,
"every level-1 form has longest side 1");
}
// `level1_forms` is retained for the A1 gates and must stay the same set
assert(len(level1_forms()) == len(forms_at_level(1)),
"level1_forms is forms_at_level(1), got {len(level1_forms())}");
}