hex_recover API

← hex_recover · version 0.1.6

37 public items, in source order. Generated from the registry index, which re-derives them from the package source at publish time — so a signature here is the one that version actually ships.

pub fn orient_k(k: integer, m: integer, o: integer) -> integer

── 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_m(k: integer, m: integer, o: integer) -> integer

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 field_digest(cells: HexSet) -> vector<integer>

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.

pub fn digest_eq(a: vector<integer>, b: vector<integer>) -> boolean

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_text(d: vector<integer>) -> text

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 form_key(f: Form) -> text

── 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_noturn(f: Form) -> text
pub fn count_digest(cells: HexSet) -> integer

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.

pub fn turns_cyclic_eq(a: vector<integer>, b: vector<integer>) -> boolean

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.

pub fn field_exact(cells: HexSet) -> vector<integer>

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

pub fn field_exact_text(cells: HexSet) -> text

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_norm(cells: HexSet) -> vector<integer>

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)

pub fn field_norm_text(cells: HexSet) -> text

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 form_maxlen(f: Form) -> integer

── 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 forms_upto(maxlen: integer) -> vector<Form>

Every admitted 3-sided form with all side lengths in 1..maxlen, canonicalised and deduplicated.

pub fn forms_at_level(n: integer) -> vector<Form>

Only the forms NEW at this level — longest side exactly n.

pub fn turns_reverse_eq(a: vector<integer>, b: vector<integer>) -> boolean

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.

pub fn forms_sides(sides: integer, maxlen: integer) -> vector<Form>

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_a3() -> vector<Form>

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 fit_chunk(f: Form) -> HexSet

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

pub struct Rebuilt { rb_regime: integer, rb_form: Form, rb_rho: integer, rb_matches: integer }

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 fn candidate_forms() -> vector<Form>

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.

pub fn level1_forms() -> vector<Form>

Retained for the A1 gates: the level-1 enumeration on its own.

pub fn rebuild(cells: HexSet) -> Rebuilt

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.

pub fn rebuild_with(cand: vector<Form>, cells: HexSet) -> Rebuilt

The core, with the candidate set supplied — build it once per gate run, not once per lookup.

pub fn rebuild_text_with(cand: vector<Form>, cells: HexSet, name: text) -> text

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(cells: HexSet, name: text) -> text

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 struct IdxEntry { ix_digest: text, ix_form: Form }

── 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 RecoveryIndex { ri_map: hash<IdxEntry[ix_digest]>, ri_count: integer, ri_collisions: integer, ri_fills: integer }

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 fn index_build(cand: vector<Form>) -> RecoveryIndex
pub fn rebuild_indexed(idx: RecoveryIndex, cand: vector<Form>, cells: HexSet) -> Rebuilt

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.

pub fn rebuild_indexed_text(idx: RecoveryIndex, cand: vector<Form>, cells: HexSet, name: text) -> text

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 hull_keys(cells: HexSet) -> vector<integer>

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 side_heading(dk: integer, dm: integer) -> integer

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_len(dk: integer, dm: integer, h: integer) -> integer

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 rebuild_construct(cells: HexSet) -> Rebuilt

THE ROUTINE: recover the form from the field, then verify by re-drawing it.

pub fn rebuild_construct_text(cells: HexSet, name: text) -> text

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.