Skip to content

server: stop requests at the wall's ends, keep placeholder distances out of them, name things without ids - #122

Open
AidenJohnston wants to merge 15 commits into
t3/serverfrom
t3/41-server-requests-text
Open

AidenJohnston wants to merge 15 commits into
t3/serverfrom
t3/41-server-requests-text

Conversation

@AidenJohnston

@AidenJohnston AidenJohnston commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #78
Fixes #74
Part of #75 (the server half; the app's span copy is a separate unit)

Base branch: t3/server, merged in at 3baa338 (52a7fd5, ee882e4, then 7046c93). This PR doesn't depend on another open PR.

Base owner's OK needed: 7f5ddc4 and 2f93263 change Scene.view_to_cover, and f6f6089 changes Scene.coverable. Both are in server/scene.py, which t3/server owns (see "Files touched").

What changed

In the build 4.1 field runs, the upload's "Still needed" list asked homeowners for views nobody could give, and the result screen showed internal ids and the team's demo-rules notice.

Requests stop at the wall's ends (#78)

  • Before: every band could be asked for past a "Something blocks it" (limit) end. A spot beside that end asked for wall up to 26 ft 2 in on a wall that ends at 19 ft 9 in, and the app drops any request that reaches past an end.
  • Now, ground: only the ground is asked for past a limit end. Its request says what the view looks for, taken from the checks whose views reach past the end: "point the camera there from the end to check for an AC unit".
  • Now, wall, facing and overhead: these requests stop at the chain's ends.
  • A check that needs one of those bands past a limit end asks for no views at all. Its views neither size a request nor appear in one. Examples are the gas and opening clearances whose radius crosses the end, or the gap in front and headroom when the wall's error carries them past it. That stretch still counts as unseen (unobserved_wall_lines is unclipped, see "Why"), so no capture could settle the check. The summary names it for a person from the first answer: "More views are needed … ; a person also needs to check distance from gas equipment".
  • The rule: a request names only the checks that capturing it settles (settled_by_views in solver.py). The app relies on this: ResultReading.evidenceIndex(settling:) treats checks as the checks a request settles. Earlier revisions of this PR clipped the request at the end but still named those checks, which broke that rule (the Codex review of c8d3c9d).
  • Unexplored ends: a past_end request in a manual review now names, in checks, the checks whose unseen area lies past its end (walk_on_settles). Walking on shows that area, and the next answer asks for the views. The summary counts a check as "more views needed" only if some request names it. An unexplored end beyond cable reach raises no past_end request, so a check that needs anything past it asks for no views and goes to a person from the first answer (open_ends, settled_by_views).

Placeholder distances don't size the requests (#75, server half)

  • Before: _missing_json merged the unseen views of every UNSURE check in a band and asked for the deepest. The pool's 10 ft and the driveway's 5 ft therefore set the ground request (13 to 16 ft out in the three runs). Both values are placeholder: true, with no public source.
  • Now: check_clearance sets Check.asks_for_views = False when its distance is a placeholder still in effect. Those checks ask for no views. They still read the area and stay UNSURE until it is seen, and the summary names them for a person.
  • Private rules: a value the private rules set counts as real, even though the public placeholder: true merges under it (Solver._placeholder checks private_keys). Under private rules, the pool and driveway requests come back.

