hex_recover source

← 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.

ItemDefinedWorked exampleCall sites
orient_k fn40—111
orient_m fn55—112
field_digest fn133HXV-00193 100 405
digest_eq fn144—89 91 93 98 100 405 … (7 total)
digest_text fn152—239 255
form_key fn166—167
form_key_noturn fn177HXV-002168
count_digest fn188HXV-002104 145
turns_cyclic_eq fn198HXV-006331 368 370 372 376 378 … (7 total)
field_exact fn227HXV-001239 89 90
field_exact_text fn239—83 84
field_norm fn251HXV-001255 144 152 430 91 92 … (12 total)
field_norm_text fn255—203 224 244
form_maxlen fn267—312 499
forms_upto fn276HXV-009308 89 162 476 477 478
forms_at_level fn307HXV-009481 482 483 484 485 504 … (7 total)
turns_reverse_eq fn325HXV-007402 411 413
forms_sides fn350—396
forms_a3 fn393—90
fit_chunk fn38HXV-003150 221 428 143 259
REGIME_R1 const62—69 161 173 181 246 256 … (18 total)
REGIME_R2 const63—69 166 392 413 424 434 … (11 total)
Rebuilt struct68—140 143 161 166 243 246 … (12 total)
LEVEL const80—89 128
candidate_forms fn88HXV-004140 13 60 126 430
level1_forms fn96—504 505
rebuild fn140HXV-005180 20 43 302
rebuild_with fn143—140 172 248 439
rebuild_text_with fn171—456 458
rebuild_text fn179—33 307
IdxEntry struct202—210 227
RecoveryIndex struct209—217 218 243 253
index_build fn217HXV-002130 431
rebuild_indexed fn243HXV-008255 438 447
rebuild_indexed_text fn253—457 459
hull_keys fn321—389
side_heading fn361—405
side_len fn378—409
rebuild_construct fn388HXV-004440 217 223 230 238 246 … (13 total)
rebuild_construct_text fn439—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())}");
}