Skip to content

fix(observer): report must agree with the predicates at a full window (#735, #736) - #759

Merged
InauguralPhysicist merged 2 commits into
mainfrom
fix/735-report-full-window-agreement
Jul 29, 2026
Merged

fix(observer): report must agree with the predicates at a full window (#735, #736)#759
InauguralPhysicist merged 2 commits into
mainfrom
fix/735-report-full-window-agreement

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

#735report labelled a still-moving value "equilibrium"

observer_slot_report resolves the six windowed bands in priority order, then falls back to an instantaneous label read off the last dH. That fallback exists for a partial window — where the full-window predicates are false by rule and report still has to say something — but it was never gated on count < N, so it also ran at a full one.

The bands are not exhaustive. A full window of steady gray-band drift at low entropy fires nothing:

predicate why it's false here
improving / diverging every step is under dh_small — the #187 gray-band rule
equilibrium / converged window mean is over dh_zero
stable requires entropy >= h_low

Every such window was reported equilibrium. A decaying residual is exactly that state:

d is 0.9
i is 0
loop while i < 20:
    d is d * 0.5
    i is i + 1
when (assign count) : 21          <- N = 10, so the window is comprehensively full
report              : equilibrium
equilibrium/converged/stable/improving/diverging/oscillating of d : 0 0 0 0 0 0

So a convergence loop written the way PREDICATES.md recommends — the settled-plus-hold recipe, keyed on report — exits early at rc=0 with a plausible answer. docs/PREDICATES.md:328-335 promised the opposite and named the partial window as the only exception; the observed behaviour is the excluded case. Found by an agent that reimplemented the published formulas as an independent oracle: it confirms the six predicates are right and report is wrong.

Fix. The fallback is gated on a partial window. A full window with no band true reports moving — the residual label the value channel already uses, so both channels answer "not settled, not in any named band" the same way. Relaxing stable's h_low gate was the alternative, and was rejected: the oracle says the predicates are correct, so the wider blast radius would have been spent fixing the wrong thing.

The invariant is now asserted, not described. tests/test_predicate_matrix.eigs checks every full-window shape: report must name a band whose bare predicate is true, or moving when none is. That guarantee was prose only, which is why this survived. Verified red-then-green against the pre-fix build — the assert fires on report=equilibrium with all six false.

Fallout the fix exposed, fixed rather than worked around. lib/experiment.eigs was relying on the wrong answer. All five analyzers seeded tracker is 0 observed, putting a synthetic 0 -> values[0] entropy jump at the head of the trajectory — a real step to every windowed predicate. is_measurement_stable scored ten identical readings as stable only because the fallback overrode the resulting window; with report telling the truth it returned "not stable" for constant data. The sentinel is a declaration, not a measurement, so it is now seeded inside unobserved: and the trajectory is the data.

#736 — four observer papercuts

  1. A bare observer query as a statement was silent. W019 already covered the interrogatives (where is x, prev of y); it now covers the query special forms over an ident — report / report_value / observe / trajectory — which print nothing and raise nothing at statement level. That is the worst affordance the observer had: silence is indistinguishable from "the observer had nothing to say", and the bare form is the one issue bodies and READMEs reach for. Zero false positives by construction — over an ident these are compiler-resolved and never reach a user function even when one shadows the name (Name-keyed dispatch superinstruction hijacks a user-rebound 'dispatch' (silent wrong result); also fires on the parenthesized #355 form #459, see W013), and a non-ident argument (report of (x + 0.0)) is an ordinary call the check never sees.
  2. state_at leaked __loop_exit__. Now {"x": 4, "i": 0}. The same filter applies to --step's bindings listing, where it also appeared; an explicit p __loop_exit__ still answers, and the tape's binding count stays honest. The filter matches the general __name__ form rather than a hardcoded pair, so a new internal cannot drift back into view — the form is now documented as runtime-reserved in SYNTAX.md, since a user variable shaped that way would be hidden too.
  3. SYNTAX.md still called how degenerate — returns 0, 1 only at zero entropy. Observer surface coherence: implement or remove 'how'; decide |x|=1.0; fold in #383 #412 made it a real gradient: 1 - min(1, |dH| / dh_zero), the settledness of the last step.
  4. PREDICATES.md still claimed entropy is exactly 0 at |x| ∈ {0, 1}. Observer surface coherence: implement or remove 'how'; decide |x|=1.0; fold in #383 #412 deleted the |x| == 1 special case, so unity is the maximum — the horizon — and only 0 is the home point. Stale in the maximally confusing direction: 0 vs 1 is the difference between "fully converged" and "maximally undecided".

Also adds the missing worked at example inside a loop, where "the last assignment at or before this line" answers with the loop's final pass — precisely where the documented rule stops matching intuition.

Validation

  • Release suite 3273/3273.
  • ASan/UBSan with detect_leaks=1 3277/3277, leak tally 0.
  • New regression coverage: full-window agreement across every matrix shape (test_predicate_matrix.eigs), state_at internal-binding filtering (test_temporal.eigs, queried past EOF so it stays line-insensitive), W019 positives and negatives (test_lint.sh).

docs/COMPARISON.md carries no report-vocabulary claim, so it needed no change; docs/SPEC.md did and got one.

Closes #735
Closes #736

🤖 Generated with Claude Code

…#735, #736)

`observer_slot_report` resolves the six windowed bands and then falls back
to an instantaneous label read off the last dH. That fallback is for a
PARTIAL window — where the full-window predicates are false by rule and
report still has to say something — but it was never gated on count < N,
so it also ran at a full one.

The bands are not exhaustive. A full window of steady gray-band drift at
LOW entropy fires nothing: every step under dh_small excludes
improving/diverging by the #187 rule, a mean over dh_zero excludes
equilibrium/converged, and stable requires entropy >= h_low. Every such
window was reported "equilibrium". A decaying residual is exactly that
state, so a convergence loop written the way PREDICATES.md recommends —
keyed on report — exited early at rc=0 with a plausible answer, while
docs/PREDICATES.md:328-335 promised the opposite and named the partial
window as the only exception.

The fallback is now gated on a partial window, and a full window with no
band true reports "moving" — the residual label the value channel already
uses, so both channels answer "not settled, not in any named band" the
same way. The agreement invariant is asserted in test_predicate_matrix
across every full-window shape; it had been prose only, which is why this
survived.

lib/experiment.eigs was relying on the wrong answer: all five analyzers
seeded `tracker is 0` OBSERVED, putting a synthetic 0 -> values[0] entropy
jump at the head of the trajectory, and is_measurement_stable scored ten
identical readings as stable only because the fallback overrode the
resulting window. The sentinel is a declaration, not a measurement — it is
now seeded inside `unobserved:`.

Also closes the four observer papercuts in #736:
- W019 now covers the query special forms over an ident (report /
  report_value / observe / trajectory) as bare statements, which printed
  nothing and raised nothing. Zero false positives by construction: over
  an ident these never reach a user function even when one shadows the
  name (#459), and a non-ident argument is an ordinary call the check
  never sees.
- state_at no longer returns __loop_exit__; the same filter applies to
  --step's bindings listing (an explicit `p __loop_exit__` still answers,
  and the tape's binding count stays honest).
- SYNTAX.md no longer calls `how` degenerate — #412 made it a real
  gradient, 1 - min(1, |dH| / dh_zero).
- PREDICATES.md no longer claims entropy is exactly 0 at |x| = 1 — #412
  deleted that special case, so unity is the maximum, the horizon. Stale
  in the maximally confusing direction: 0 vs 1 is the difference between
  "fully converged" and "maximally undecided".
- Adds the missing worked `at` example inside a loop, where "the last
  assignment at or before this line" answers with the final pass.

Release suite 3273/3273; ASan/UBSan with detect_leaks=1 3277/3277, leak
tally 0. COMPARISON.md carries no report-vocabulary claim, so it needed no
change; SPEC.md did and got one.

Closes #735
Closes #736

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 00:36

Copilot AI 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.

Pull request overview

Fixes observer contract violations and papercuts so report/predicates, temporal queries, and lint/docs all agree with the intended observer model.

Changes:

  • Gate observer_slot_report’s instantaneous fallback to partial windows; full windows now return "moving" when no predicate band is true, and add regression coverage asserting full-window agreement.
  • Hide runtime-internal __name__ bindings from state_at and unfiltered --step binding dumps via a shared trace_name_is_internal() helper.
  • Extend W019 to warn on statement-position observer query special-forms (report/observe/report_value/trajectory over an ident) and refresh observer docs (how, entropy at |x|=1, loop at example, diagnostics/spec/changelog).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_temporal.eigs Adds regression asserting state_at does not expose __loop_exit__ while still returning user bindings.
tests/test_predicate_matrix.eigs Adds full-window residual-drift case and asserts the “full-window report agrees with predicates or ‘moving’” invariant.
tests/test_lint.sh Adds W019 positives for bare observer queries-as-statements and negatives for expression usage.
src/trace.h Declares trace_name_is_internal() for shared filtering logic.
src/trace.c Implements trace_name_is_internal() and filters internal bindings from trace_state_at().
src/step.c Filters internal bindings from unfiltered bindings listing while keeping explicit p __loop_exit__ queries working.
src/lint.c Extends W019 to warn on discarded observer-query special forms at statement level.
src/eigenscript.c Fixes observer_slot_report() so full windows with no true predicate return "moving" (no partial-window fallback).
lib/experiment.eigs Seeds analyzer tracker under unobserved: to avoid synthetic first-step entropy artifacts.
docs/SYNTAX.md Updates how semantics, adds loop at example, and documents __name__ as runtime-reserved/hidden in dumps.
docs/SPEC.md Specifies full-window report agreement contract and "moving" residual for non-exhaustive bands.
docs/PREDICATES.md Documents non-exhaustive full-window “moving” case; clarifies entropy peak at `
docs/DIAGNOSTICS.md Updates W019 description to include observer query special forms over identifiers.
docs/BUILTINS.md Updates report vocabulary to include "moving" for the residual full-window case.
CHANGELOG.md Records both #735 and #736 fixes, including contract clarification and exposed downstream reliance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…oves

The cross-repo corpus (#262) is the gate that catches an observer change
altering real consumer behavior, and it fired on three of fourteen
programs. All three are the fix working; the other eleven are
byte-identical.

- dynamics__solve — the consumer-visible half of the bug, and the best
  evidence the fix is right. All three solvers drive
  `status is report of change` and stop on "equilibrium", so all three
  were terminating early on the false label: Jacobi 14 iters ->
  0.9999995231628418 becomes 21 -> 0.9999999997671694, Gauss-Seidel
  11 -> 18, PageRank 27 -> 32. A real downstream solver was silently
  returning a less-accurate answer, exactly as #735 predicted.

- EigenScript__numerical, EigenScript__observer_predicates — decaying
  residuals relabelled from "stable"/"equilibrium" to "moving". Same
  trajectory, honest name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 00:55

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

@InauguralPhysicist
InauguralPhysicist merged commit cdcb31e into main Jul 29, 2026
32 of 33 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix/735-report-full-window-agreement branch July 29, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants