Skip to content

refactor: collapse three duplicated abstractions (vector maths, PATH lookup, tree copy) - #399

Open
pawellisowski wants to merge 5 commits into
mainfrom
routine/abstractions-2026-08-10
Open

refactor: collapse three duplicated abstractions (vector maths, PATH lookup, tree copy)#399
pawellisowski wants to merge 5 commits into
mainfrom
routine/abstractions-2026-08-10

Conversation

@pawellisowski

@pawellisowski pawellisowski commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Scheduled abstraction sweep. Three things were implemented more than once and had drifted apart; each is now one implementation both callers use.

1. 2D/3D vector maths → render::geom

render::ifc and render::viewer_3d consume the same scene JSON and each carried its own copy of the same eleven primitives: dot3, cross3, length3, distance3, normalized3, polygon_edges, point_in_polygon, point_segment_distance, cross2, segments_intersect, polygon_is_simple_nonzero. Identical maths, two spellings of a vector — ifc used tuples (f64, f64, f64), viewer_3d used arrays [f64; 3].

Beyond the pair, ifc carried a third cross product (fn cross, tuple-typed) beside its own cross3, and viewer_3d restated cross/dot/length inline in two frame guards. Arrays win as the one spelling, so ifc's Vec2/Vec3 are now the shared aliases and its tuple field access became indexing.

This one found a bug. viewer_3d's normalized3 had lost the is_finite guard its ifc twin kept. A uDir of [1e200, 0, 0] passes finite_number, but 1e200 * 1e200 overflows to inf, so the old code divided by infinity and returned Some([0, 0, 0]) — a "unit" vector of length zero. It only failed at all because that zero vector went on to trip the orthonormality check downstream, by luck. The shared version rejects the non-finite length up front.

The user-visible consequence is a changed error message on that input: directions must be nonzerodirections must be nonzero and finite, which is also more truthful about what is wrong. degenerate_bolt_frame_is_rejected_not_panicked is updated with a comment explaining the change; it still asserts what it was written to assert, that this input is a validation error and not a panic.

2. find_on_pathcrate::which

runtime::invoker (finds claude/codex) and render::blender (finds blender, or whatever AWARE_BLENDER names) each had one. They had drifted in ways that only bite on Windows, and — as Codex caught on review — neither copy was wholly right:

  • blender's omitted the .com extension.
  • blender's appended an extension even to a name that already carried one, so AWARE_BLENDER=blender.exe searched for blender.exe.exe.
  • invoker's verbatim rule keyed on any extension. Path::extension splits at the last ., so the documented AWARE_BLENDER=blender-4.2 form parses as extension "2" — which would have stopped it resolving to blender-4.2.exe, a regression blender's append-always copy did not have.

The unified rule keys on the extension being one Windows can actually spawn (exe/cmd/bat/com, case-insensitive), and tries the verbatim name last rather than instead — so every lookup either copy resolved still resolves, and both drifted cases are fixed. find_in_dirs moved with it, along with the test that covered it; six more tests pin the behaviours.

3. copy_dir_recursivecrate::fs_tree

Three copies: install::local (shared by install::rename and install::registry), commands::voice, and a plugins::claude_code test fixture. Two were byte-identical. voice's differed in three ways: it returned AwareError, it left the destination root for its caller to create, and it classified entries with Path::is_dir.

The first two are not behaviour any caller needs kept, so the shared version creates the destination and returns io::Error, which voice maps at its one call site. No flag, no mode parameter.

The third one mattered, and Codex caught that the first revision of this PR got it backwards. Path::is_dir follows symlinks; DirEntry::file_type does not. Unifying on install's rule would have made a voice pack containing a symlink to a directory stop installing — the entry takes the file branch and fs::copy fails on a directory source. So the voice rule survives, which is also what install would have wanted, since its rule turned such a tree into an error rather than a copy. Following links makes cycles reachable, so the walk tracks the canonical path of every directory open on the recursion stack and returns an error on a cycle rather than recursing until the stack dies.

Review

Codex reviewed 23c5015e and raised two P2 findings — the which dotted-name case and the fs_tree symlink case above. Both were real defects introduced by the unification, neither was a false positive, and both are fixed at the root in 60ae5b8c with tests. Nothing was rejected or triaged away.

Considered and rejected

Two more pairs look like duplicates and are deliberately left apart:

  • builder::mod::now_iso and runtime::provenance::now_iso — byte-identical (chrono::Utc::now().to_rfc3339()), but they timestamp two unrelated contracts: generated-agent provenance metadata and the runtime's JSONL log envelope. Sharing one function would couple the two formats such that changing one silently changes the other. A four-token expression is not worth that coupling. Left as-is.
  • render::file::abs_path and render::blender::abs_path — genuinely identical and a fair candidate, but this PR is capped at three unifications to stay reviewable in one sitting. Deferred rather than rejected.
  • kebab_ascii and openapi::kebab — already documented in-tree as not the same slugifier. Untouched.

Gates

Run from cli/ on the pinned 1.95.0 toolchain, with CI's apt deps (clang libsecret-1-dev libdbus-1-dev pkg-config) installed:

  • cargo fmt --all -- --check — pass
  • cargo clippy --all-targets -- -D warnings — pass
  • cargo test — pass (42 test binaries green)

Substrate check

Nothing here bakes in a host or an extension. render::geom is scene geometry with no domain meaning; crate::which is a generic PATH lookup (its one host-shaped comment, about the blender override, describes a caller, not a behaviour); crate::fs_tree is a plain filesystem walk.

claude added 3 commits August 10, 2026 03:55
…der::geom

`render::ifc` and `render::viewer_3d` consume the same scene JSON and each
carried its own copy of the same eleven primitives — dot/cross/length/distance,
normalize, the polygon walk, the segment-intersection and simple-polygon tests.
The two copies were identical maths in two spellings of a vector: `ifc` used
tuples, `viewer_3d` used fixed arrays. `ifc` also carried a *third* cross
product (`fn cross`) beside its own `cross3`, and `viewer_3d` restated
cross/dot/length inline in its frame guards.

Arrays win as the one spelling, so `ifc`'s `Vec2`/`Vec3` are now the shared
aliases and its tuple field access becomes indexing.

The drift this exposes: `viewer_3d`'s `normalized3` had lost the `is_finite`
guard its `ifc` twin kept, so a `1e200` direction overflowed to an infinite
length and it answered `Some([0,0,0])` — a unit vector of length zero. The
shared version rejects it up front, so that frame now fails as
"directions must be nonzero and finite" instead of surviving to fail the
orthonormality check by luck. Its regression test is updated to say so.
`runtime::invoker` and `render::blender` each had a `find_on_path`, and the two
had drifted in the way that only bites on Windows: `blender`'s omitted the
`.com` extension, and appended an extension even to a name that already carried
one — so an `AWARE_BLENDER=blender.exe` override searched for `blender.exe.exe`
and reported Blender missing. `invoker`'s handled both, so its behaviour is what
survives; `blender` gains the fix.

`find_in_dirs` moves with it, along with the test that covered it.
Three copies of the same tree walk: `install::local` (shared by
`install::rename` and `install::registry`), `commands::voice`, and a
`plugins::claude_code` test fixture. Two were byte-identical; `voice`'s differed
only in returning `AwareError` and leaving the destination root for its caller
to create.

Neither edge is behaviour a caller needs kept, so the shared version creates the
destination and returns `io::Error`, which `voice` maps at its one call site —
no flag, no mode parameter.

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23c5015e57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/which.rs Outdated
let dirs: Vec<PathBuf> = std::env::split_paths(&path).collect();
// If the caller already supplied an extension (e.g. "codex.cmd"), don't append a
// second one — look the name up verbatim.
let exts: &[&str] = if cfg!(windows) && Path::new(name).extension().is_none() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Try executable suffixes for dotted bare names

