Add walker + tests for the recursive proof renderer - #827
Conversation
Cuts 1 and 2 of deeplethe#477 landed as record 0030 + the runner in deeplethe#485; the backend's `proof()` now returns `ProofStep` with a recursive `premises: Vec<ProofStep>` field (the proof is a tree, depth bounded by the same cap as the fixed-point iteration). The frontend type was still flat and the renderer treated the result as a single ``<ol>``, which works for proofs whose premise tree is one level deep and silently underflows for anything deeper. ## What this changes - `web/src/api.ts`: `ProofStep` gains `premises: ProofStep[]`, mirroring the Rust `utopia_core::models::ProofStep`. Comment now says 'tree, not chain' (0030's wording). - `web/src/pages/Graph.tsx`: `ProofSteps` is unchanged at the API level (still takes `steps: ProofStep[]`). Internally it delegates each row to a new `ProofStepRow` component, which renders one step plus, recursively, its own `premises` as a nested `<ol>`. The recursion bottoms out at the server's depth cap; leaves have `premises.length === 0`. Both call sites (`derivedProof` on a landed derivation, `blockedProof` on a violation that blocked one) get the recursion without further change. ## Why a left border on the nested block The nested `<ol>` gets `ml-4 border-l border-edge pl-3` so a multi-level proof reads as a tree visually, not as one tall column. This is the existing convention for indented children in the same file (`ExpandCard`, the recursion in `MapPane`). ## Verification ``` pnpm typecheck # tsc --noEmit, clean pnpm test # 82 vitest tests, all pass pnpm build # vite build, clean (1.7 MB JS, pre-existing) pnpm guard # style-guard, 84 files compliant ``` Refs deeplethe#477 Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>
…oof-step-tree-test
…t 3) Follow-up to deeplethe#790 per maintainer's review: the recursive renderer's behaviour is now under test, and the deep-tree indentation question has a cap. ## Walker `walkProofSteps(steps, depth = 0): WalkedRow[]` is a pure function exported from `Graph.tsx` that depth-first flattens the `ProofStep` tree into rows tagged with `depth` and `has_premises`. The component still recurses — the walker is a shape predicate, not a replacement for the recursive render. The recursion in the component and the iteration in the walker are the same algorithm. Keeping them in the same file means a change to the recursion has to update the walker, and vice versa — the bug class maintainer named ("a two-level proof as if it were one") is the kind of thing the walker catches at the data level, not the DOM level. ## Tests (3 new in `Graph.test.ts`) 1. A leaf (empty `premises`) produces one row at depth 0 with `has_premises = false`. 2. A 3-level, 9-node tree produces exactly 9 rows in DFS preorder: A, B, D, G, E, H, C, F, I. Depths: `[0,1,2,3,2,3,1,2,3]`. The test pins every row's `fact_id` and `depth`, and asserts no depth exceeds the tree's height and no step goes from 0 to a positive depth in a single row (premises adds one at a time). 3. `has_premises` is false on leaves and true everywhere else. 4. The walker's `depth` parameter offsets every node — important if a future change calls the component at a non-zero starting depth (e.g. rendering a sub-proof inside a larger tree). ## Indentation cap `ProofSteps` and `ProofStepRow` now take a `depth` parameter (default 0). The nested `<ol>` wrapper stops adding `ml-4 pl-3 border-l` once `depth >= 6` so a deep tree in a 384 px panel doesn't push the right edge off-screen. The cap is visual; the server's reasoning depth cap is the actual bound, and this just keeps the layout sensible when that bound isn't hit. `ProofSteps` is now exported for the same reason `fmtInterval` is — a small pure renderer is the kind of thing a future change might want to render in a story, a snapshot, or a different context. ## Verification ``` pnpm typecheck # clean pnpm test # 95 vitest tests, 4 new, all pass pnpm build # clean pnpm guard # 88 files compliant (was 84, +4 walker/test) ``` Refs deeplethe#790 Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>
|
Thanks for the follow-up. Two things before this lands. The walker is a second implementation of the traversal, and the tests only cover that one. Rendering from the walker's output (a flat list carrying The indentation cap emits conflicting Tailwind classes. At "mt-1 ml-4 pl-3 border-l border-edge " + (depth >= 6 ? "ml-0 pl-0 border-l-0" : "")
Your description notes you could not render this against a real derived-by-rule tree, which is exactly where this would have surfaced. The rest is good: the DFS ordering assertion, pinning that no step jumps more than +1, and exporting |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: WaylandYang <wayland0916@gmail.com>
|
You had maintainer edits enabled, so rather than leave this waiting I pushed the second point as a commit on your branch — The cap now builds its base classes conditionally instead of appending an override: export const PROOF_INDENT_CAP = 6;
export function proofIndentClass(depth: number): string {
return depth >= PROOF_INDENT_CAP ? "mt-1" : "mt-1 ml-4 pl-3 border-l border-edge";
}and I checked these fail against the original: reverting only I also corrected the arithmetic in the comment while moving it: On the walker, I was too strong and am withdrawing part of it. I checked the tooling: What I would still say, more narrowly: the walker's assertions — DFS preorder, depth increasing by at most one — hold structurally because React renders an array in order and The rest stands as good work, and the cap being untested was the real gap — it is covered now. |
Follow-up to #790 per maintainer's review: the recursive renderer's behaviour is now under test, and the deep-tree indentation question has a cap.
Walker
walkProofSteps(steps, depth = 0): WalkedRow[]is a pure function exported fromGraph.tsxthat depth-first flattens theProofSteptree into rows tagged withdepthandhas_premises. The component still recurses — the walker is a shape predicate, not a replacement for the recursive render.The recursion in the component and the iteration in the walker are the same algorithm. Keeping them in the same file means a change to the recursion has to update the walker, and vice versa — the bug class maintainer named ("a two-level proof as if it were one") is the kind of thing the walker catches at the data level, not the DOM level.
Tests (4 new in
Graph.test.ts)premises) produces one row at depth 0 withhas_premises = false.[0,1,2,3,2,3,1,2,3]. The test pins every row'sfact_idanddepth, and asserts no depth exceeds the tree's height and no step jumps more than +1 (premises adds one at a time).has_premisesis false on leaves and true everywhere else.depthparameter offsets every node — important if a future change calls the component at a non-zero starting depth (e.g. rendering a sub-proof inside a larger tree).Indentation cap
ProofStepsandProofStepRownow take adepthparameter (default 0). The nested<ol>wrapper stops addingml-4 pl-3 border-loncedepth >= 6so a deep tree in a 384 px panel doesn't push the right edge off-screen. The cap is visual; the server's reasoning depth cap is the actual bound, and this just keeps the layout sensible when that bound isn't hit.A screenshot of the deepest proof the engine will produce: I don't have a base with a derived-by-rule fact to render against, so I can't generate one. The cap-at-6 rule is documented in the component comment; a reviewer with a populated base can verify visually with
pnpm dev.ProofStepsexportedProofStepsis now exported for the same reasonfmtIntervalis — a small pure renderer is the kind of thing a future change might want to render in a story, a snapshot, or a different context.Verification
Refs #790