feat(demo): humanise teaching-grounded surface for layperson display

The conversation demo's Scene 4 was emitting CORE's raw production
teaching-grounded surface, which reads engineer-y for a layperson:

  narrative — teaching-grounded (cognition_chains_v1):
  rhetoric.narrative; language.discourse. narrative reveals
  meaning (cognition.meaning). No session evidence yet.

The production format is the trust-boundary contract (12+ tests + eval
byte-equivalence + several ADRs depend on it), so it stays unchanged.

This change adds a demo-only display layer that rewrites the same
surface to put the propositional sentence first, with provenance as a
trailing parenthetical:

  Narrative reveals meaning. (teaching-grounded from
  cognition_chains_v1 — narrative: rhetoric.narrative;
  language.discourse; final term: cognition.meaning.
  No session evidence yet.)

Trust-boundary preserving:
  - Only fires when grounding_source == "teaching" AND surface matches
    the production format.
  - Every load-bearing token preserved (subject, connective, object,
    corpus_id, semantic_domains, "No session evidence yet").
  - Pack-grounded surfaces + discourse-planner surfaces pass through
    unchanged.
  - JSON report's `surface` field still carries the raw production
    surface — only the chat-style print is humanised.

Test gate: 2 new tests pin the rewrite contract (proposition-first,
all load-bearing tokens preserved, passthrough for non-teaching).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Shay 2026-05-19 14:14:02 -07:00
parent ece7e3d2b1
commit c435bdf88c
2 changed files with 133 additions and 8 deletions

View file