Homeowner text names things, not ids (#74)

  • subject still holds the id (objects[1] ac), because siteplan keys geometry by it. Reasons name the thing and roughly where it is, for example "The AC unit about 13 ft right of the meter is 3 ft 1 in (± 0 ft 2 in) from the battery…" or "the stretch with no wall about 3 ft right of the meter". A driveway is "the driveway" with no position.
  • _a() picks the article ("an AC unit"). Surfaces that are materials take none ("stands on gravel", but "stands on a driveway"). Marks set aside past an end use the same nouns ("The electrical box marked…").
  • Check labels keep their initialisms inside a sentence ("distance from AC units"). The reject summary names checks by label instead of by id.
  • summary no longer ends with policy.notice. The notice is still in policy.notice.

Ground request depth (found while merging)

  • What happened: after the merge, test_coderabbit_review's property found that capturing a small ground request exactly could leave the gas and AC clearances unseen. view_to_cover (5682a67 on t3/server) bisected until the unseen area left was under 1e-9 sq ft, which left a speck inside the check's radius.
  • First fix, not enough: 7f5ddc4 tightened the bound to 1e-12. A verifier's sweep still found a sliver 4.25e-7 ft inside a radius.
  • Final fix: 2f93263 passes the check's own test (Solver._covered over what a view can show) into view_to_cover, so the search stops exactly where the check is settled. As a result, requests come out up to 0.005 ft (SEEN_GROWTH_FT + SEAM_FT) shallower than the depth the check needs, because seen ground grows by that much.
  • Follow-up: ad7d5eb keeps that test to areas. test_s4_round's property found an overlay that left line fragments.

Why

  • Which placeholder checks stop asking. The unit brief said to exclude every check whose rule is a placeholder. Under the public rules that would also include headroom (6.5 ft) and wall_equipment_above (0 ft). headroom is the only check that asks for the overhead band, which the app answers with a tilt-up, so dropping it would switch that flow off under the demo rules. wall_equipment_above asks only for views the gas check already asks for. So the change is limited to the clearance distances Ground requests driven by placeholder rules ask for 14–16 ft of ground nobody can show, and the copy misdescribes the span #75 is about (pool and driveway). If headroom should stop asking too, that is a one-line change and a team decision.
  • unobserved no longer means "more photos fix it". A check is unobserved whenever the area it reads wasn't seen, but views settle it only when a request names it. result.schema.json's unsure_cause and missing_evidence descriptions and the README now say so. No enum value or key was added.
  • Open question from "Still needed" asks for wall past a limit end (wall to 26 ft 2 in; the wall ends at 19 ft 9 in) #78: should unobserved_wall_lines also stop at a limit end? Clipping it would let the checks above pass without anyone seeing that stretch, so this PR leaves it unclipped and sends them to a person. That's a team decision.

Verification

Commands run in server/ at 7046c93:

  • uv run ruff check .: "All checks passed!"
  • uv run ruff format --check .: "38 files already formatted"
  • CI=1 uv run pytest -q: 424 passed, 1 xfailed, 2 failed. The 2 failures are the known Windows-only tests: test_zip_entry_escaping_the_bundle_is_400[..\\x] and test_deeply_nested_input_is_a_scene_error. GitHub's Linux Server checks job runs them.
  • The capture properties with more examples (see the PR comment for counts), and a 303-scene sweep around the pinned ground-depth scene: no named check stayed unseen after capturing exactly what was asked.

New tests (all synthetic scenes):

Updated tests:

  • test_capture_contract and test_s4_round pin pool-sized requests under rules with real pool and driveway values. The depth bounds allow SEEN_GROWTH_FT + SEAM_FT.
  • test_coderabbit_review and test_final_review count only checks for which settled_by_views is true.
  • test_policy_and_auth, test_s4_round and test_field_run_2 match the new wording.

Not run, or only CI or a device can verify:

  • private/tests isn't on this machine.
  • On a phone, beside a limit end:
    • The wall request should be capturable.
    • The checks that need the wall past the end should go to a person.
    • The past_end item now carries checks.
  • policy.notice no longer appears in the summary. On beta the app reads the notice from policy.notice (rulesNotice), and summaryWithoutNotice is a no-op.

App-side follow-ups (not in this PR)

  • "One more photo would settle this". beta's PlacementCheck.needsPerson is false for every unobserved check, so the result screen shows that line under a summary that says a person needs to check. The fix is needsPerson = unsureCause != .unobserved || evidenceIndex(settling: id) == nil.
  • Vendored schema. The app keeps a copy of result.schema.json with a hash (JSONSchemaValidatorTests). The copy needs refreshing once this lands on t3/server.

Files touched

  • server/solver.py:
    • Changed: Check.asks_for_views, Check.past_ends_later, Check.all_missing (cached), Solver._placeholder, Solver._items (with the base's in-plan flag), check_ground, check_clearance, check_along_wall, check_measured, _report_range, route_for, evaluate, Solver._missing, _missing_json, _past_end_hint, and solve (summary, past_end checks, the objects_not_used message).
    • New: open_ends, settled_by_views, walk_on_settles, _sides_past, Solver._unseen_parts, Solver._past_unexplored, _noun, _a, _surface, _about, _describe, _sentence, _phrase, _either.
  • server/scene.py (base-owned): Scene.coverable (docstring; the wall band keeps the base's stretch past a limit end so that settled_by_views can see it) and Scene.view_to_cover (takes the check's test instead of an area bound).
  • server/schemas/result.schema.json: the descriptions of missing_evidence and unsure_cause only.
  • server/README.md: limit-end requests, placeholder requests, what settles a check, subject compared with homeowner text, and where the notice appears.
  • Tests:
    • New: server/tests/test_requests.py, server/tests/test_homeowner_text.py.
    • Updated: test_capture_contract.py, test_coderabbit_review.py, test_final_review.py, test_policy_and_auth.py, test_s4_round.py, test_field_run_2.py.

No change to rules.yaml, reason codes or the error model. The schema's shape is unchanged: past_end requests may already carry checks.

Dependencies

Risk

  • Checks go to a person where they didn't before. Under the public rules, pool and driveway can only be settled by a person unless the scan happens to show the ground far enough out. Checks that need the wall past a limit end now go to a person as well. In both cases the old behaviour asked for a view nobody could give, or one that settled nothing.
  • Clients that read summary for the demo notice will no longer find it there. None in this repo do.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • N/A: before/after images. The server has no UI.
  • N/A: video. There is no motion or interaction change.

🤖 Generated with Claude Code

AidenJohnston and others added 3 commits September 27, 2026 00:33
Scene.coverable let every band reach the extension piece past a limit
end, so a spot beside a "Something blocks it" end asked for wall past
the end (to 26 ft 2 in on a wall that ends at 19 ft 9 in), which the
app can never show and drops. Only the ground is coverable past a limit
end now. _missing_json also clips the views checks take straight from
the 1D bands (the wall above the battery, the gap in front, the space
overhead) to the chain's ends. A check whose radius crosses a limit end
stays UNSURE for a person.

The ground request past a limit end now says what the view looks for
("to check for an AC unit or a pool"), from the checks whose views
reach past the end.

Fixes #78.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
_missing_json merged every UNSURE check's unseen views per band and
asked for the deepest, so the pool's 10 ft and the driveway's 5 ft,
both placeholders with no public source, set the ground request: 14-16
ft out over the whole wall in every 4.1 field run, which nobody could
show.

A clearance whose distance is a placeholder still in effect now asks
for no view (Check.asks_for_views). It still reads the area and stays
UNSURE until it is seen, and the summary names it for a person rather
than counting it among the checks more views settle. A value the
private rules set counts as real even though the public flag merges
under it, so the requests come back under those rules.

Scoped to check_clearance (pool, driveway). headroom and
wall_equipment_above are placeholders too but keep asking: headroom is
the only check that asks for the overhead band, which the app answers
with a tilt-up, and wall_equipment_above's views lie inside the gas
check's.

The tests that pinned a pool-sized request now use real pool and
driveway values.

Part of #75.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reasons were built from the ids in `subject`: "objects[1] ac is 3 ft 0 in
(± 6 ft 0 in) from the battery", "facing[1] ... may or may not be over
the battery", "ground[0] drive", and the route's blockers. The unseen
reason hardcoded "a {noun}" ("a AC unit"), and solve() appended the
demo-rules notice to the summary the app shows.

- `subject` stays the id (siteplan keys geometry by it). Reasons name
  the thing by what it is and roughly where: "the AC unit about 13 ft
  right of the meter", "what faces the wall about 6 ft right of the
  meter", "the stretch with no wall about 3 ft right of the meter",
  "the driveway". check_clearance's items carry the description.
- _a() picks the article ("an AC unit", "an electrical box").
- Check labels keep their initialisms inside a sentence ("distance from
  AC units"), and the reject summary names checks by label, not id.
- The summary no longer ends with policy.notice; the notice stays in
  policy.notice for the team.

test_homeowner_text scans every answer and every weighed position's
reasons over the examples, scenes that reach each kind of reason, and
random cluttered scenes: no id, no underscore, no "a AC", no notice in
the summary.

Fixes #74.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7edc0061-342e-4d0a-8c5f-696973cbcdce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:L 100-499 effective changed lines. label Sep 27, 2026

@AidenJohnston AidenJohnston left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Adversarial review of draft #122 at a22c2d4 (U8: #78, #74, server half of #75).

What I ran

  • PR head, detached checkout: ruff check clean, ruff format --check clean, pytest -q gave 372 passed and 2 failed. The 2 failures are the known Windows-only ones.
  • The PR merged over the current t3/server tip (9176125). t3/server has moved on 2 commits since the 7133283 base the body names. The merge is clean, and pytest -q on the merged tree gave 378 passed and the same 2 Windows-only failures.
  • gh pr checks 122: Server checks pass, Label PR size passes, CodeRabbit skipped (draft). No iOS files changed, so the missing iOS run doesn't matter here.
  • A two-round probe on the PR's own spot_beside_a_limit_end() fixture: solve, add exactly the coverage.observed entries that missing_evidence asks for, then solve again. Result below.

Must fix

  1. server/solver.py:1729-1745: after #78, the summary still asks for "more views" for checks that no request can settle. The PR clips wall, facing and overhead views at the end marks and leaves unobserved_wall_lines unclipped. So a wall check whose radius crosses a limit end stays UNSURE with unsure_cause: "unobserved" and asks_for_views == True, but _missing_json drops the part past the end. Probe, round 2, after the homeowner supplied every requested view:

    • missing_evidence is []
    • gas_clearance and opening_clearance are still unobserved
    • summary: "More views are needed around the best spot, 10 ft 3 in right of the meter: 2 checks depend on areas the scan did not see; a person also needs to check distance from the driveway; distance from a pool."

    So the homeowner is told to capture more with nothing on the "Still needed" list. The #49/U9 gap loop reaches this state as soon as it captures the newly capturable clipped wall item, which is the flow this PR unblocks. It is the same inconsistency the PR fixed for the placeholder checks (#75), and it contradicts the README and commit text ("a check whose radius crosses a limit end stays UNSURE for a person"): the summary doesn't name those checks for a person.
    Fix: count a check as "more views needed" only if it appears in a band request that is actually emitted. For example, asked = {i for m in missing if m["kind"] == "band" for i in m["checks"]} and unseen = [c for c in best.checks if c.outcome == UNSURE and c.unsure_cause == "unobserved" and c.id in asked], with every other UNSURE check going into the "a person also needs to check" list. That also covers asks_for_views. Add a round-2 test to tests/test_requests.py: supply spot_beside_a_limit_end()'s requests, then assert missing_evidence == [] and that the summary starts with "A person needs to check".

Should fix

  1. server/solver.py:449: _a() on a mass noun. "The footprint stands on {_a(_noun(g.type))}" becomes "stands on a gravel", "a mulch" or "a concrete" whenever a rules file leaves those surfaces out of ground.allowed. Under the public rules only drive and deck fail, so the tests don't reach it, but a private ground.allowed can. Before this PR the text read "stands on gravel". Only count nouns should get an article; keep the bare noun for concrete, gravel and mulch.
  2. server/solver.py:1830: objects_not_used messages still use the raw type: "The ac marked 3 ft 0 in right of the meter…" and "The elec box marked…". test_homeowner_text scans these messages, but it only rejects underscores, ids and "a AC", so both pass. The new README line says every homeowner message names things by what they are. Use _noun(kind) here. The app doesn't show this field today, so this is not a blocker.
  3. server/solver.py:1578 with _either: the hint can read "to check for a gas meter or pipe or an AC unit" (probe, round 1). The double "or" reads badly. Either give gas the hint noun "a gas meter" or join with commas.

Notes

  • Scope deviation, surfaced properly. The brief said to drop the views of every check on a placeholder rule. The PR limits that to check_clearance (pool and driveway), so headroom and wall_equipment_above keep asking. The reason given (headroom is the only check that asks for the overhead band, which the app's tilt-up answers) is sound, and the body calls it a team decision. Nothing else looks like a product decision made silently. Dropping the notice from summary follows the issue's fix direction.
  • Solver._placeholder agrees with overridden_keys and _placeholders_left for a cited value, including the stricter group handling in 6200098. The private-file parametrisation of test_a_real_pool_rule_brings_its_request_back pins it.
  • Private tests. The Base-derived tests in private/tests are not on this machine, so I couldn't run them. They may assert on the old reason text (ids) or on pool-sized ground requests. Run uv run pytest ../private/tests -q before this leaves draft.
  • Clipped wall item vs the app. The wall item ends at scene.s_max, rounded outward to 1e-6 ft. GapPlanner.reachesPastEnd allows 0.01 ft, so the app should keep the item. That still needs a device check, and the body already lists it as device-only.
  • subject still holds the id, and siteplan._dimensions keys geometry by it. The new "what" key on route crossings isn't serialized, and _route_json picks its fields one by one, so there's no schema change. I grepped every call site of _report_range and check_clearance, and all are updated.
  • Public-repo check: diff, body and the three commit messages are clean. They contain no names, meter numbers or serials, addresses, private values or media. The distances quoted come from the public issues.
  • PR body: it has Fixes #78, Fixes #74 and Part of #75, the base, verified vs CI/device-only, files touched, dependencies and the attribution line. The base SHA (7133283) is now 2 commits behind the t3/server tip. The merge is clean and passes (see above), but consider rebasing so the Linux CI runs on the combined code.

Verdict: blocked (one must-fix: the summary says "More views are needed" with no request to settle it, in the limit-end flow this PR opens up)

🤖 Generated with Claude Code

After the end clipping (#78), a check whose radius crosses a limit end
stays unseen once every emitted request is met, so the summary kept
saying "More views are needed" with nothing left to show. Only checks
named in an emitted band request now count as needing views; every
other unsure check is named for a person.

Also from the review: ground types that are materials read without an
article ("stands on gravel"), set-aside marks use the homeowner noun,
and the past-end hint names "a gas meter" so it reads with one "or".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Addressed the review of a22c2d4 in c8d3c9d.

Must fix

  1. Summary asks for "more views" that no request can give (solve, unsure branch). Fixed as suggested. solve now builds requested = {i for m in missing if m["kind"] == "band" for i in m["checks"]}, and a check counts toward "More views are needed…" only if it is UNSURE, unobserved and in requested. Every other UNSURE check goes to the "a person also needs to check" list, or to "A person needs to check the best spot…" when no check is requested. This covers both the placeholder checks (Ground requests driven by placeholder rules ask for 14–16 ft of ground nobody can show, and the copy misdescribes the span #75) and the checks whose remaining views lie past a limit end ("Still needed" asks for wall past a limit end (wall to 26 ft 2 in; the wall ends at 19 ft 9 in) #78), and it replaces the asks_for_views test. New test test_once_every_request_is_met_the_rest_goes_to_a_person in tests/test_requests.py: it solves spot_beside_a_limit_end(), adds exactly the band requests as coverage.observed entries (with out_ft where given), solves again, and asserts missing_evidence == [], a summary starting with "A person needs to check", and at least one check still unobserved.

Should fix

  1. _a() on a mass noun. Fixed. The new _surface(kind) leaves concrete, gravel and mulch bare ("stands on gravel") and gives count nouns an article ("a driveway", "a deck", "a lawn"). It's pinned by test_a_surface_takes_an_article_only_if_it_is_a_thing.
  2. objects_not_used uses the raw type. Fixed: the message uses _noun(kind) ("The electrical box marked…"), pinned by test_a_mark_set_aside_is_named_by_what_it_is. The object field keeps the id, as subject does.
  3. "a gas meter or pipe or an AC unit". Fixed. The past-end hint now names the gas check's target as "a gas meter", which is the only object type the gas check reads (o.type == "gas_meter"). _either already joins with commas, so the hint has a single "or". The existing ground-hint test now asserts one " or ". The unobserved reason's noun ("a gas meter or pipe") is unchanged, because it reads fine on its own.

Notes

  • Rebase onto the t3/server tip: not done. It would need a force-push, and you found the merge clean and passing. It can happen at merge time if the team wants Linux CI on the combined tree.
  • Private tests: private/tests isn't on this machine, so they still need a run before this leaves draft.

Checks (in server/)

  • ruff check .: all checks passed.
  • ruff format --check .: 33 files already formatted.
  • pytest -q: 377 passed, 3 failed. Two are the known Windows-only failures. The third, test_smoke_script.py::test_a_keyed_request_does_not_follow_a_redirect, isn't touched here and passed on a rerun (tests/test_smoke_script.py: 8 passed).

I updated the PR body's #78 description, verification, tests and files-touched lines to match.

🤖 Generated with Claude Code

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Verdict: ready

  • The must-fix is addressed in c8d3c9d. solve now counts a check toward "More views are needed" only if an emitted band request names it (requested = {i for m in missing if m["kind"] == "band" ...}). Every other UNSURE check, including unseen ones whose remaining views lie past an end mark, goes to "A person needs to check". The homeowner is no longer asked for views that no request lists.
  • The new round-2 test test_once_every_request_is_met_the_rest_goes_to_a_person covers the probe that failed before. It supplies exactly the requests from spot_beside_a_limit_end(), then asserts missing_evidence == [], a summary that starts with "A person needs to check", and that unobserved checks are still there.
  • The other changes in the new commits are small wording fixes, each with a test: the ground-surface article (_surface, e.g. "gravel", not "a gravel"), a single "or" in past-end ground requests, and set-aside marks named with _noun.
  • I ran ruff check, ruff format --check and pytest on the head commit locally: 378 passed. The only 2 failures are the known Windows-only baseline tests. CI's server checks pass.

🤖 Generated with Claude Code

@AidenJohnston
AidenJohnston marked this pull request as ready for review September 27, 2026 07:54
@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add house-scanning-master to your team's repositories.

@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: c8d3c9d5d4

ℹ️ 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 server/solver.py
Comment on lines +1536 to +1539
a, b = max(view.a, scene.s_min), min(view.b, scene.s_max)
if b - a < COVERAGE_TOLERANCE_FT:
continue
view = replace(view, a=a, b=b)

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 the request-settles-check contract

When a clearance view crosses a limit end, clipping it here still leaves the check ID attached to the shortened request, but submitting exactly that request cannot settle the check because the unseen portion beyond the end remains. This contradicts schemas/result.schema.json, which guarantees that supplying a band request's span_ft and out_ft settles it, and can break clients that use missing_evidence as a recapture protocol. Either distinguish this as partial evidence requiring manual review or avoid advertising the clipped request as settling that check.

Useful? React with 👍 / 👎.

@SamGu-NRX

Copy link
Copy Markdown
Owner

@greptileai

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Changes how ground requests are sized and which checks ask for views.

The PR should not merge until a clipped request can no longer name a check that remains unobserved after capture.

Fix All in Claude CodeFindings

  1. P1 Clipped view repeats indefinitely ▶
Fix with agent prompt
### Issue 1
server/solver.py:1874
When wall-position error makes a facing or overhead check reach less than 0.01 ft past a limit end, the check is still treated as one a view can settle. This line clips the request at the wall end, leaving that short stretch unseen. Capturing the requested view can therefore produce the same request again instead of sending the check to a person.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

The PR changes how the server requests views near wall ends and for placeholder clearances, and replaces internal IDs in homeowner-facing text. It also changes ground-request depth calculation and updates the contract documentation and tests. One limit-end edge case still produces a request that cannot settle its named check.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Measured span extends slightly past limit end] --> B[Check treated as view-settleable]
  B --> C[Request clipped to wall end]
  C --> D[Capture leaves short tail unseen]
  D --> B
Loading

Reviews (1) · Last reviewed commit: "Merge t3/server into t3/41-server-reques..."

AidenJohnston and others added 3 commits September 27, 2026 06:11
Conflicts:
- scene.py coverable: the ground keeps the base's coverable_span() and house() difference;
  the wall bands keep this branch's stop at both end marks (#78).
- solver.py route_for: the base's route error over every wall the cable runs along, with this
  branch's maybe_blockers holding crossings (their homeowner name) rather than ids.
- test_policy_and_auth: the unsure example asks only for past_end; both comments kept.

After the merge the ground request is the smallest depth that covers the region (5682a67),
which the seen ground's tolerance can put a hair under D + 3, so the two D + 3 bounds allow
COVERAGE_TOLERANCE_FT. The base's one-round property now counts only checks that ask for
views, as test_coderabbit_review already did: placeholder distances ask for none (#75).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
view_to_cover bisects to the depth at which the unseen area left falls to its bound, so a
speck just under the bound always remains. At 1e-9 sq ft one sat where the gas and AC
clearances' 3 ft arc meets the view's edge, inside their radius: capturing exactly the
request left both unseen, with nothing more to ask for. test_coderabbit_review's property
found it after the merge, once the placeholder pool and driveway stopped sizing the request
(#75) and the request shrank to the gas and AC reach. The bound is now 1e-12 sq ft (the
request there grows from 3.362958 to 3.363014 ft). The scene is pinned in test_requests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After #78 a request for the wall, the gap in front of it or the space overhead stopped at a
limit end but still named the check whose radius crossed the end. That stretch still counts
as unseen, so capturing exactly the request left the check UNSURE. The app reads a request's
`checks` as the checks it settles (ResultReading.evidenceIndex(settling:) on beta), and
result.schema.json promises that supplying a request settles it. Beside a limit end the gas
and opening clearances were named on the wall request, and facing_gap and headroom on theirs,
and all four stayed unseen after the capture (Codex review of c8d3c9d).

settled_by_views() now says whether views can settle a check: not one on a placeholder
distance (#75), nor one whose view of the wall, gap or overhead reaches past a limit end by
more than the coverage tolerance. Such a check asks for none of its views, so they neither
size nor name a request, and the summary names it for a person from the first answer.
Scene.coverable keeps the base's wall stretch past a limit end so that view shows up to be
tested; views past an unexplored end are still clipped (walking on settles those).

test_requests pins it beside a limit end for the wall and for facing and overhead: those
checks aren't named, the wall request stays inside the end, and capturing every request
leaves no named check unseen. The one-"or" hint is tested on _either directly, since the gas
clearance no longer asks there. The one-round properties in test_final_review and
test_coderabbit_review count checks by settled_by_views.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Updated for review at f6f6089. t3/server is merged in, the Codex item is fixed, and server CI passes.

Merge of t3/server (52a7fd5)

t3/server was 8 commits ahead (6200098 to a726199). Three files conflicted:

  • scene.py coverable: the ground band keeps the base's coverable_span() and house() difference. The wall bands keep this branch's stop at both end marks. f6f6089 changes the wall band again, see below.
  • solver.py route_for: the route error now uses the base's formula, which adds every wall segment the cable runs along (5a4b4f4). maybe_blockers still holds this branch's crossings (with their homeowner names), not ids.
  • test_policy_and_auth.py: both sides assert the unsure example asks only for past_end. I kept both comments.

Two tests needed changes after the merge:

Review items

  • Codex P2 on solver.py:1539, "Preserve the request-settles-check contract": fixed in f6f6089. Probing it confirmed the problem. Beside a limit end, the clipped wall request named gas_clearance and opening_clearance, and the facing and overhead requests named facing_gap and headroom. After capturing exactly those requests, all four stayed unobserved, because the stretch past the end still counts as unseen. The app on beta reads a request's checks as the checks it settles (ResultReading.evidenceIndex(settling:)), so it would have shown those checks as capturable.
    • The new settled_by_views() returns false for a check on a placeholder distance, and for a check whose wall, facing or overhead view reaches past a limit end by more than the tolerance.
    • Such a check asks for no views. Its views don't set a request's size and the check isn't listed on one. The summary names it for a person from round 1.
    • Views past an unexplored end are still clipped, because walking on settles those.
    • Tests in test_requests: those checks are not listed on any request, and capturing every request leaves no listed check unseen, for both a wall case and a facing/overhead case. Both failed before the fix. The one-"or" hint is now tested on _either directly, because the gas check no longer asks for views there.
  • The one-round property found a base bug, fixed in 7f5ddc4. A random run of test_coderabbit_review found a scene where the gas and AC checks stayed unseen after the ground request was captured exactly. The cause is in the base: view_to_cover bisects until the unseen area is under 1e-9 sq ft, and a speck just under that bound was left inside the 3 ft radius. Once the placeholder pool stopped sizing the request, the request was small enough to expose it. The bound is now 1e-12 sq ft, and that request grows from 3.362958 to 3.363014 ft. The scene is pinned in test_requests. This change is in scene.py, which t3/server owns, so it's worth a look from whoever looks after the base.
  • Sam's review of a22c2d4 was addressed in c8d3c9d (Sam re-reviewed and said ready). Greptile (5/5) left no inline items.

Behaviour change to note

The PR body's "The wall request is clipped to the end, not dropped" no longer holds for checks whose view crosses a limit end. Those now go to a person, and the wall request covers only what the other checks need (in the fixture it ends at 11.58 ft, not at the 12 ft end). Response shape is unchanged. result.schema.json isn't touched, and every key the app decodes on beta is still there.

Checks

  • Local, in server/: ruff check passes and ruff format --check reports 37 files formatted. CI=1 pytest -q gave 413 passed, 1 xfailed, and 2 failed. The 2 failures are the known Windows-only tests (..\x zip entry and deep nesting). The property tests also passed three times under random seeds.
  • GitHub: Server checks pass, https://github.com/SamGu-NRX/house-scanning-master/actions/runs/36315515770. No iOS files changed.
  • private/tests isn't on this machine, so it still hasn't been run.

🤖 Generated with Claude Code

AidenJohnston and others added 5 commits September 27, 2026 06:47
7f5ddc4 bounded the unseen area left at 1e-12 sq ft instead of 1e-9. That moved the speck
rather than removing it: a verifier's sweep still found a sliver 4.25e-7 ft inside a radius,
against the checks' 1e-6 ft tolerance. view_to_cover now takes the check's own test.
_missing passes one that asks Solver._covered whether anything the check reads there, that a
view can show, is left strictly within its radius. The bisection then stops exactly where
the check is settled. The area bound is gone.

Requests come out up to SEEN_GROWTH_FT + SEAM_FT (0.005 ft) shallower than the depth a check
needs, as seen ground grows by that. The depth bounds in test_requests and
test_capture_contract allow that and no more (before: the full 0.01 ft tolerance, and exact
D + 10 for the pool). The scene pinned in 7f5ddc4 still passes, and a sweep of 303 gap
positions around it leaves no named check unseen after the capture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ests leave

Beside an unexplored end, round 1 named the AC, gas and opening clearances on band requests.
After capturing exactly those, all three were still unseen: part of what they read lies past
the end, where only walking on (the past_end request) shows it. Round 2 had only the past_end
request, and the summary sent the three to a person ("A person needs to check..."), because
c8d3c9d counted only checks named on band requests. On a726199 it read "More views are
needed... 5 checks depend".

- walk_on_settles() finds the unexplored ends past which a check's unseen area lies. That is
  a wall, gap or overhead view reaching past the end, or ground or wall within the radius
  that Scene.coverable leaves out (Solver._past_unexplored, deferred like missing_later).
- A past_end request in a manual review now lists those checks in `checks`, which the schema
  already allows. solve() counts a check as needing more views when any request names it.
  So the contract holds for band requests together with walking on, and the app can find
  the request that settles a check the same way for both kinds.
- _missing skips a part whose unseen area within the check's exact radius is empty. With the
  depth search stopping at the check's own test (2f93263), the sliver between the radius and
  the polygon that circumscribes it gave a ground request 1e-6 ft deep in round 2.
- Check.all_missing() is cached, since the summary now reads it again.

test_capturing_the_requests_settles_every_check_they_name gains the unexplored variant: round 2
has only the past_end request, it names exactly the three checks still unseen, and the
summary counts them as needing more views.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… names it

result.schema.json described `unobserved` as "more photos fix it". That is no longer true.
Placeholder distances (#75) and checks that need the wall past a limit end (#78) are
unobserved but named by no request, and the summary sends them to a person. The unsure_cause
description now says that more views settle an unobserved check only when a missing_evidence
request names it, and that a check no request names needs a person. The missing_evidence
description and the README say that every band request, together with walking on for any
past_end request, settles every check the requests name. No enum value or key is added.

The app vendors result.schema.json and checks its hash (JSONSchemaValidatorTests on beta).
Its copy needs refreshing once this lands on t3/server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_s4_round's property found a scene where the check's test from 2f93263 raised "Overlay
input is mixed-dimension". Overlaying the unseen ground after a capture can leave line
fragments, which the next overlay refuses. The test and Solver._past_unexplored now keep only
the areas (polygonal), as view_to_cover already did. The scene is pinned as an @example on
test_a_captured_request_never_comes_back, and it fails without the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
t3/server gained da6d308 and 0ac96a3: the battery's errors in plan and along the walls, and
check_clearance items that say whether each was placed in plan. solver.py conflicted:

- check_clearance's items keep this branch's homeowner description and gain the base's in-plan
  flag: (subject, description, geometry, error, counts, in plan). _items adds o.in_plan, and
  the driveway item passes True with the base's along_err and this branch's noun.
- _missing keeps this branch's skip of parts no view can show within the radius, and the
  base's polygonal() on the ground region.
- route_for takes the base's error (the meter's one-sided, and the slide), with this
  branch's maybe_blockers holding crossings rather than ids.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Follow-up to the verifier's review of f6f6089. New head: ee882e4.

  • 2f93263: the ground-depth search now stops on the check's own test (Solver._covered), not an area bound. The 1e-12 bound from 7f5ddc4 is gone. The depth bounds in the tests now allow SEEN_GROWTH_FT + SEAM_FT (0.005 ft), not 0.01 ft.
  • 5213683: at an unexplored end, past_end requests now name the checks whose unseen area lies past that end (walk_on_settles), and the summary counts every check any request names. This fixes the round-2 summary, which said "A person needs to check…". The unexplored-end variant is added to test_capturing_the_requests_settles_every_check_they_name. _missing also skips parts with nothing within the radius, which removes a 1e-6 ft ground request.
  • 23a139c: the unobserved description in result.schema.json and the README now say that views settle a check only when a request names it. No enum value or key was added. The app's vendored schema copy needs refreshing once this lands on t3/server.
  • ad7d5eb: the depth test keeps only areas. It fixes a mixed-dimension GEOS error that test_s4_round's property found, and pins that scene as an @example.
  • ee882e4: merge of t3/server (da6d308, 0ac96a3). check_clearance items carry the base's in-plan flag next to this branch's descriptions.
  • PR body rewritten. 7f5ddc4, 2f93263 and f6f6089 change base-owned scene.py, so they need the base owner's OK.

Checks at ee882e4:

🤖 Generated with Claude Code

AidenJohnston and others added 3 commits September 27, 2026 07:58
…es there

An unexplored end out of cable reach raises no past_end request, but settled_by_views didn't
know that. With the wall ending 22 ft right of the meter and the lawn pad just before it,
round 1 named the AC, gas and opening clearances on band requests ("7 checks depend"). After
capturing exactly those, all three stayed unobserved with nothing naming them, and round 2
sent them to a person. A verifier hit this in 7 of 400 random scenes.

- open_ends(solver) gives the unexplored ends within reach, the ones past_end requests ask
  to walk on past; solve() uses it throughout.
- settled_by_views(chk, scene, reachable) is false when part of what the check reads lies past
  an unexplored end not in `reachable`. Such a check asks for no views and goes to a person
  from round 1. walk_on_settles is cached per check.
- The unobserved_area reason is built after the past_end requests, so a round whose only
  request is past_end still lists the checks it names, as the summary does.

test_capturing_the_requests_settles_every_check_they_name gains a beyond-reach variant, which
fails on ee882e4, and the unexplored variant checks the reason. The one-round properties pass
the reachable ends in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The missing_evidence description said walking on for a past_end request "settles" the checks
it names. It doesn't: it lets the next answer ask for their views. It now says that,
matching the README and the unsure_cause description, and both of those also list an
unexplored end beyond reach among the cases no request names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in 3baa338 (the route's meter error bounded exactly, a PASS held strictly). No
conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Follow-up to the round-2 verifier's review of ee882e4. New head: 7046c93.

  • Must-fix, 4014cb8: settled_by_views now takes the reachable open ends (open_ends(solver)). It returns false for a check with unseen area past an unexplored end that is beyond reach. Such a check asks for no views and goes to a person from round 1. test_capturing_the_requests_settles_every_check_they_name gains a beyond-reach variant (wall ending at 22 ft), which fails on ee882e4. The one-round properties pass the reachable ends in.
  • Nit 1, 4014cb8: the unobserved_area reason is now built after the past_end requests. A round whose only request is past_end lists the checks it names, and the unexplored variant asserts that.
  • Nit 2, dc375af: the schema's missing_evidence description now says the next answer asks for the views of the checks a past_end request names. The README and unsure_cause wording match it, and both now also list "an unexplored end beyond reach".
  • 7046c93: merge of t3/server (3baa338), no conflicts. PR body updated.

Checks at 7046c93:

🤖 Generated with Claude Code

@SamGu-NRX

Copy link
Copy Markdown
Owner

@greptileai review

Please review the current head 7046c93b467cc04f5da0d6a9861bca1bce8e7ef3 against this PR's actual base. Catch-up review requested by Sam after enabling BaseScanning in Greptile.

Comment thread server/solver.py
# Only the ground is asked for past an end mark: past an unexplored end only
# walking on settles the wall (a past_end request), and a view past a limit end
# by less than the tolerance is rounding.
a, b = max(view.a, scene.s_min), min(view.b, scene.s_max)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Clipped view repeats indefinitely When wall-position error makes a facing or overhead check reach less than 0.01 ft past a limit end, the check is still treated as one a view can settle. This line clips the request at the wall end, leaving that short stretch unseen. Capturing the requested view can therefore produce the same request again instead of sending the check to a person.

Prompt To Fix With AI
This is a comment left during a code review.
Path: server/solver.py
Line: 1874

Comment:
**Clipped view repeats indefinitely** When wall-position error makes a facing or overhead check reach less than 0.01 ft past a limit end, the check is still treated as one a view can settle. This line clips the request at the wall end, leaving that short stretch unseen. Capturing the requested view can therefore produce the same request again instead of sending the check to a person.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@AidenJohnston, verified at 7046c93b. The request generator does repeat. Full solve has not been shown to pick a candidate that hits it, though.

How it happens: wall error widens a candidate at [9, 11.583] to [8.578, 12.005], which crosses the right limit end at 12 ft by 0.005 ft. _sides_past uses COVERAGE_TOLERANCE_FT (0.01 ft, solver.py:1811-1814), so settled_by_views still treats facing and overhead as settleable. The request is then clipped to 12 ft (:1874). After the capture, seen_to still reports no depth in the last 0.005 ft (scene.py:378-384), so both checks stay unsure/unobserved (solver.py:900-908) and the same requests come back.

The reproduction below uses the repository's own test helpers on a synthetic scene, with nothing mocked. I extracted the tree with git archive 7046c93b server and ran it with the server's venv:

import sys
sys.path[:0] = ['<tree>/server', '<tree>/server/tests']
from helpers import golden_rules, pads_ground, parsed, run
from test_requests import facing_and_overhead_past_the_end, captured_exactly
from solver import Solver, evaluate_start, _missing_json, open_ends

loaded = golden_rules()
raw = facing_and_overhead_past_the_end()
raw['walls'][0]['plus_minus_ft'] = 0.4216666666666667   # 0.005 ft past the 12 ft limit
raw['ground'] = pads_ground([(9, 17)], hi=32)
for round_no in range(3):
    scene = parsed(raw, loaded)
    candidate = evaluate_start(scene, loaded, 9)
    requests = _missing_json(candidate, scene, open_ends(Solver(scene, loaded)))
    print(round_no + 1, [(m['band'], m['span_ft']) for m in requests if m.get('band') in ('facing', 'overhead')])
    raw = captured_exactly(raw, {'missing_evidence': requests})

Output: all three rounds request facing [8.578333, 12.0] and overhead [8.578333, 12.0] for facing_gap and headroom. Both checks stay unsure/unobserved after each round's requests are captured exactly. As controls, with error 0.415 (the span stops short of the end) the checks pass after one capture, and with 0.43 (more than 0.01 ft past) no view is requested. On the same scene, run(raw, loaded) chooses [8.995, 11.578333] and leaves no requests, so a homeowner-facing loop has not been shown, and P1 severity isn't established.

A possible fix: have settled_by_views reject a non-ground view that goes past a limit end by more than a numerical epsilon, and keep the 0.01 ft coverage tolerance where it is. Add this 0.005 ft tail as a fixed-candidate regression that asserts neither check is offered as capturable, and keep the no-tail control. I have not changed this branch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the detailed reproduction. This confirms the underlying correctness issue in _missing_json: a non-ground view can be clipped at a limit end while settled_by_views() still claims the check is capturable, so capturing the exact request leaves the same checks unseen and regenerates the request. The 0.005 ft case is especially useful because it is within COVERAGE_TOLERANCE_FT; the tolerance should remain useful for ordinary rounding, but it must not make a clipped wall/facing/overhead request appear settleable.

I agree that the reproduction establishes the generator defect, but not yet a homeowner-facing loop or P1 impact: it calls _missing_json on a fixed candidate, while solve() chooses the candidate itself. I would keep the finding as a correctness issue and add the fixed-candidate regression described. If the candidate can also be selected by solve(), an end-to-end assertion would establish the stronger impact. The fix should make settled_by_views() reject any non-ground missing view that crosses a limit end by any positive geometric tail, using a numerical epsilon rather than the 0.01 ft coverage tolerance.

This branch has not been deployed

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

Labels

size:L 100-499 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants