docs(plans): the 2A/2B split is a measured result, not a prediction #129

Merged
core-labs merged 1 commit from docs/grammar-unification-plan into main 2026-07-26 23:51:12 +00:00
Owner

Follow-up to #128. One commit, docs-only, docs/plans/grammar-unification-2026-07-26.md (+32/-5).

#128 landed the plan with two of three corrections. This is the third, and it retracts a rule I wrote in the second one.

What was wrong

Correction 2 replaced "Phases 1–3 cannot change serving behaviour" with a test:

a table is off-serving only when every caller is proven to be eval-only

I then actually ran that test. I computed the serving import closure — 227 first-party modules reachable from chat/runtime.py, core/cognition/pipeline.py, chat/deduction_surface.py, chat/curriculum_surface.py, chat/pack_grounding.py, chat/teaching_grounding.py — and all 13 table-owning modules are in it. 13 of 13. Including generate/templates.py, via pipeline.pyrealizer.pytemplates.py.

A rule that classifies every candidate the same way discriminates nothing. It would have let me call any change "serving-risky" or argue any change into the safe bucket, with equal justification either way.

What replaces it

Import-reachable is not call-reachable: importing a module evaluates its table literals (pure data) but does not call its functions. Where the call chain is closed and small it can still be checked by hand — realize_targetrender_step_inflect_predicatebase_form has zero non-eval, non-test callers, so §1.2/§1.4 stand on that basis.

But Python call-reachability is not statically decidable in general, so this arc's arbiter is empirical: make the change, run the 11 pinned lanes, let byte-identity decide.

The 2A/2B split is a result, not a prediction. A change is 2A if the pins come back byte-identical and 2B if they move. Work the changes in the listed order, run the pins, and let the split fall out. Do not argue a change into 2A.

This is strictly stronger than what it replaces: it tests behaviour instead of predicting it, and it is the doctrine the lane pins already encode.

Why it is worth a PR of its own

The plan's whole premise is that this arc exists to fix measurements that were asserted rather than taken. A plan that shipped with an undiscriminating classification rule at its Phase 2 gate would be the same defect one level up.

[Verification]: docs-only diff — no .py, no corpus, no pin touched, so no lane can move. The sibling Phase 1 branch ran uv run core test --suite deductive -q green (364 passed) against this same base in-session.

Follow-up to #128. One commit, docs-only, `docs/plans/grammar-unification-2026-07-26.md` (+32/-5). #128 landed the plan with two of three corrections. This is the third, and it retracts a rule I wrote in the second one. ## What was wrong Correction 2 replaced "Phases 1–3 cannot change serving behaviour" with a test: > a table is off-serving only when every caller is proven to be eval-only I then actually ran that test. I computed the serving import closure — 227 first-party modules reachable from `chat/runtime.py`, `core/cognition/pipeline.py`, `chat/deduction_surface.py`, `chat/curriculum_surface.py`, `chat/pack_grounding.py`, `chat/teaching_grounding.py` — and **all 13 table-owning modules are in it. 13 of 13.** Including `generate/templates.py`, via `pipeline.py` → `realizer.py` → `templates.py`. A rule that classifies every candidate the same way discriminates nothing. It would have let me call any change "serving-risky" or argue any change into the safe bucket, with equal justification either way. ## What replaces it Import-reachable is not call-reachable: importing a module evaluates its table literals (pure data) but does not call its functions. Where the call chain is closed and small it can still be checked by hand — `realize_target` → `render_step` → `_inflect_predicate` → `base_form` has **zero non-eval, non-test callers**, so §1.2/§1.4 stand on that basis. But Python call-reachability is not statically decidable in general, so this arc's arbiter is **empirical**: make the change, run the 11 pinned lanes, let byte-identity decide. ⇒ **The 2A/2B split is a *result*, not a prediction.** A change is 2A if the pins come back byte-identical and 2B if they move. Work the changes in the listed order, run the pins, and let the split fall out. **Do not argue a change into 2A.** This is strictly stronger than what it replaces: it tests behaviour instead of predicting it, and it is the doctrine the lane pins already encode. ## Why it is worth a PR of its own The plan's whole premise is that this arc exists to fix measurements that were asserted rather than taken. A plan that shipped with an undiscriminating classification rule at its Phase 2 gate would be the same defect one level up. [Verification]: docs-only diff — no `.py`, no corpus, no pin touched, so no lane can move. The sibling Phase 1 branch ran `uv run core test --suite deductive -q` green (364 passed) against this same base in-session.
core-labs added 1 commit 2026-07-26 23:32:26 +00:00
Third correction, same session, again from measurement rather than
argument.

I specified the Phase 2A/2B split as "a table is off-serving only when
every caller is proven eval-only," then computed the actual serving import
closure: 227 first-party modules from the six real serving entries, and
ALL 13 table-owning modules are in it — 13 of 13, including
generate/templates.py via pipeline.py -> realizer.py -> templates.py. So
the static test I had just written returns "everything is serving" and
discriminates nothing.

Import-reachable is not call-reachable. Importing a module executes its
table literals (pure data) but not its functions. The chain
realize_target -> render_step -> _inflect_predicate -> base_form is closed
and small enough to verify exhaustively, and realize_target has zero
non-eval non-test callers, so §1.2 and §1.4 stand as written.

But Python call-reachability is not statically decidable in general.
So the arbiter for this arc is EMPIRICAL: make the change, run the 11
pinned lanes, let byte-identity decide. That is stronger than a static
claim because it tests behaviour instead of predicting it, and it is what
the lane pins already encode as doctrine.

Consequence: 2A and 2B are now defined as a RESULT. A change is 2A if the
pins come back byte-identical and 2B if they move. Work the changes in
order, run the pins, let the split fall out — do not argue a change into
2A.
core-labs merged commit 9cafe5f6f4 into main 2026-07-26 23:51:12 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: core-labs/core#129
No description provided.