core/evals/close_derived_climb/runner.py
Shay 7e5ac0ac20
feat: strengthen visibility and measurement of CLOSE flywheel proposal review/ratification side (#794)
- Ratified first (docs/analysis/close-flywheel-proposal-review-visibility-ratification-2026-06-16.md) before any impl edits.
- Additive proposal_review_posture in evals/close_derived_climb (from already-captured emission bodies; asserts born proposal_only/SPECULATIVE/requires_review posture, review_eligible, none_accepted_or_promoted inside the yardstick).
- Additive proposal_review_summary in evals/anti_regression DemoReport + run_demo (teaching gate review_states + ProposalLog transition counts from the temp log events exercised by S1-S3; close_derived subsection from the embedded climb).
- Updated RESULT text, contract test docstring + soft presence assert, climb __init__/contract, testing-lanes (new subsection), anti-regression docs, runtime_contracts.
- All strictly within scope: measurement/visibility only; no changes to review logic, FrameVerdict, promotion, teaching mutation, or core behavior.
- Verified: 90 contract tests green, core demo anti-regression JSON + text show the signals, climb posture present with correct values, wrong_total=0, all_gates_held + corpus_identical preserved, smoke lanes unaffected.
- Builds on #793 dedicated surface + #792/#791 embedding. Third Door / Mechanical Sympathy / Semantic Rigor alignment per ratif.
- No brief adjustments needed; workflow, scope, and success criteria followed exactly.
2026-06-16 19:33:25 -07:00

419 lines
No EOL
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Yardstick for CLOSE derived climb (PR-3 after #788+#789), hardened for full Claim B.
Deterministic proof that:
- is-a + relational-transitive derived facts enlarge the directly-answerable set
monotonically to fixed point across idle_tick (with consolidate_determinations=True).
- wrong_total == 0: excluded predicates (parent_of etc.) and membermember canary
never derive; negatives remain Undetermined.
- proposal emission uses *real* ChatRuntime.idle_tick() + IdleTickResult.derived_close_proposals_emitted
(gated by review_derived_close_proposals).
- Positive growth scored via semantic determine() calls on materialized facts (rule='direct' post-FP).
- All trajectories/replays stable at *content* level (closures + proposal bodies) + aggregates (no clock/LLM).
Uses real comprehend/realize/idle_tick path. Small fixed scenarios for is-a,
less_than, before_event, negatives. Proposal flag via lived idle_tick (not simulation).
replay_checksum: aggregate sizes + wrong + flag (compatibility).
content_replay_checksum: canonical closures (structure_key + Derivation/premises) + proposal bodies.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
from dataclasses import replace
from generate.determine import Determined, Undetermined, determine
from generate.meaning_graph.reader import comprehend
from generate.meaning_graph.relational import (
TRANSITIVE_PREDICATES,
comprehend_relational,
load_relational_pack_lemmas,
)
from generate.realize import RealizedRecord, realize_comprehension, recall_realized
from session.context import SessionContext
_HIGH = 10**9
_PACK = load_relational_pack_lemmas()
def _fresh_ctx(
consolidate: bool = True, proposals: bool = False
) -> tuple[SessionContext, ChatRuntime]:
from core.config import DEFAULT_CONFIG
cfg = replace(
DEFAULT_CONFIG,
consolidate_determinations=consolidate,
review_derived_close_proposals=proposals,
)
rt = ChatRuntime(config=cfg, no_load_state=True)
ctx = rt._context
# ensure high reproject like other lanes
if hasattr(ctx, "vault_reproject_interval"):
ctx.vault_reproject_interval = _HIGH
return ctx, rt
def _tell(ctx: SessionContext, text: str) -> None:
realize_comprehension(comprehend(text), ctx)
def _tell_rel(ctx: SessionContext, text: str) -> None:
realize_comprehension(comprehend_relational(text, _PACK), ctx)
def _ask(ctx: SessionContext, text: str) -> Determined | Undetermined:
return determine(comprehend(text), ctx)
def _ask_rel(ctx: SessionContext, text: str) -> Determined | Undetermined:
return determine(comprehend_relational(text, _PACK), ctx)
def _count_answerable(ctx: SessionContext, asks: list[tuple[str, bool]]) -> int:
"""Count how many of the asked facts are directly realized in the vault (the 'direct answerable' from CLOSE)."""
count = 0
for q, is_rel in asks:
q = q.lower().strip('?').strip()
if not q.startswith("is "):
continue
rest = q[3:].strip()
parts = rest.split()
if len(parts) < 2:
continue
subj = parts[0]
if "less than" in rest:
pred = "less_than"
obj = parts[-1]
elif "before" in rest:
pred = "before_event"
obj = parts[-1]
else:
# is-a "is a X" or "is an X"
pred = "member"
obj = parts[-1]
# check if the specific (pred, subj, obj) is realized
hits = [r for r in recall_realized(ctx, subject=subj, predicate=pred) if len(r.relation_arguments) > 1 and r.relation_arguments[1] == obj]
if hits:
count += 1
return count
def _get_closure_content(ctx: SessionContext) -> list[dict[str, Any]]:
"""Project full closure for Claim B content checksum (structure_key + derivation with premises)."""
data: list[dict[str, Any]] = []
for p in ["member", "subset"] + sorted(TRANSITIVE_PREDICATES):
for r in recall_realized(ctx, predicate=p):
der = asdict(r.derivation) if r.derivation is not None else None
data.append(
{
"predicate": p,
"args": list(r.relation_arguments),
"structure_key": r.structure_key,
"derived": r.derived,
"derivation": der,
"epistemic_status": getattr(r, "epistemic_status", None),
}
)
data.sort(key=lambda x: (x["predicate"], tuple(x["args"])))
return data
# --- Scenarios ---
def _is_a_climb() -> dict[str, Any]:
"""member/subset climb: rex dog -> mammal -> ... creature. Expect growth 1->4."""
ctx, rt = _fresh_ctx()
_tell(ctx, "Rex is a dog.")
_tell(ctx, "All dogs are mammals.")
_tell(ctx, "All mammals are animals.")
_tell(ctx, "All animals are creatures.")
# Canary: member(dog, kingdom) + member(rex, dog) should not derive member(rex, kingdom)
_tell(ctx, "Dog is a kingdom.")
# Query set for answerable - used for live measurement at each stage
queries = [
("Is Rex a dog?", False),
("Is Rex a mammal?", False),
("Is Rex an animal?", False),
("Is Rex a creature?", False),
("Is Rex a kingdom?", False), # must stay refused
]
before = _count_answerable(ctx, queries)
res1 = rt.idle_tick()
after1 = _count_answerable(ctx, queries)
# continue ticking until fixed point (use facts_consolidated==0 as proxy, since IdleTickResult has no at_fixed_point)
fp = res1
for _ in range(10): # safety bound
fp = rt.idle_tick()
if fp.facts_consolidated == 0:
break
after_fp = _count_answerable(ctx, queries)
wrong_total = 0
# Check canary not derived (still valid for wrong=0)
if "kingdom" in [r.relation_arguments[1] for r in recall_realized(ctx, subject="rex", predicate="member")]:
wrong_total += 1
# Semantic Answerability (Claim B): explicitly call determine on positives post-fixed-point
# and assert Determined(True) with rule='direct' (the materialized direct path).
positive_queries = [q for q in queries if "kingdom" not in q[0].lower()]
for q, is_rel in positive_queries:
det = _ask(ctx, q) if not is_rel else _ask_rel(ctx, q)
assert isinstance(det, Determined) and getattr(det, "answer", False) and getattr(det, "rule", None) == "direct", (
f"Claim B semantic answerability failed for {q}: got {det}"
)
closure = _get_closure_content(ctx)
return {
"scenario": "is_a_climb",
"before": before,
"after_tick_1": after1,
"after_fixed_point": after_fp,
"facts_consolidated_tick1": res1.facts_consolidated if hasattr(res1, 'facts_consolidated') else 0,
"at_fixed_point": (fp.facts_consolidated == 0),
"wrong_total": wrong_total,
"proposals_emitted": 0, # separate flag run below
"closure": closure,
"semantic_positives_determined_direct": len(positive_queries),
}
def _relational_climb_less_than() -> dict[str, Any]:
ctx, rt = _fresh_ctx()
_tell_rel(ctx, "A is less than B.")
_tell_rel(ctx, "B is less than C.")
_tell_rel(ctx, "C is less than D.")
# Consistent query set for all stages
queries = [
("Is A less than C?", True),
("Is B less than D?", True),
("Is A less than D?", True),
]
before = _count_answerable(ctx, queries)
res1 = rt.idle_tick()
after1 = _count_answerable(ctx, queries)
res2 = rt.idle_tick()
after_fp = _count_answerable(ctx, queries)
# Negative: no cross (refused)
neg = _ask_rel(ctx, "Is A less than E?") # no E, but to test refused for non-chain
wrong = 0
if isinstance(neg, Determined):
wrong += 1
# Semantic Answerability (Claim B)
for q, is_rel in queries:
det = _ask_rel(ctx, q)
assert isinstance(det, Determined) and getattr(det, "answer", False) and getattr(det, "rule", None) == "direct", (
f"Claim B semantic answerability failed for {q}: got {det}"
)
closure = _get_closure_content(ctx)
return {
"scenario": "less_than_climb",
"before": before,
"after_tick_1": after1,
"after_fixed_point": after_fp,
"wrong_total": wrong,
"closure": closure,
"semantic_positives_determined_direct": len(queries),
}
def _temporal_climb() -> dict[str, Any]:
ctx, rt = _fresh_ctx()
_tell_rel(ctx, "Dawn is before noon.")
_tell_rel(ctx, "Noon is before dusk.")
# Consistent query set (single but via helper for uniformity)
queries = [
("Is Dawn before dusk?", True),
]
before = _count_answerable(ctx, queries)
rt.idle_tick()
after = _count_answerable(ctx, queries)
# Semantic Answerability (Claim B)
for q, is_rel in queries:
det = _ask_rel(ctx, q)
assert isinstance(det, Determined) and getattr(det, "answer", False) and getattr(det, "rule", None) == "direct", (
f"Claim B semantic answerability failed for {q}: got {det}"
)
closure = _get_closure_content(ctx)
return {
"scenario": "before_event_climb",
"before": before,
"after_fixed_point": after,
"wrong_total": 0,
"closure": closure,
"semantic_positives_determined_direct": len(queries),
}
def _negatives_refused() -> dict[str, Any]:
ctx, rt = _fresh_ctx()
_tell_rel(ctx, "X is parent of Y.")
_tell_rel(ctx, "Y is parent of Z.")
_tell_rel(ctx, "P is sibling of Q.")
_tell_rel(ctx, "Q is sibling of R.")
rt.idle_tick()
rt.idle_tick()
refused = 0
for q in ["Is X parent of Z?", "Is P sibling of R?"]:
if isinstance(_ask_rel(ctx, q), Undetermined):
refused += 1
return {
"scenario": "negatives_refused",
"parent_refused": refused >= 1,
"sibling_refused": refused >= 2,
"wrong_total": 0 if refused == 2 else 1,
}
def _proposal_flag_effect() -> dict[str, Any]:
"""Measure proposal bridge via *lived* runtime flag path.
Uses real ChatRuntime.idle_tick() + IdleTickResult.derived_close_proposals_emitted
(the runtime flag in idle_tick gates the call to the bridge)."""
import generate.determine.derived_close_proposals as dcp
import tempfile
# "without" via real idle_tick with flag off (must emit 0)
ctx_off, rt_off = _fresh_ctx(consolidate=True, proposals=False)
_tell_rel(ctx_off, "A is less than B.")
_tell_rel(ctx_off, "B is less than C.")
res_off = rt_off.idle_tick()
emitted_without = res_off.derived_close_proposals_emitted
# "with" via real idle_tick with flag on (captures from IdleTickResult; temp sink for isolation + content)
with tempfile.TemporaryDirectory() as tmp:
sink = Path(tmp)
orig_sink = dcp.DEFAULT_SINK
dcp.DEFAULT_SINK = sink
try:
ctx_on, rt_on = _fresh_ctx(consolidate=True, proposals=True)
_tell_rel(ctx_on, "A is less than B.")
_tell_rel(ctx_on, "B is less than C.")
res_on = rt_on.idle_tick()
emitted_with = res_on.derived_close_proposals_emitted
# capture full bodies for content checksum (lived emission path)
proposals = [json.loads(p.read_text()) for p in sorted(sink.glob("*.json"))]
finally:
dcp.DEFAULT_SINK = orig_sink
return {
"scenario": "proposal_flag",
"emitted_without_flag": emitted_without,
"emitted_with_flag": emitted_with,
"only_with_flag": emitted_with > 0 and emitted_without == 0,
"proposals": proposals,
}
def run() -> dict[str, Any]:
"""Run the full yardstick. Returns report with climb metrics, wrong_total=0,
lived proposal flag isolation (via IdleTickResult), semantic determine() on positives,
replay_checksum (aggregates) + content_replay_checksum (closures + proposal bodies),
and proposal_review_posture (additive visibility into the review/ratification side:
emitted proposals are born proposal_only/SPECULATIVE/requires_review; the yardstick
exercises no acceptance, rejection, or promotion paths)."""
is_a = _is_a_climb()
less = _relational_climb_less_than()
temporal = _temporal_climb()
neg = _negatives_refused()
prop = _proposal_flag_effect()
# Aggregate
before_sets = [is_a["before"], less["before"], temporal["before"]]
after1_sets = [is_a["after_tick_1"], less["after_tick_1"], temporal["after_fixed_point"]]
fp_sets = [is_a["after_fixed_point"], less["after_fixed_point"], temporal["after_fixed_point"]]
total_wrong = is_a["wrong_total"] + less["wrong_total"] + temporal.get("wrong_total", 0) + neg["wrong_total"]
monotone_growth = all(a <= b for a, b in zip(before_sets, fp_sets))
strict_growth = sum(before_sets) < sum(fp_sets)
report = {
"is_a_climb": is_a,
"less_than_climb": less,
"before_event_climb": temporal,
"negatives": neg,
"proposal_flag": prop,
"aggregate": {
"direct_answerable_before": sum(before_sets),
"direct_answerable_after_tick_1": sum(after1_sets),
"direct_answerable_after_fixed_point": sum(fp_sets),
"monotone_growth": monotone_growth,
"strict_growth": strict_growth,
"wrong_total": total_wrong,
"proposals_only_with_flag": prop["only_with_flag"],
},
}
# Additive review/ratification-posture instrumentation for the CLOSE flywheel
# (the previously weaker half). Computed purely from the proposal bodies
# already emitted and captured for content_replay_checksum. No review logic
# is invoked; the yardstick only observes the explicit review-gated posture
# that every derived CLOSE proposal is born with (proposal_only / SPECULATIVE
# / requires_review). This surfaces acceptance/rejection eligibility signals
# and documents that the yardstick itself performs no ratification.
_proposals = prop.get("proposals", []) or []
_all_status = [p.get("status") for p in _proposals]
_all_epist = [p.get("epistemic_status") for p in _proposals]
_all_req = [p.get("requires_review") for p in _proposals]
report["proposal_review_posture"] = {
"emitted_count": len(_proposals),
"all_proposal_only": all(s == "proposal_only" for s in _all_status) if _proposals else True,
"all_speculative": all(e == "speculative" for e in _all_epist) if _proposals else True,
"all_requires_review": all(r is True for r in _all_req) if _proposals else True,
"review_eligible": len(_proposals),
"none_accepted_or_promoted": True, # yardstick is emission + semantic only; ratification is HITL / operator-gated outside this surface
}
# Replay checksum: stable sizes + wrongs + flag effect (kept for compatibility)
checksum_input = json.dumps(
{
"sizes": [report["aggregate"][k] for k in ("direct_answerable_before", "direct_answerable_after_tick_1", "direct_answerable_after_fixed_point")],
"wrong_total": total_wrong,
"flag_isolation": prop["only_with_flag"],
},
sort_keys=True,
).encode()
report["replay_checksum"] = hashlib.sha256(checksum_input).hexdigest()
# Parallel full-content checksum for Claim B fidelity (canonical closures + proposal bodies)
# This aligns the yardstick with its documentation ("closure sets", "exact trajectories").
content = {
"closures": {
"is_a": is_a.get("closure", []),
"less_than": less.get("closure", []),
"before_event": temporal.get("closure", []),
},
"proposals": prop.get("proposals", []),
}
content_input = json.dumps(content, sort_keys=True).encode()
report["content_replay_checksum"] = hashlib.sha256(content_input).hexdigest()
return report
if __name__ == "__main__":
import pprint
pprint.pprint(run())