On Windows, when AWARE_BLENDER uses the explicitly supported bare command form blender-4.2 and PATH contains blender-4.2.exe, Path::extension() returns Some("2"), so this condition searches only for the unsuffixed name. resolve_blender_binary routes that documented override form through this helper, while Blender's previous lookup appended .exe, meaning the override now incorrectly reports that no executable exists. Treat only recognized executable suffixes as already extended, or try the Windows suffixes in addition to the verbatim name.

Useful? React with 👍 / 👎.

Comment thread cli/src/fs_tree.rs Outdated
for entry in std::fs::read_dir(src)?.flatten() {
let from = entry.path();
let to = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve voice-pack directory symlinks

When a source passed to aware voice install contains a symlink to a directory, its former copier used from.is_dir(), which followed the link and recursively copied the referenced contents. DirEntry::file_type() does not follow symlinks, so the shared copier now takes the file branch and std::fs::copy fails because the source resolves to a directory. Such voice packs therefore regress from installing successfully to returning a copy error; preserve the former follow-and-recurse behavior for this caller or explicitly handle directory symlinks here.

Useful? React with 👍 / 👎.

Two real defects the unification introduced, both caught on review.

`which`: the verbatim-lookup rule keyed on *any* extension, and
`Path::extension` splits at the last `.`, so the documented
`AWARE_BLENDER=blender-4.2` override parsed as extension "2" and stopped
resolving to `blender-4.2.exe` on Windows — something `render::blender`'s
append-always copy had handled. The rule now keys on the extension being one
Windows can actually spawn, and the verbatim name is tried last as a fallback,
so every lookup either former copy resolved still resolves.

`fs_tree`: `install`'s copy classified entries with `DirEntry::file_type`,
which does not follow symlinks, while `commands::voice`'s used `Path::is_dir`,
which does. Taking the former would have made a voice pack containing a symlink
to a directory stop installing — the entry takes the file branch and
`fs::copy` fails on a directory source. The `voice` rule survives instead;
since following links makes cycles reachable, the walk now tracks its open
ancestors and reports a cycle rather than recursing until the stack dies.

Copy link
Copy Markdown
Contributor Author

Both P2 findings were real and are fixed in 60ae5b8c. Neither was a false positive — thank you.

cli/src/which.rs — dotted bare names. Confirmed the premise before fixing: Path::new("blender-4.2").extension() is Some("2"), so the verbatim-lookup guard misfired on exactly the override form resolve_blender_binary documents. The rule now keys on the extension being one Windows can actually spawn (exe/cmd/bat/com, case-insensitive) rather than on an extension merely being present, and the verbatim name is tried last rather than instead — so blender-4.2 resolves to blender-4.2.exe, codex.cmd is still looked up verbatim, and nothing either former copy could resolve became unfindable. Pinned by a_dotted_bare_name_is_not_treated_as_already_extended and suffixes_are_tried_before_the_verbatim_name.

cli/src/fs_tree.rs — directory symlinks. Correct, and I had the direction of the drift backwards in the PR body: this was not a difference "no caller needs kept". install's DirEntry::file_type does not follow symlinks, voice's Path::is_dir does, and unifying on the former would have sent a linked directory down the file branch to fail in fs::copy. The voice rule survives instead — which is also what install would have wanted, since its rule turned such a tree into an error rather than a copy. Since following links makes cycles reachable, the walk now tracks the canonical path of every directory open on the recursion stack and returns an InvalidInput error on a cycle rather than recursing until the stack dies. Pinned by three #[cfg(unix)] tests covering a linked directory, a linked file, and a cycle.

Gates re-run from cli/ on the pinned 1.95.0 toolchain: cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, and cargo test all pass (42 test binaries green).

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60ae5b8c5a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/which.rs Outdated
// name like "blender-4.2" resolving to "blender-4.2.exe".
let exts: &[&str] = match () {
_ if !cfg!(windows) || has_executable_extension(name) => &[""],
_ => &[".exe", ".cmd", ".bat", ".com", ""],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid returning extensionless Windows files

On Windows, including "" makes find_on_path accept an extensionless regular file even though this module explicitly notes that such files are not directly spawnable by CreateProcess. If an earlier PATH directory contains a Unix-style codex, claude, or Blender shim while a later directory contains the usable .cmd/.exe, the directory-first loop returns the unspawnable file and the caller fails instead of reaching the valid executable. Keep the verbatim fallback for Unix or already-spawnable extensions; dotted bare names still resolve by trying the Windows suffixes.

Useful? React with 👍 / 👎.

Second-round Codex finding, and correct: the search is directory-outer, so
including "" in the Windows suffix list let an extensionless shim in an early
`PATH` entry win over the spawnable `.cmd` in a later one — handing the caller
a file `CreateProcess` cannot launch instead of the valid executable further
down. Returning an unspawnable name is not a better answer than `None`; it is
the same failure moved somewhere the caller cannot diagnose it.

Windows now searches the executable suffixes only. Nothing regresses: a dotted
bare name like `blender-4.2` still resolves through them, and a name that
already carries a spawnable extension is still looked up verbatim.

The suffix lists are named constants so the tests exercise the list
`find_on_path` really uses — the previous test asserted against a hand-copied
list, which is how it passed while the code under it was wrong.

Copy link
Copy Markdown
Contributor Author

Third P2 addressed in bf612190. Also real, and it was a defect in my own fix rather than in the original code — thank you for the second pass.

The search is directory-outer, so including "" in the Windows suffix list let an extensionless shim in an early PATH entry win over the spawnable .cmd in a later one. Windows now searches the executable suffixes only, with no verbatim fallback: returning a name CreateProcess cannot launch is not a better answer than None, it is the same failure moved somewhere the caller cannot diagnose it. Nothing regresses — a dotted bare name like blender-4.2 still resolves through the suffixes, and a name already carrying a spawnable extension is still looked up verbatim.

One thing worth naming, because it is the reason the bug survived my own testing: the test I added asserted against a hand-copied suffix list rather than the one find_on_path actually passes, so it went green over wrong code. The lists are now named constants (WINDOWS_SEARCH_SUFFIXES, VERBATIM) that both the function and the tests reference, and an_extensionless_early_shim_does_not_shadow_a_later_spawnable_one pins the shadowing case directly.

Gates re-run from cli/ on the pinned 1.95.0 toolchain: cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, cargo test — all pass, 42 test binaries green.

Note for anyone reading the run log: the local test suite initially reported failures that were StorageFull, not test failures — 97 orphaned 401 MB tempfile directories in /tmp, left behind by earlier aborted runs, had filled the disk. Cleaned up and re-run green; no code was changed in response to those.

All three findings from both rounds are now fixed at the root. None were rejected.

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf61219084

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/fs_tree.rs
Comment on lines +61 to +62
if from.is_dir() {
visit(&from, &to, ancestors)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent registry copies from following directory symlinks

When installing or updating from a registry archive containing a directory symlink whose target exists outside the extraction root (for example, an absolute link to / or a user directory), from.is_dir() follows the link and visit recursively copies that external tree into the installation. This is reachable through custom AWARE_REGISTRY payloads because tar extraction preserves symlinks; the former install copier used entry.file_type() and rejected directory symlinks through fs::copy instead. Keep the follow-links behavior only for the voice-pack caller, or otherwise ensure registry/install copies cannot traverse outside their source root.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Stopping here — not merging. This one needs a human.

Codex reviewed bf612190 and raised a fourth P2, on cli/src/fs_tree.rs: a registry archive containing a directory symlink pointing outside the extraction root would now be followed, copying an external tree into the installation, where the former install copier rejected it. That is reachable through a custom AWARE_REGISTRY payload, since tar extraction preserves symlinks.

The finding is real, and I am not going to fix it in this run. Two reasons.

1. I am at the review cap

This routine may re-request review at most twice after fixes. I have used both rounds (23c5015e60ae5b8cbf612190). The rule for still-open findings after the second round is to stop and leave the PR for the maintainer rather than keep iterating or merge on an approval that no longer covers the head. So: not merged, left open. CI is green on bf612190 (all four checks), but a green build was never the gate here.

2. More importantly — this finding says unification #3 was the wrong call

The repo's own rule for this sweep is that two things which merely look alike must stay apart, and that a shared signature which makes one caller grow a flag to get its old behaviour back is the tell. That is exactly what has happened, and it took three rounds to surface:

  • commands::voice followed directory symlinks and needs to keep doing so, or symlinked voice packs stop installing (round 1).
  • install::local / install::registry must not follow them out of the source root, or a hostile registry payload reaches outside it (round 3).

Those are two different policies, not one function with drift. Every fix I pushed moved the shared copier toward one caller at the other's expense. The honest read is that copy_dir_recursive looked identical in three places because the symlink question had never been asked in any of them — not because the three callers wanted the same answer.

Deciding what aware voice install and aware agent install should do about symlinks is a security-policy call about the substrate's trust boundary with registry payloads. It is well outside "collapse a duplicated helper," and it is not a call an unattended sweep should make.

Recommended disposition

Drop unification #3 and land the other two. Unifications #1 (render::geom) and #2 (crate::which) are independent commits, are unaffected by any of this, and each fixed a real drift bug — viewer_3d's lost is_finite guard and blender's two PATH-lookup gaps. Reverting 4c9e21c-equivalent (refactor(cli): one recursive directory copy) plus its follow-up leaves those two clean.

Alternatively, if the intended policy is "no copy ever traverses a symlink out of its source root," that is a good change — but it should be its own PR with the trust boundary stated, not a side effect of deduplication.

Findings status, for the record: three fixed at the root with tests (60ae5b8c, bf612190), one open and deliberately unaddressed (this one). None rejected as wrong.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

PR sweeper, fresh pass. Not merging, and not pushing a fix. Codex's fourth P2 on cli/src/fs_tree.rs:62 is still live against the head bf612190 — it is the one review thread not marked outdated — so the merge gate is unsatisfied regardless of anything else. CI is green on that commit; that was never the gate here.

I re-derived the question from main rather than taking the thread at its word, and it confirms the previous pass's read while sharpening it. The two former copiers on origin/main:

classifier a directory symlink
commands::voice (voice.rs:288) from.is_dir() followed, wherever it points
install::local (local.rs:187) entry.file_type()?.is_dir() takes the file branch → fs::copy errors

So there is no zero-regression single rule, and that is now demonstrable rather than argued:

  1. Follow always — the current head. Registry payloads traverse out of the source root. That is Codex's finding.
  2. Never followinstall's rule. Breaks symlinked voice packs. That was Codex's round-1 finding.
  3. Follow only within the source root — the containment rule, and the option Codex's own text offers second. This looks like the free lunch and is not: a_symlinked_directory_is_followed_and_its_contents_copied (fs_tree.rs:127) plants its target at tmp/real while the source root is tmp/srcdeliberately out of root. Containment cannot be adopted without rewriting the very test that pins round 1's fix, and it still narrows voice relative to main.
  4. A per-caller flag — which is precisely the tell this sweep's own rule names as "these were never one function."

Each option is a product decision about the substrate's trust boundary with registry payloads, and three of the four regress a caller that works on main today. That is not a call an unattended sweep should make, so this stays open.

The previous pass's recommendation still looks right to me on the evidence: land unifications #1 (render::geom) and #2 (crate::which), drop #3. Those two are independent, each fixed a real drift bug, and neither is touched by any of this.

No code was changed in this run. No cross-model review is outstanding — Codex has answered, and its answer is a finding.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants