Skip to content

Finish two more fixes that merged incomplete - #402

Merged
dovvnloading merged 1 commit into
mainfrom
fix/incomplete-restore-and-doc-gates
Sep 4, 2026
Merged

Finish two more fixes that merged incomplete#402
dovvnloading merged 1 commit into
mainfrom
fix/incomplete-restore-and-doc-gates

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

Both shipped earlier today with the defect still reachable.

Budget caps could still restore unusable — #393

That commit claimed the plan's caps "keep falling back to their real defaults rather than zero, since a restored plan with max_steps=0 could never run again". They did not. _non_negative_int is max(0, int(value)) with a fallback only when int() raises, so every numeric route to zero went straight through — and the outer or default only catches Python-falsy values, so the string "0" and "-5" passed it too:

max_steps=-5    -> 0        max_steps='0'  -> 0
max_steps='-5'  -> 0        max_steps=0.5  -> 0
max_steps=0     -> 12       (the one case the test covered)

The harness restorer was worse — its local _int helper passed negatives through unchanged:

max_turns=-5    -> -5       max_context_tokens=-1 -> -1
max_turns='0'   ->  0

builder._spend_breach rejects the node on its first tick, so it lands on the canvas and can never run again.

The field-comment gate could not see the modules it was written to cover — #400

That PR corrected content's comment to "TEN kinds" and added a gate to pin it. Both were wrong. backend/domain/groups.py:330 and :368 build kind="frame" and kind="container" nodes with content= — twelve kinds, not ten. And the gate hard-coded graph.py and session_load.py while its own docstring called them "the two modules that create nodes", so a thirteenth kind landing in groups.py would have left it green.

That is the exact failure the gate exists to prevent — a closed set asserted by hand, growing, nothing failing — reproduced inside the gate itself.

Change

_positive_int is the right helper for a cap, where zero is not a value; both restorers use it. Counters keep _non_negative_int, because 0 is a real count. The harness's local _int is gone in favour of the two shared helpers.

The gate now discovers every module under backend/ that constructs a SceneNode instead of naming them, with a test that fails if discovery ever narrows back to the original two. The kind sets stay hand-authored on purpose: a human deciding "yes, a thirteenth kind should write content" is the checkpoint.

Test plan

  • 9 parametrised plan-cap cases and 6 harness-cap cases covering every numeric and non-numeric route to zero or negative, plus one pinning that a real cap survives and a counter of 0 is not rewritten to a default.
  • A discovery test asserting the module scan finds groups.py alongside the original two.
  • tests/test_node_state_migration.py caught the first draft of these tests aliasing node.state — rewritten to the required X.state.<field> chain rather than weakening the gate.
  • Full suite: 3181 passed, 19 skipped. ruff check . clean.

🤖 Generated with Claude Code

Both shipped earlier today with the defect still reachable. A verification
pass measured them; neither was theoretical.

Budget caps could still restore unusable. #393's commit message claimed
the plan's caps "keep falling back to their real defaults rather than
zero, since a restored plan with max_steps=0 could never run again". They
did not - _non_negative_int is `max(0, int(value))` with a fallback only
when int() RAISES, so every numeric route to zero went straight through,
and the outer `or default` only catches Python-falsy values:

    max_steps=-5  -> 0     max_steps='0'  -> 0
    max_steps='-5'-> 0     max_steps=0.5  -> 0

The harness restorer was worse: its local `_int` helper passed negatives
through unchanged, so a saved -5 restored as -5 and a saved '0' as 0.
builder._spend_breach then rejects the node on its first tick - it sits on
the canvas and can never run again.

_positive_int is the right helper for a cap, where zero is not a value,
and both restorers now use it. Counters keep _non_negative_int, because 0
is a real count. The harness's local helper is gone in favour of the two
shared ones.

The field-comment gate could not see the modules it was written to cover.
#400 corrected `content`'s comment to "TEN kinds" and added a gate to pin
it. Both were wrong: backend/domain/groups.py builds `kind="frame"` and
`kind="container"` nodes with `content=`, and the gate hard-coded
graph.py and session_load.py while its own docstring called them "the two
modules that create nodes". Twelve kinds, not ten - and a thirteenth
landing in groups.py would have left the gate green.

That is the failure the gate exists to prevent, reproduced inside the
gate. It now DISCOVERS every module under backend/ that constructs a
SceneNode instead of naming them, with a test asserting discovery finds
more than the original two. The kind sets stay hand-authored on purpose:
a human deciding "yes, a thirteenth kind should write content" is the
checkpoint.

Test plan:
- 9 parametrised plan-cap cases and 6 harness-cap cases covering every
  numeric and non-numeric route to zero or negative, plus a case pinning
  that a real cap survives and a counter of 0 is not rewritten.
- A discovery test that fails if the module scan narrows back.
- tests/test_node_state_migration.py caught the first draft of these
  tests aliasing node.state; rewritten to the required X.state.<field>
  chain rather than weakening the gate.
- Full suite: 3181 passed, 19 skipped. ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 2c185c8 into main Sep 4, 2026
5 checks passed
@dovvnloading
dovvnloading deleted the fix/incomplete-restore-and-doc-gates branch September 4, 2026 14:22
dovvnloading added a commit that referenced this pull request Sep 4, 2026
SceneDocument had 144 public members, 79 of them specific to a single node
kind, in a 3,037-line module. That is most of what makes adding a node
kind a 44-file change, and it is the half of QA item C3 that is real -
the other half, "kind-specific fields left on SceneNode", turned out on
measurement to be shared fields that belong there (see #400, #402).

backend/domain/ already decomposes its CROSS-CUTTING concerns this way:
SceneDocument is composed from BranchOps, GroupOps, LayoutOps and
CommandOps. This extends the same idiom to the PER-KIND ones, starting
with the two largest groups:

    backend/domain/nodes_code_review.py   CodeReviewOps    9 methods
    backend/domain/nodes_gitlink.py       GitlinkOps      11 methods

    graph.py                3,037 -> 2,591 lines
    methods in graph.py        93 -> 73
    SceneDocument public API  144 -> 144  (unchanged, by design)

Both mixins need only what SceneDocumentParts already declares - nodes,
connect, _counter - so they inherit it and stay type-checkable in
isolation like their cross-cutting siblings, and
tests/test_mixin_declaration_bases.py covers them automatically through
the MRO rather than by being told about them.

This is a relocation, not a rewrite. Every one of the 20 moved methods was
verified AST-identical to its pre-move version (ast.unparse round-trip
compared against `git show HEAD:backend/domain/graph.py`), and every one
is still reachable on SceneDocument. store_code_review_diff's own
_bundle_int helper moved with the methods that use it.

Two kinds, not all fifteen: this proves the extraction on the largest
groups and leaves the pattern for the rest to follow one at a time. The
remaining thirteen are 13-128 lines each.

Test plan:
- 20/20 moved methods AST-identical; MRO is
  SceneDocument -> Branch/Group/Layout/Command/CodeReview/Gitlink Ops ->
  SceneDocumentParts -> object.
- tests/test_domain_purity.py green: neither new module imports anything
  the domain layer may not.
- Full suite: 3224 passed, 19 skipped. ruff and mypy clean.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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