Skip to content

[00070] Restore Server Side App Routing Reverted by the 00034 Merge - #111

Merged
rorychatt merged 16 commits into
mainfrom
tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
Aug 2, 2026
Merged

[00070] Restore Server Side App Routing Reverted by the 00034 Merge#111
rorychatt merged 16 commits into
mainfrom
tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge

Conversation

@rorychatt

Copy link
Copy Markdown
Contributor

00070 — Restore Server Side App Routing Reverted by the 00034 Merge

Multi-app routing is back on the server. RustyServer can now register several named apps, a
client picks one with /ws?appId=…, and a live connection can switch apps mid-session via
{"method":"navigate","appId":"…"} without reconnecting. rusty/src/core/apps.rs — 274 lines of
public API that the 53dff77 merge left with no caller — now has one.

The merge that dropped 00035's routing punted on a real conflict: 00035 gave every session one
shared ServiceRegistry, while 00034 gives each connection its own, and use_download URLs are
keyed by the per-connection AppContext. This branch keeps 00034's wiring and folds the
server-level services into each per-session registry, so neither side is reverted.

Final state: branch HEAD 065579a, merged up to origin/main = e6c4398, worktree clean.

Changes

ServiceRegistry::extend_from (rusty/src/core/services.rs) — copies another registry's
entries in, replacing same-typed ones. map is private, so the merge has to live on the type.
Copying Arcs means server-level instances stay shared; only the lookup table is per-session.

AppSessionStore is now built around an app registry (rusty/src/server/session.rs) —
root_factory is replaced by apps: Arc<AppRegistry> + server_services: Arc<ServiceRegistry>. AppSession gained app_id: String.

build_session(connection_id, app_id) is where the two designs are reconciled. It creates a
fresh per-connection registry, folds in the server-level services first, then registers the
framework's own services on top:

services.extend_from(&self.server_services);
services.register(Arc::new(AppContext::new(connection_id.to_string())));
services.register(Arc::clone(&self.query_service));
services.register(Arc::new(ServerSignals::new(...)));
services.register(Arc::new(SignalRegistry::new()));
services.register(Arc::new(DownloadService::new(connection_id.to_string())));

That ordering is load-bearing, not stylistic. Reversed — which reads more naturally as "server
services win" — a with_service::<AppContext> would overwrite the per-connection one and hand
the session another connection's id, and with it another connection's download URLs. It has a
dedicated regression test.

An unresolvable app id yields an $error-not-found session rather than a failed connection.

navigate_session(session, app_id) rebuilds the session in place and returns bool. It checks
the id with AppRegistry::get, not resolve, so an unknown id is refused instead of silently
falling back to the default. The connection id is carried across the swap, so download URLs
minted before the navigation still resolve.

RustyServer grew a multi-app builder (rusty/src/server/ws.rs) — empty(port),
with_app(id, title, factory), with_service(value), alongside the existing
with_bind_address and with_static_dir. new is now a thin wrapper that registers its single
view under AppIds::DEFAULT, so no existing call site changed — including the 39
RustyServer::new arms in widget_harness.rs.

The socket handler routes and navigatesws_handler reads ?appId= via
Query<ConnectParams>; send_refresh is extracted from the inline initial-render block and
resets the reconciler baseline to the tree it just sent; the Navigate arm swaps the app, re-reads
the runtime's channels, and sends a full Refresh. An unknown id logs a warning and holds
position rather than dropping the connection.

Sending a full Refresh after navigation rather than Update patches is deliberate: the entire
tree is replaced, so patches diffed against the old app's baseline would be meaningless.

API Changes

All additive. Nothing was removed or given a breaking signature.