@ -37,6 +37,7 @@ deterministic.
from __future__ import annotations from __future__ import annotations
import re
import sys import sys
import textwrap import textwrap
import time import time
@ -47,6 +48,76 @@ from chat.runtime import ChatRuntime
from core.config import RuntimeConfig from core.config import RuntimeConfig
# Production teaching-grounded surface format (chat/teaching_grounding.py):
# "{subject} — teaching-grounded ({corpus_id}): {ds1}; {ds2}.
# {subject} {conn} {object} ({do}). No session evidence yet."
#
# Semantic domains contain dots ("rhetoric.narrative"), so we can't
# split on '.' alone. Instead we anchor on the fixed trailing
# "No session evidence yet.", the corpus-id parenthetical, and the
# fact that the propositional sentence begins with the subject lemma
# (which we capture from the header). This makes the parse
# unambiguous against the live format.
_TEACHING_HEADER_RE = re.compile(
r"^(?P<subject>[A-Za-z][A-Za-z_-]*)\s*—\s*teaching-grounded\s*"
r"\((?P<corpus_id>[^)]+)\):\s*"
)
_TEACHING_TAIL_LITERAL = "No session evidence yet."
def _humanize_surface(surface: str, *, grounding_source: str) -> str:
"""Layperson-friendly rewrite of CORE's surface for display.
Trust-boundary preserving:
* Only fires for ``grounding_source == "teaching"`` surfaces
matching the production format.
* Keeps every load-bearing token (subject, connective, object,
corpus_id, semantic_domains, "No session evidence yet").
* Reorders so the propositional sentence reads first, with
provenance as a trailing parenthetical.
Production surface is unchanged this is presentation only and is
not applied to the JSON report's ``surface`` field.
"""
if grounding_source != "teaching":
return surface
text = surface.strip()
if not text.endswith(_TEACHING_TAIL_LITERAL):
return surface
header = _TEACHING_HEADER_RE.match(text)
if header is None:
return surface
subject = header.group("subject")
corpus_id = header.group("corpus_id").strip()
body = text[header.end():-len(_TEACHING_TAIL_LITERAL)].rstrip().rstrip(".").strip()
# Body shape: "{ds1}; {ds2}. {subject} {conn} {object} ({do})"
# The split between subject_domains and the sentence is the FIRST
# ". " followed by the subject lemma.
sentence_marker = f". {subject} "
idx = body.find(sentence_marker)
if idx == -1:
return surface
subject_domains = body[:idx].strip()
sentence_and_obj = body[idx + 2:].strip() # skip ". "
# Trailing "(do)" parenthetical:
paren_open = sentence_and_obj.rfind("(")
paren_close = sentence_and_obj.rfind(")")
if paren_open == -1 or paren_close == -1 or paren_close < paren_open:
return surface
sentence = sentence_and_obj[:paren_open].strip()
object_domains = sentence_and_obj[paren_open + 1:paren_close].strip()
if not sentence:
return surface
sentence_cased = sentence[:1].upper() + sentence[1:]
return (
f"{sentence_cased}. "
f"(teaching-grounded from {corpus_id}"
f"{subject}: {subject_domains}; "
f"final term: {object_domains}. "
f"No session evidence yet.)"
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Streaming presentation # Streaming presentation
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -120,13 +191,26 @@ def _scene_header(num: int, title: str) -> None:
sys.stdout.flush() sys.stdout.flush()
def _emit_turn(prompt: str, response_text: str, note: str, *, stream: bool) -> None: def _emit_turn(
prompt: str,
response_text: str,
note: str,
*,
stream: bool,
grounding_source: str = "",
) -> None:
"""Render one You/CORE turn with a caption. """Render one You/CORE turn with a caption.
``stream=True`` adds per-character / per-word delays (live feel). ``stream=True`` adds per-character / per-word delays (live feel).
``stream=False`` prints the same layout instantly (CI / tests / ``stream=False`` prints the same layout instantly (CI / tests /
fast capture). fast capture).
``response_text`` is humanised for display only when it matches
the production teaching-grounded format, it's rewritten to put
the propositional sentence first and provenance in a trailing
parenthetical. The raw surface remains in the JSON report.
""" """
displayed = _humanize_surface(response_text, grounding_source=grounding_source)
if stream: if stream:
sys.stdout.write(" You: ") sys.stdout.write(" You: ")
_stream_write(prompt, _CARET_DELAY_SECONDS) _stream_write(prompt, _CARET_DELAY_SECONDS)
@ -134,12 +218,12 @@ def _emit_turn(prompt: str, response_text: str, note: str, *, stream: bool) -> N
sys.stdout.write(" CORE: ") sys.stdout.write(" CORE: ")
sys.stdout.flush() sys.stdout.flush()
time.sleep(0.25) # tiny "thinking" pause time.sleep(0.25) # tiny "thinking" pause
_stream_words(response_text, prefix=" ", width=58) _stream_words(displayed, prefix=" ", width=58)
_stream_note(note) _stream_note(note)
else: else:
sys.stdout.write(f" You: {prompt}\n\n") sys.stdout.write(f" You: {prompt}\n\n")
wrapped_response = textwrap.fill( wrapped_response = textwrap.fill(
response_text, width=58, displayed, width=58,
initial_indent=" ", subsequent_indent=" ", initial_indent=" ", subsequent_indent=" ",
) )
sys.stdout.write(f" CORE: {wrapped_response.lstrip()}\n\n") sys.stdout.write(f" CORE: {wrapped_response.lstrip()}\n\n")
@ -215,7 +299,7 @@ def _scene1_pack_lookup(*, show: bool, stream: bool) -> TurnRecord:
"internet, no guessing." "internet, no guessing."
) )
if show: if show:
_emit_turn(prompt, surface, note, stream=stream) _emit_turn(prompt, surface, note, stream=stream, grounding_source=grounding)
return TurnRecord( return TurnRecord(
scene="S1_pack_lookup", prompt=prompt, surface=surface, scene="S1_pack_lookup", prompt=prompt, surface=surface,
grounding_source=grounding, note=note, grounding_source=grounding, note=note,
@ -233,7 +317,7 @@ def _scene2_teaching_chain(*, show: bool, stream: bool) -> TurnRecord:
"an operator approved." "an operator approved."
) )
if show: if show:
_emit_turn(prompt, surface, note, stream=stream) _emit_turn(prompt, surface, note, stream=stream, grounding_source=grounding)
return TurnRecord( return TurnRecord(
scene="S2_teaching_chain", prompt=prompt, surface=surface, scene="S2_teaching_chain", prompt=prompt, surface=surface,
grounding_source=grounding, note=note, grounding_source=grounding, note=note,
@ -251,7 +335,7 @@ def _scene3_compound(*, show: bool, stream: bool) -> TurnRecord:
"in the lexicon or in a reviewed chain." "in the lexicon or in a reviewed chain."
) )
if show: if show:
_emit_turn(prompt, surface, note, stream=stream) _emit_turn(prompt, surface, note, stream=stream, grounding_source=grounding)
return TurnRecord( return TurnRecord(
scene="S3_compound", prompt=prompt, surface=surface, scene="S3_compound", prompt=prompt, surface=surface,
grounding_source=grounding, note=note, grounding_source=grounding, note=note,
@ -301,13 +385,19 @@ def _scene4_learning_loop(*, show: bool, stream: bool) -> tuple[TurnRecord, Turn
) )
if show: if show:
_emit_turn(prompt, before_surface, before_note, stream=stream) _emit_turn(
prompt, before_surface, before_note,
stream=stream, grounding_source=before_grounding,
)
sys.stdout.write("\n") sys.stdout.write("\n")
sys.stdout.write(" ┄ ┄ ┄ operator teaches CORE one new fact ┄ ┄ ┄\n\n") sys.stdout.write(" ┄ ┄ ┄ operator teaches CORE one new fact ┄ ┄ ┄\n\n")
sys.stdout.flush() sys.stdout.flush()
if stream: if stream:
time.sleep(0.6) time.sleep(0.6)
_emit_turn(prompt, after_surface, after_note, stream=stream) _emit_turn(
prompt, after_surface, after_note,
stream=stream, grounding_source=after_grounding,
)
before = TurnRecord( before = TurnRecord(
scene="S4a_cold_turn", prompt=prompt, surface=before_surface, scene="S4a_cold_turn", prompt=prompt, surface=before_surface,

View file

@ -80,6 +80,41 @@ def test_demo_json_shape_is_stable(demo_report: dict) -> None:
} }
def test_humanize_surface_rewrites_teaching_grounded() -> None:
"""The display-only humaniser must put the propositional sentence
first and keep every load-bearing token from the production
teaching-grounded format."""
from evals.conversation.run_demo import _humanize_surface
raw = (
"narrative — teaching-grounded (cognition_chains_v1): "
"rhetoric.narrative; language.discourse. narrative reveals meaning "
"(cognition.meaning). No session evidence yet."
)
out = _humanize_surface(raw, grounding_source="teaching")
# Propositional sentence first, sentence-cased.
assert out.startswith("Narrative reveals meaning.")
# Every load-bearing token preserved (trust boundary).
assert "teaching-grounded" in out
assert "cognition_chains_v1" in out
assert "rhetoric.narrative" in out
assert "language.discourse" in out
assert "cognition.meaning" in out
assert "No session evidence yet." in out
def test_humanize_surface_is_passthrough_for_non_teaching() -> None:
from evals.conversation.run_demo import _humanize_surface
pack_surface = "Truth is a claim. pack-grounded (en_core_cognition_v1)."
assert _humanize_surface(pack_surface, grounding_source="pack") == pack_surface
discourse_surface = "Recall is to retrieve a stored state from memory. Recall reveals memory."
# Discourse-planner output doesn't match the raw teaching-grounded
# format — passes through unchanged.
assert _humanize_surface(discourse_surface, grounding_source="teaching") == discourse_surface
def test_demo_does_not_mutate_active_teaching_corpus() -> None: def test_demo_does_not_mutate_active_teaching_corpus() -> None:
"""The demo must be read-only against the live corpus.""" """The demo must be read-only against the live corpus."""
from chat import teaching_grounding as _tg from chat import teaching_grounding as _tg