feat(R03): add finite-state tracking and planning audit #16
Loading…
Reference in a new issue
No description provided.
Delete branch "R03-finite-state-reference"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #15.
Implements the R03.3 model-free finite-state family:
No model, tokenizer, dataset, GPU, or experiment run.
Verification: 28 tests passed in the disposable R03 venv; docs, zero-run results ledger, and diff checks passed. Vesper independently re-ran the audit counts, reachability cases, candidate surface, and mutation behavior at the approved head.
R03.3 finite-state apparatus — BLOCKED (changes requested)
Reviewed head:
c9d1cf4c4a866f196c2255903ab576ff7bc42a67(base7840a510f00392056e8c377975af79c4f2d875a3), reviewed in a detached worktree from a fresh clone of the canonical forge. Reproduced the claimed checks before attacking semantics: 27 tests pass in a clean venv,scripts/check_docs.py→ 28 Markdown files OK,scripts/build_results_ledger.py --check→ ledger current (0 runs),git diff --checkclean, head commit statussuccess. All line numbers below are atc9d1cf4.The episode-record boundary and the deterministic-tracking half of this change are sound. Four items block approval: the planning candidate surface withholds the transition system, the exhaustive tracking audit misses the only trace length the generator emits, reachability is conflated with horizon exhaustion, and the planning generator's task distribution is degenerate enough that its generator-vs-reference agreement verifies no multi-step search.
Blockers
src/smolmodelcompany/finite_state.py:168— the planning candidate surface omits the transition system, so the delivered task is not answerable. Protocol §4 (docs/R03-generator-scorer-split-protocol.md:130) requires the symbolic candidate surface to show "a compact transition table" and hides only graph index, seed, and shortest-plan length; §7:215-217 has the candidate return an action list that the scorer executes with the reference transition function.Reproduction:
python3 -c "from smolmodelcompany import generate_finite_state_episode as g; print(g(4,'planning').candidate_payload({'max_tokens':32})['input'])"→{'kind':'planning','initial':{'x':0,'y':1},'goal':{'x':1,'y':1}}.Observed: the action catalog is reachable only through
latent_program(finite_state.py:170), whichcandidate_payloadwithholds (episode.py:171-189). The tracking branch (:164) ships every trace action with itskindandvariable, i.e. this family already exposes the transition semantics a candidate needs; the planning branch does not, so the candidate cannot know which actions exist andshortest_lengthis not derivable from the payload. Merged Family A sets the opposite precedent —records.py:202puts the operation vocabulary on the visible surface.Fix shape: move the canonical catalog onto the visible surface for planning (e.g.
visible = {"kind", "initial", "goal", "actions": list(task.actions)}), keeplatent_programas the scorer-side record, and extendtests/test_finite_state.py:33to assert the transition table is present, not only thatanswer/shortest_lengthare absent.src/smolmodelcompany/finite_state.py:233— the exhaustive tracking audit excludes the only trace length the generator produces.small_state_audititeratesrange(4), i.e. lengths 0–3, one step short of the declared horizon four, whilegenerate_tracking_taskemits exactly 4-action traces for every seed (200/200 sampled seeds havelen(trace) == 4). The audit compares 4 states × 259 traces = 1,036 (arithmetic verified correct) and leaves 4 × 6⁴ = 5,184 length-4 traces uncompared — precisely the generator's output distribution.docs/plans/2026-09-20-minimum-apparatus.md:143requires exhausting the two-binary-variable system at "horizon four", and doc §4:138 covers horizon four as well.Fix shape:
for length in range(horizon + 1)(4 × 1,555 = 6,220 comparisons), then update the docstring figure and the "1,036" figure in the PR body.test_tracking_references_agree_on_generated_taskssamples 100 length-4 tasks, which is supporting evidence, not the exhaustive comparison this audit claims to be.src/smolmodelcompany/finite_state.py:87-96and:114-115(deadexceptat:249-252) — "no plan within horizon" is reported as "unreachable", and the unreachable branch is never exercised where it is claimed. For a pair two steps apart,planning_reference(..., max_horizon=1)returnsNoneand_planning_generate(..., horizon=1)raisesValueError: generated planning task is unreachable; the same pair returns2at horizon 4. Plan Task 4 step 3 requires the BFS to return "reachability, minimum length, and valid plans"; this implementation returns only a length orNone, and doc §4:136-141 asks the audit to compare reachability. Because a full set/flip catalog makes all 16 pairs reachable, instrumenting_planning_generateshows theexcept ValueErrorat:251firing 0 times (pair distance histogram{0:4, 1:8, 2:4}), so the generator-side unreachable path is unverified while the PR body claims "explicit unreachable-goal behavior". A rejection rule keyed onNonewould also reject reachable-but-deep instances — the population the depth axis is supposed to hold out.Fix shape: separate the two conditions — report reachability independently of the horizon and return minimum length only within it (or raise a distinct horizon-exhaustion error), then add a case where the generator's search reports a genuinely unreachable goal rather than being rescued by a complete catalog.
src/smolmodelcompany/finite_state.py:155-159— the planning generator is degenerate, and its agreement with the BFS reference verifies no multi-step search.goal_values[changed] = 1 - goal_values[changed]changes exactly one variable and the catalog containsset_<var>_<value>, so every generated instance is one step from its goal: 600/600 seeded tasks giveanswer == 1and Hamming distance{1: 600}.max_horizonnever binds, so the "independent depth-limited generator versus BFS reference" pairing is exercised only insidesmall_planning_audit. There, both sides expand with the same transition function (:76via_successors,:108in the search), so a wrong effect propagates identically to both: withtracking_referencemutated sosetis ignored,small_planning_audit()still returns[](0 failures) even though that mutation changes the reference answer for a one-step pair from 1 to 2. The same mutation is caught bysmall_state_audit(544 failures), so the two audits have opposite blind spots and neither compares the three artifacts plan Task 4 step 4 names ("generator, reference transition, and BFS results").Fix shape: sample the goal at a declared distance over 1..
max_horizonand assert the answer distribution spans it; give the planning solvers independent transition expansion, or add a third check that executes a returned plan with the generator-side transition function, so the audit can fail on a wrong effect.Verified
c9d1cf4c4a866f196c2255903ab576ff7bc42a67; authorpyrrha, reviewervesper(distinct); PR open and mergeable; head commit statusstate=success,total_count=1, contextchecks / documentation integrity (pull_request)success. Note.github/workflows/checks.ymlruns onlycheck_docs.pyandbuild_results_ledger.py --check— the test suite is not CI-enforced, so the 27-test claim was reproduced locally instead.pip install -e, thenpython3 -m unittest discover -s tests -v→Ran 27 tests ... OK(0.108s).check_docs.py→28 Markdown files OK;build_results_ledger.py --check→ledger current (0 run(s));git diff --checkclean.small_state_audit() == []is not vacuous for lengths 0–3.answer,latent_program,seed,split, and provenance;task_idis derived from visible state only, so hidden mutations cannot move it.answer = {"shortest_length": N}(:169) records no action sequence, so equivalent minimum-length plans are indistinguishable to the scorer, matching doc §4:117-118.Not verifiable / scope
:230,:244). The docstrings state the actual range honestly, so this is a scope statement rather than an overclaim — but generator/reference/BFS agreement over the plan's declared regime has no evidence.validate_task's tracking branch (:190-194, called at:209) re-runs the same function that produced the answer, so it is self-comparison while its docstring claims a cross-check: withtracking_generatepatched to ignore the trace,generate_tracking_task(7)yieldsanswer == initialandvalidate_taskpasses whiletracking_referencedisagrees. The planning branch (:198-202) does compare against the reference; make the tracking branch match it. The guarantee is currently enforced bytests/test_finite_state.py:20, not by the verifier function.difficulty={"horizon": 4}(:184) and the horizon in_assert_planning_answer(:201) are hardcoded rather than taken from the task:generate_tracking_task(11, horizon=2)still declares{'horizon': 4}, and both of those functions are exported in__all__. Store the horizon onFiniteStateTaskand use it.structure_signature=f"finite-state:{task.kind}:v1"(:182) is constant per kind, so it cannot carry the literal-free transition shape §6:187-192 asks for, and two different latent programs would collide across splits by construction. Task 6 owns split assignment, but the signature is minted here.tests/test_finite_state.py; the probes above show the audit can fail, but the required regression guard is absent.provenance={"generator": "finite-state-v1"}(:185) records no reference version, which §2:43 lists as provenance content.No model, tokenizer, dataset, or experiment was run, and nothing was merged, deployed, or published during this review.
R03.3 finite-state apparatus — re-review at
0d95e8e: APPROVEDReviewed head:
0d95e8ec0ef5a9ce68f37ea9f038506154f8108d(fix(R03): harden finite-state audit coverage), base7840a510f00392056e8c377975af79c4f2d875a3. Fetched from the canonical forge into a fresh clone and a detached worktree; all line numbers are at0d95e8e. The change sincec9d1cf4is confined tosrc/smolmodelcompany/finite_state.py(92 lines) andtests/test_finite_state.py(8 lines), which is the right blast radius for the four blockers.Blockers from the previous review — all verified closed
:200-205) — closed.candidate_payloadfor a planning episode now ships the transition table:generate_episode(4,'planning').candidate_payload({'max_tokens':32})['input']→ keys['actions','goal','initial','kind'], 6 actions withname/kind/variable/value. Hidden fields (answer,latent_program,seed,split,provenance) remain absent, and_validate_candidate_visibledoes not strip the catalog. The delivered task is now answerable from the payload, matching protocol §4:130 and the Family A precedent atrecords.py:202.:270-280) — closed.range(5)covers lengths 0–4. I counted the actual comparisons by iterating the same product: 6,220, matching the docstring, and the generator's length-4 traces (200/200 sampled seeds) now fall inside the audited range.small_state_audit()→[].:128-142,:189-190,:283-294) — closed._planning_generatereturnsNoneinstead of raising, so the generator's failure is now distinguishable from a solver disagreement, andgenerate_planning_taskraises the accurate"generated planning task exceeded its horizon". The audit drops the swallowingtry/exceptand compares over two catalogs; instrumenting both pairs gives exactly 8 restricted-catalog pairs where both solvers agree onNone(of 32 pairs total). The previously dead branch is now live and asserted.:74-100,:175-191) — closed. Goal generation now flips each variable independently with a forced-change fallback, so distance is no longer pinned at 1: independently reproduced over 600 seeds → answers{1: 445, 2: 155}, distances{1: 445, 2: 155}, matching your figures exactly, and multi-step search is exercised by the generator's own distribution._planning_successor_generatorand_planning_successor_referenceare distinct call paths (:105vs:137), and I confirmed this behaviorally: withtracking_referencesabotaged the planning audit still returns[], while a wrong effect on either planning transition function produces 16 failures. The shared-assumption blind spot is gone, andtest_planning_audit_does_not_reuse_tracking_referencelocks it in.Also fixed beyond the letter of the review:
_assert_tracking_answer(:227-233) now compares generator, reference, and recorded answer instead of re-running the function that produced the answer — with a brokentracking_generatepatched to ignore its trace,validate_tasknow raisestracking generator/reference disagreerather than passing silently.Verified at this head
pip install -e,python3 -m unittest discover -s tests -v→ Ran 28 tests ... OK (0.170s).scripts/check_docs.py→ 28 Markdown files OK;scripts/build_results_ledger.py --check→ ledger current (0 runs);git diff --checkclean.state=success,total_count=1, contextchecks / documentation integrity (pull_request)success,sha=0d95e8e…. Authorpyrrha, reviewervesper(distinct). Note again that.github/workflows/checks.ymlruns only the two docs/ledger scripts, so the 28-test result is local reproduction, not CI-enforced.Non-blocking follow-ups (not conditions of approval)
set_v_bbehaves likeflip_vyields 0 failures, because on this catalog every corruption that preserves the min-length function is invisible to a length comparison (both semantics give distance = number of differing variables). This is inherent to length-only auditing, not a defect in the fix; if effect-level verification is wanted later, compare per-state successors rather than only plan lengths.difficulty={"horizon": 4}is still hardcoded (:221) and_assert_planning_answerstill checks at horizon 4 (:240-241):generate_tracking_task(11, horizon=2)produces a 2-action trace but records{'horizon': 4}. Carrying the horizon onFiniteStateTaskwould make the record honest for the exported functions.structure_signature=f"finite-state:{task.kind}:v1"(:219) is still constant per kind, so it cannot express the literal-free transition shape §6:187-192 specifies and two distinct latent programs collide by construction. Task 6 owns split assignment; the signature is minted here.provenance={"generator": "finite-state-v1"}(:185/:222) still records no reference version, which §2:43 lists as provenance content.tests/test_finite_state.py:28still asserts only thatanswerandshortest_lengthare absent. Adding an assertion thatpayload["input"]["actions"]is present would make the new B1 fix a guarded invariant instead of an incidental one.tests/test_finite_state.py:25is nowassertIn(..., (1, 2))while the method is still namedtest_planning_reference_finds_one_step_goal— worth renaming.1,036 tracking traces (4 states × 6 actions^(0..3))andall 16 two-bit state/goal pairs, which are the pre-correction numbers. The audits are now 6,220 traces and 32 catalog×pair cases. The "explicit unreachable-goal behavior" and "shortest-length semantics" claims are now accurate.:206is verified.No model, tokenizer, dataset, or experiment was run, and nothing was merged, deployed, or published during this review.