API Signature
ServiceRegistry::extend_from pub fn extend_from(&self, other: &ServiceRegistry)
RustyServer::empty pub fn empty(port: u16) -> Self
RustyServer::with_app pub fn with_app<F, V>(mut self, id: impl Into<String>, title: impl Into<String>, factory: F) -> Self
RustyServer::with_service pub fn with_service<T: Send + Sync + 'static>(self, value: T) -> Self
ConnectParams pub struct ConnectParams { pub app_id: Option<String> } (#[serde(rename = "appId")])
AppSession::app_id pub app_id: String
AppSessionStore::with_apps pub fn with_apps(apps: Arc<AppRegistry>, server_services: Arc<ServiceRegistry>) -> Self
AppSessionStore::apps pub fn apps(&self) -> &Arc<AppRegistry>
AppSessionStore::build_session pub fn build_session(&self, connection_id: &str, app_id: Option<&str>) -> AppSession
AppSessionStore::create_session_for_app pub async fn create_session_for_app(&self, connection_id: String, app_id: Option<&str>) -> Arc<RwLock<AppSession>>
AppSessionStore::navigate_session pub async fn navigate_session(&self, session: &Arc<RwLock<AppSession>>, app_id: &str) -> bool

RustyServer::new and AppSessionStore::new keep their signatures and behaviour.
RustyServer's private root_view field became apps + services, and AppSessionStore's
private root_factory became apps + server_services — both private, so no external impact.

The preserved new + router() contract was checked against a real out-of-crate consumer that
did not exist when the plan was written: rusty-desktop (landed by plan 00137) layers its own
/ route onto RustyServer::router(). It builds, lints and passes its 3 tests/router.rs tests
unchanged.

A new multi-app server reads:

RustyServer::empty(3000)
    .with_app("$default", "Home", || HomeView)
    .with_app("settings", "Settings", || SettingsView)
    .with_service(Db::connect()?)
    .serve()
    .await?;

Files Modified

The feature is three files, exactly as planned. Four more are repairs to a base that does not
build — see the divergences.

File Change
rusty/src/core/services.rs +12 — extend_from
rusty/src/server/session.rs +407 — store rebuilt around AppRegistry; build_session, create_session_for_app, navigate_session, apps(); 14 tests
rusty/src/server/ws.rs +451 — ConnectParams, empty/with_app/with_service, send_refresh, real Navigate arm; 12 tests
rusty-server/src/bin/widget_harness.rs −38/+1 — base repair: invalid UTF-8 byte, main will not compile
rusty/src/shared/widget_names.rs +118/−… — base repair: widget-type scan blind to #[derive(Widget)], 2 tests failing
rusty-macros/src/lib.rs +17/−… — base repair:clippy::question_mark, plus a stray blank line rustfmt rejects
rusty/src/shared/types.rs −4 — base repair: orphaned size_css doc comment fails empty_line_after_doc_comments
src/frontend/.husky/pre-commit +11/−… — base repair: hook harness 3/13, missing [ -f ] guard and || exit 1

rusty/src/core/apps.rs is untouched, as the plan required — it needed a caller, not a change.
So is every RustyServer::new call site, and everything under e2e/ and src/frontend/src/.

26 tests added, 0 pre-existing tests changed, removed or renamed. Verified by diffing test
name lists rather than counts, since a rename hides inside a total — and independently by the
Test inventory CI step: "intact: 495 at e6c4398, 521 in the working tree". Workspace lib
tests: 351 at the fork point → 438 now (26 mine, the rest from main moving underneath).

Manual Testing

All gates green at 065579a, re-run after each of the five merges of main. Commands were
derived from node scripts/ci-step.js rather than taken from the verification prompts, which are
stale — CI now carries --no-default-features on Build/Test/Clippy (plan 00137 feature-gated the
GUI deps):

Gate Result
cargo fmt --all -- --check EXIT=0
cargo build --workspace --no-default-features EXIT=0 (also clean with default features)
cargo clippy --workspace --all-targets --no-default-features -- -D warnings EXIT=0
cargo test --workspace --no-default-features EXIT=0 — 14 targets, 438 lib tests, 0 failed
sh ./scripts/check-test-inventory.sh $(git rev-parse origin/main) EXIT=0 — 495 → 521
sh ./scripts/test-precommit-hook.sh 13/13
sh ./scripts/test-check-test-inventory.sh 6/6
sh ./scripts/test-precommit-gates.sh 10/10
sh ./scripts/test-precommit-frontend.sh all pass

The windows-latest Rust job was also run for real, since this host is Windows: cargo build -p rusty-desktop and cargo clippy -p rusty-desktop --all-targets -- -D warnings, both EXIT=0.
It is the only gate on shell.rs and src/main.rs, which are invisible to the
--no-default-features job.

Each gate was probed with a deliberate violation to prove it actually inspected this branch's
code rather than passing vacuously — trailing whitespace for fmt, clippy::len_zero inside the
new test module for clippy, a fresh CARGO_TARGET_DIR for build. Every probe was reverted and
git status re-confirmed clean.

Two invariants would have passed by construction if the code were wrong in the exact way the
plan warns about, so both were broken on purpose to confirm the tests bite:

  • Flipping extend_from to run last makes
    test_per_connection_services_win_over_server_level_ones fail with left: "server-wide", right: "conn-1".
  • Removing the event_tx re-read in the Navigate arm makes
    test_events_dispatch_to_the_app_mounted_after_navigation hang and time out at 5.02s, because
    the click goes to the dropped runtime's channel.

The pre-commit hook repair was exercised directly, since no build or test gate reaches it and
the commit's own comments make runtime claims. In a scratch repo with no scripts/ directory the
hook exits 0 instead of 127 (the [ -f ] guard), and with a deliberately failing gate script it
exits 1 when hand-run without sh -e (the || exit 1). Both claims hold.

E2E: routing.spec.ts now covers this plan through a real browser, which it did not at the
start of execution. Commit c59be80 landed ?appId= forwarding and window.rustyNavigate in the
harness client, so the browser now drives the ConnectParams this branch added:

$ cd e2e && npx playwright test routing.spec.ts --reporter=list
  5 passed (5.5s)

That includes appId from the page query is forwarded onto the socket URL, asserting
/ws?appId=beta reaches the server. Earlier in execution the full suite was run too: 138 passed,
1 failed — avatar › exposes the density as a size, pre-existing and unrelated, proven by
reverting this branch's three files to main's version and watching it fail identically. It
belongs to plan 00125.

Not manually exercised: no multi-app server was driven by hand in a browser, because nothing in
the repo registers a second app yet — the harness has one RustyServer::new call and zero
with_app calls. The socket tests cover that path over real TCP instead (?appId=, navigate,
navigate-to-unknown, cross-connection isolation, events after navigation). Wiring a real
multi-app harness app is 00041's scope.

Where I diverged from the plan, and why

The plan was drafted at f5c01fe; main advanced well past that before and during execution,
including 44 commits across four moves in the final session alone. Six divergences, all forced by
that movement.

  1. Edit 1 was already done. Plan 00066 (PR [00066] Make ClientMessage Navigate State Optional and Log Undecodable Client Messages #44) had landed #[serde(default)] on
    Navigate.state plus its tracing::warn!. The plan anticipated this and said to keep one of
    each — verified, no duplication, no action.

  2. rebuild_notify also has to be re-read after navigating. The plan re-reads only
    event_tx, because at drafting time the push arm was 00034's 50 ms ticker, which the
    Runtime does not own. Plan 00057 (b2107e2) has since replaced it with
    runtime.rebuild_notifier(). Since *guard = fresh drops the old Runtime and its Notify
    with it, keeping the stale notifier would silently kill every out-of-band push for the rest of
    the connection. This applies the plan's own stated reason for re-reading event_tx to a
    channel that did not exist when it was written.

  3. 26 tests, not the planned 25. The plan's guard for the extend_from ordering
    (test_navigation_preserves_framework_and_server_services) does not actually catch the bug:
    flipping the order still passed, because no test registered a server-level AppContext.
    test_per_connection_services_win_over_server_level_ones was added and does fail, with
    exactly the output the plan documented from its prototype.

  4. The plan's baseline figures were stale. It measured 346 tests and reported the existing
    mod tests blocks as 12 (session.rs) and 2 (ws.rs). Measured at the real fork point
    0eef5ab: 351, with 10 and 5. The plan's own Verification section said to re-measure
    rather than trust the table, which is what was done. Its "13 new in session.rs, 12 in
    ws.rs" also reads as module totals but is not — sibling plans hold tests in both modules, so
    the totals are 24 and 20.

  5. The plan's quoted gate commands no longer match CI. --no-default-features arrived with
    plan 00137. CI wins; the build was run both ways and both are clean.

  6. Four commits are outside the plan's three files, because origin/main is red at
    e6c4398 — in four independent ways.
    Each was proven inherited on a pristine git archive
    of origin/main with a separate CARGO_TARGET_DIR, and corroborated by gh run list --branch main showing CI red on the last five merges. Each blocked a required gate, so none was
    optional:

    Commit Defect Gate it blocked
    7df54a5 Invalid UTF-8 byte in widget_harness.rs from merge 245c372, which pasted the DownloadsApp block twice with an em-dash mangled in one copy RustBuild
    3e3c0c7 The widget-type scan matched only "type": "..." literals, so plan 00093's 12 #[derive(Widget)] conversions became invisible to it — 2 failing tests. Plus a stray blank line rustfmt rejects RustTest, RustFmt
    8f27ad9 clippy::question_mark in rusty-macros/src/lib.rs RustClippy
    aa5328d Plan 00117 deleted size_css and left its doc comment behind, where it documents an unrelated enum → empty_line_after_doc_comments RustClippy
    065579a The precommit-rust-gates.sh call had no [ -f ] guard (127 on older checkouts) and neither call had `

    Precedent for repairing rather than blocking: 5575a29 [00037] Repair broken base: restore 7 harness views dropped by merge.

    Two things about this are worth more attention than the individual bytes. First, on the UTF-8
    tree cargo fmt, cargo clippy and cargo test all reported success — fmt cannot read the
    file so finds no diff, clippy exits 0 while two targets fail to compile, and test never
    builds the target it would have failed in. Second, the clippy defects mask each other:
    clippy stops at the first failing crate, so the types.rs finding was invisible until the
    rusty-macros one was fixed, which took a probe tree carrying only that fix to prove
    inherited. Both are recorded as recommendations.

Five merges of main in all. The first (703e66b) conflicted in ws.rs against plan 00056's
streamed downloads and plan 00068's push debounce; both sides were additive and both were kept.
The second-to-last (8e3c8b8) conflicted in widget_harness.rs, where branch and main had
independently made the same repair — took --theirs and verified the result byte-identical to
main. The rest were clean.

One process note for anyone reading the gate reports: the Test inventory gate reported deleted
tests twice during execution, and both were false alarms caused by main having moved rather
than by anything on this branch — the "deleted" names lived in commits the branch had not yet
merged. The gate compares against whatever ref it is given, so it only means anything once the
branch is current with that ref.
065579a [00070] Repair broken base: pre-commit hook harness fails 3 of 13 cases
aa5328d [00070] Repair broken base: orphaned size_css doc comment fails clippy
0c72bb9 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
8f27ad9 [00070] Repair broken base: clippy question_mark in rusty-macros
3e3c0c7 [00070] Repair broken base: widget type scan misses derive-based widgets
f79c0ed Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
8e3c8b8 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
ab1a162 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
7df54a5 [00070] Repair broken base: main does not compile
0824ce0 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
418e62a Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
703e66b [00070] Resolve merge conflicts with main
4c8e859 [00070] Add 12 socket-level app-routing tests to ws.rs
8168fba [00070] Add 14 app-routing tests to session.rs
9d5fe00 [00070] Restore server-side app routing dropped by the 00034 merge


Created using Ivy Tendril.

rorychatt and others added 15 commits August 1, 2026 23:29
Plan 00035 (PR #23) shipped multi-app routing; the plan-00034 merge 53dff77
dropped all of it ("App routing from #23 deferred for follow-up integration"),
leaving core/apps.rs as dead public API with no runtime caller.

Restores routing while keeping 00034's per-connection service registry, which
is what the merge punted on reconciling:

- ServiceRegistry::extend_from folds server-level services into a per-session
  registry by copying Arcs, so instances stay shared and only the lookup table
  is per-session.
- AppSessionStore is rebuilt around Arc<AppRegistry> + server_services.
  build_session does both halves of the reconciliation: server services are
  folded in FIRST, so the framework's per-connection AppContext and
  DownloadService always win. Reversing that order lets a
  with_service::<AppContext> hand one session another connection's id, and
  with it another connection's download URLs.
- create_session_for_app and navigate_session; navigation carries the
  connection id across the swap so download URLs minted earlier still resolve,
  and refuses unknown ids via AppRegistry::get rather than resolve.
- RustyServer::empty/with_app/with_service, and a ConnectParams ?appId= query
  on /ws. new() registers its root under AppIds::DEFAULT, so all 8 existing
  call sites and all 351 pre-existing tests are unchanged.
- The Navigate arm swaps the app and sends a full Refresh, since the old
  reconciler baseline no longer applies. It re-reads both event_sender and
  rebuild_notifier: *guard = fresh drops the old Runtime, so retaining either
  would post into a dead channel or park on a Notify nobody signals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers app resolution at connect time (explicit id, absent id, unknown id
falling back to the default), navigation (swap, reconciler reset, unknown id
holding position, same-id rebuild, cross-session isolation), the empty-registry
$error-not-found session, and service lifetime across a navigation.

Two are ordering guards for build_session's extend_from:
test_per_connection_services_win_over_server_level_ones registers a
server-level AppContext and asserts the session still sees its own connection
id. Flipping extend_from to run after the framework registrations reproduces
the documented failure (left: "server-wide", right: "conn-1"), confirming the
test has teeth rather than passing by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drives a real WebSocket client against the router: the ?appId= query (default,
explicit, unknown falling back), navigation (refresh not patches, back and
forth, unknown id leaving the socket usable, no cross-connection effect),
RustyServer::new still serving its root view, with_service resolving through
use_service over a live connection, a bare navigate with no state key, and
/health still answering on raw TCP.

test_events_dispatch_to_the_app_mounted_after_navigation is the guard for
re-reading event_sender after the swap: dropping that re-read makes the
post-navigation click time out against the old runtime's dead channel (5.02s),
so the test fails rather than passing by construction.

Binds 127.0.0.1:0 rather than going through serve_background, whose
bind_address these tests bypass; the production 0.0.0.0 opt-in is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both conflicts in `rusty/src/server/ws.rs` were additive on each side, so
both sides are kept:

- Imports: upstream's `DownloadPayload` (plan 00056, streamed downloads)
  alongside this branch's `AppSession`.
- `handle_socket` locals: upstream's leading-edge push debounce
  (`push_pending`/`next_push`, plan 00068) alongside this branch's `mut`
  on `rebuild_notify`, which a Navigate must re-read after the runtime is
  swapped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`origin/main` at df4e685 fails `cargo build --workspace`, verified on a pristine
detached checkout:

    error: couldn't read `rusty-server\src\bin\widget_harness.rs`:
           stream did not contain valid UTF-8
      --> rusty-server\src\bin\widget_harness.rs:376:73
    376 | /// ... there is no anchor widget <0x97> the

Introduced by 245c372 `[00124] Resolve merge conflicts with main`, which pasted
`DownloadsApp` twice - once correctly encoded, once with the em-dash mangled to a
lone cp1252 byte 0x97. The two blocks were byte-identical apart from that, and
`struct DownloadsApp` being defined twice is a hard error in its own right.

Three defects, all pre-existing in main and all masked by the file failing to
parse:

1. The duplicated `DownloadsApp` block (lines 372-409) - deleted, keeping the
   valid-UTF-8 copy.
2. `let server = RustyServer::new(...)` was unindented; rustfmt could not reach
   it while the file would not parse.
3. `tests::harness_services` did not register a `DownloadService`, so
   `all_widget_kinds_build_a_tree` panicked on the `downloads` variant added by
   00124. The test never ran on main, so nothing caught it.

This is outside plan 00070's stated scope, but its four gates cannot run against
a base that does not compile. Repaired minimally and to the smallest diff that
makes the workspace build, per the precedent in 5575a29
`[00037] Repair broken base: restore 7 harness views dropped by merge`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ServerSideAppRoutingRevertedByThe00034Merge

# Conflicts:
#	rusty-server/src/bin/widget_harness.rs
`main` is red at 0b13ab7: `shared::widget_names::every_widget_type_is_mapped`
and `widget_type_scan_finds_known_widgets` both fail. Verified on a pristine
`git archive` of origin/main with a separate CARGO_TARGET_DIR, so it is not a
local or incremental-build artefact, and it is not caused by this plan's
routing changes.

Plan 00093's 66128e0 converted 12 widgets to `#[derive(Widget)]`, which emits
the wire type instead of spelling it as a `"type": "..."` literal in a
hand-written `to_json`. The scan only matched the literal, so those 12 widgets
became invisible to it: 26 types found where 38 were expected, and the negative
control caught 'button' going missing.

Teach the scan the second convention: a `#[derive(..Widget..)]` line means the
name comes from the following struct, either from `#[widget(type = "...")]` or
from `to_snake_case` of the struct name (mirroring the macro's own function).

Verified the repair discriminates rather than merely passing: the scan now
finds exactly 38 types, identical to `ivy_widget`'s 38 match arms, and adding
`#[widget(type = "new_gadget")]` to Skeleton makes the test fail with
"1 widget type(s) have no ivy_widget entry" instead of passing vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cargo clippy --workspace --all-targets --no-default-features -- -D warnings`
fails on `main` at 0b13ab7 with two `clippy::question_mark` errors in
rusty-macros/src/lib.rs (lines 115 and 129 there). Verified on a pristine
`git archive` of origin/main with a separate CARGO_TARGET_DIR, so it is
inherited, not caused by this plan.

Both sites are `match expr { Ok(v) => v, Err(err) => return Err(err) }`, which
is what `?` means. Applied clippy's own suggestion. The first needs an explicit
`collect::<syn::Result<Vec<_>>>()` turbofish: with the match gone there is
nothing left to infer the intermediate collection from, and a bare `collect()?`
fails with E0283.

This is a code-generation change, so the whole suite was re-run rather than
just clippy: 432 lib tests and the 51 rusty-macros trybuild UI tests -- which
assert the derive's exact emitted diagnostics -- all still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cargo clippy --workspace --all-targets --no-default-features -- -D warnings`
fails with `clippy::empty_line_after_doc_comments` at
rusty/src/shared/types.rs:77. Inherited from `main` at e6c4398, not caused by
this plan: reproduced on a pristine `git archive` of origin/main with only the
rusty-macros question_mark fix applied, which is what lets clippy get far
enough to reach `rusty` at all. That masking is why it had to be found second.

Plan 00093's d26f6fd added `pub fn size_css(&Option<Size>) -> Option<String>`
with a three-line doc comment. Plan 00117's merge (86d9396) deleted the
function but left the comment, which then documented the unrelated `Density`
enum four lines below it.

Removing the comment rather than restoring the function: 00117 replaced
`Size`'s untagged derive with a custom `Serialize` that emits `to_css()`
directly, so the wire format is already lossless and no widget uses
`#[prop(with = ...)]` for size any more. The comment's own premise -- "`Size`'s
derived `Serialize` is `untagged` and therefore lossy" -- is now false.

Three stale references to `size_css` remain in comments and one ignored doc
example; recorded as a recommendation rather than edited here, since they are
cosmetic and belong to other plans' files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sh ./scripts/test-precommit-hook.sh` -- CI's "Pre-commit hook harness" step in
the `build` job -- reports FAILURES: 3 on `main` at e6c4398. Verified identically
on a pristine `git archive` of origin/main, and CI is red on main for the last
five merges, so this is inherited rather than caused by this plan.

Two real defects in src/frontend/.husky/pre-commit, both in calls the harness
was written to check:

1. The `precommit-rust-gates.sh` call had no `[ -f ]` guard, so any checkout
   predating the script (the harness counts 123 such commits) fails every commit
   with 127 rather than skipping the gate. The fmt call two lines above already
   had this guard; the newer call did not copy it.

2. Neither call had `|| exit 1`. Under husky's `sh -e` wrapper a bare call
   propagates, but a developer running `sh .husky/pre-commit` by hand gets no
   `-e`, and there a non-final command's exit status is discarded -- so a
   formatting failure silently let the commit through. The fmt call's own comment
   already claimed this propagation ("the -e is load-bearing"); it was only true
   for the husky path.

All 13 cases now pass. Also re-ran the neighbouring harnesses to be sure the
edit did not shift their assumptions: test-precommit-gates.sh 10/10 and
test-check-test-inventory.sh 6/6.

Committed with --no-verify: the hook under repair is the one that would run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rorychatt rorychatt self-assigned this Aug 2, 2026
@rorychatt
rorychatt merged commit c6f233b into main Aug 2, 2026
@rorychatt
rorychatt deleted the tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge branch August 2, 2026 10:49
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.

1 participant