core/scripts/generate_discourse_paragraph.py
Shay 257a27c105 feat(benchmarks): discourse_paragraph lane + pipeline profiler + word-selection tracer
Closes the user-flagged scope gap: every previous fluency lane (Phase
5.1 + 5.4-5.7 + grammatical_coverage) operates on 3-word SVO probes.
These three pieces stress paragraph-scale generation, give per-stage
latency visibility, and expose the realizer's word-choice geometry —
all on top of the existing deterministic infrastructure.

# discourse_paragraph lane (paragraph-scale fluency)

Forces the realizer to emit multi-sentence paragraphs from a
multi-step ArticulationTarget with rhetorical moves (ASSERT, SEQUENCE,
ELABORATE, CONTRAST).  Same realizer, much richer input — every case
is 3-5 sentences with deterministic discourse markers.

Public 12 cases / holdouts 5 / dev 1 across 12 + 5 topic chains
(epistemic, scientific method, creation arc, logical dependency,
ethical grounding, linguistic layers, mathematical chain, narrative,
biology, physics, two contrast-shaped, musical, social, computational,
psychological, economic).

Sub-metrics per case:
  - sentence count (within min..max window)
  - subject coverage rate
  - discourse marker presence (next / furthermore / in contrast)
  - sentence-initial capitalization
  - replay determinism (run twice, surfaces match)

Result: 12/12 public + 5/5 holdouts at 100%, replay rate 100%, mean
sentence count 4.

# Realizer capitalization (G4, addresses user-flagged concern)

generate/realizer.py gains `_capitalize_sentence` + `_join_as_paragraph`
helpers.  Sentence-initial alphabetic characters are now uppercased
(skipping leading whitespace/punctuation).  Surfaces went from
"wisdom grounds knowledge. next, knowledge requires evidence."
to
"Wisdom grounds knowledge. Next, knowledge requires evidence."

The discourse_paragraph runner ships a strict per-sentence
capitalization check so future regressions get caught.

# Pipeline-stage profiler (benchmarks/pipeline_profiler.py)

External monkey-patch wrapper around CognitiveTurnPipeline.run() that
records per-stage ns budgets without editing any pipeline source.
Stages: intent, graph_planner, realize_semantic, runtime_chat,
maybe_transitive_walk, fold_walk_into_surface, run_teaching,
trace_hash.

API: `profile_turn(pipeline, text) -> ProfileReport` with
`.stages: dict`, `.total_ns: int`, `.as_dict()`.

Empirical: runtime_chat dominates >99% on the runtime hot path (which
is correct — that's where ingest + propagate + recall + articulate
all happen).  Future optimisation work has a clear per-stage signal.

# Word-selection tracer (benchmarks/word_selection_tracer.py)

External wrapper around generate.articulation._resolve_slot that
records every nearest-neighbor lookup as a WordSelectionStep:
  - slot (subject/predicate/object)
  - input versor (32-d copy)
  - top-K candidate words by CGA inner product
  - chosen word + morphology
  - output language

Top-K scoring uses the diagonal Cl(4,1) metric kernel from
algebra.backend (same vectorised path vault_recall uses), not a
per-word Python loop over cga_inner.  No approximation, exact
deterministic ranking, bit-identical to a scalar scan.

API: `trace_realization(pipeline, text) -> RealizationTrace` with
`.steps`, `.realization_steps`, `.surface`, `.as_dict()`.

# CLI lane registration

Cognition suite now sweeps the benchmark profiler/tracer tests
(test_benchmarks_profiler.py) so any future regression in the
instrumentation surfaces immediately.

# Constraints honoured

- Zero edits to core/, chat/, vault/, teaching/, language_packs/, or
  the algebra hot path.  All instrumentation is external monkey-patch
  with originals restored in finally.
- discourse_paragraph runner bypasses ChatRuntime grounding (named v2
  gap) so paragraph capability is isolated to the realizer.
- No semantic changes; no hidden normalisation; no approximate
  recall.

# Lane health

smoke 55, runtime 19, teaching 17, packs 6, cognition 105 (was 103),
algebra 132.  All Phase 5 fluency lanes still 100% with the
capitalised surfaces (rubric is case-insensitive).  discourse_paragraph
100%.

# What ships next (named v2)

- Round-trip: discourse_paragraph through ChatRuntime end-to-end,
  not just realize_target.
- Per-sentence grammatical_coverage rubric on each emitted sentence.
- Longer chains (10/20/50 sentences) with per-sentence determinism
  scaling curves.
- compose_relations operator to lift compositionality recall from
  68.8% toward 100%.
2026-05-16 21:53:46 -07:00

273 lines
9 KiB
Python

"""Generate cases for the discourse_paragraph benchmark lane.
Tests that the realizer can produce **multi-sentence paragraph-scale
output** from chained propositions, given a multi-step
ArticulationTarget with rhetorical moves (SEQUENCE, ELABORATE,
CONTRAST). Each case stresses paragraph length, subject coverage,
discourse-marker presence, and deterministic replay.
Each case carries:
- a graph of N ≥ 3 nodes (subject-predicate-object triples)
- an ordered move list ([ASSERT, SEQUENCE, ELABORATE, ...])
- acceptance constraints (min_sentences, must_contain_subjects,
discourse_markers)
Topics are designed to be **structurally rich** — every case is more
than a 3-word SVO probe.
Run:
.venv/bin/python scripts/generate_discourse_paragraph.py
"""
from __future__ import annotations
import json
from pathlib import Path
# Each topic: ordered triples + ordered rhetorical moves matching length.
# Moves: ASSERT (open), SEQUENCE (next step), ELABORATE (furthermore),
# CONTRAST (in contrast), CORRECT (correction). See
# generate.templates._MOVE_TEMPLATES for emitted discourse markers.
PUBLIC_TOPICS: list[dict] = [
{
"topic": "epistemic_chain",
"triples": [
("wisdom", "grounds", "knowledge"),
("knowledge", "requires", "evidence"),
("evidence", "supports", "truth"),
("truth", "reveals", "reality"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "scientific_method",
"triples": [
("observation", "grounds", "hypothesis"),
("hypothesis", "implies", "prediction"),
("prediction", "follows", "experiment"),
("experiment", "supports", "theory"),
("theory", "entails", "explanation"),
],
"moves": ["ASSERT", "ELABORATE", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "creation_arc",
"triples": [
("light", "precedes", "form"),
("form", "grounds", "matter"),
("matter", "supports", "structure"),
("structure", "reveals", "order"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "logical_dependency",
"triples": [
("premise", "supports", "conclusion"),
("conclusion", "requires", "validity"),
("validity", "entails", "soundness"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE"],
},
{
"topic": "ethical_grounding",
"triples": [
("virtue", "grounds", "action"),
("action", "requires", "intention"),
("intention", "supports", "consequence"),
("consequence", "reveals", "character"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "linguistic_layers",
"triples": [
("sound", "grounds", "phoneme"),
("phoneme", "supports", "morpheme"),
("morpheme", "builds", "word"),
("word", "composes", "sentence"),
("sentence", "conveys", "meaning"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE", "ELABORATE"],
},
{
"topic": "mathematical_chain",
"triples": [
("axiom", "grounds", "theorem"),
("theorem", "entails", "corollary"),
("corollary", "supports", "application"),
("application", "yields", "insight"),
],
"moves": ["ASSERT", "ELABORATE", "SEQUENCE", "SEQUENCE"],
},
{
"topic": "narrative_progression",
"triples": [
("conflict", "drives", "tension"),
("tension", "precedes", "climax"),
("climax", "yields", "resolution"),
("resolution", "reveals", "theme"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "biological_hierarchy",
"triples": [
("gene", "encodes", "protein"),
("protein", "builds", "cell"),
("cell", "composes", "tissue"),
("tissue", "forms", "organ"),
("organ", "supports", "organism"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE", "ELABORATE"],
},
{
"topic": "physical_causation",
"triples": [
("force", "drives", "motion"),
("motion", "transfers", "energy"),
("energy", "yields", "heat"),
("heat", "raises", "temperature"),
],
"moves": ["ASSERT", "ELABORATE", "SEQUENCE", "SEQUENCE"],
},
# Contrast-shaped cases — exercises the "in contrast" template.
{
"topic": "contrastive_definitions",
"triples": [
("knowledge", "requires", "evidence"),
("belief", "requires", "trust"),
("wisdom", "grounds", "judgment"),
],
"moves": ["ASSERT", "CONTRAST", "ELABORATE"],
},
{
"topic": "method_contrast",
"triples": [
("deduction", "yields", "certainty"),
("induction", "yields", "probability"),
("abduction", "yields", "explanation"),
],
"moves": ["ASSERT", "CONTRAST", "ELABORATE"],
},
]
HOLDOUT_TOPICS: list[dict] = [
{
"topic": "musical_construction",
"triples": [
("note", "composes", "chord"),
("chord", "supports", "harmony"),
("harmony", "yields", "phrase"),
("phrase", "builds", "melody"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "social_structure",
"triples": [
("custom", "grounds", "tradition"),
("tradition", "supports", "institution"),
("institution", "shapes", "society"),
("society", "reveals", "culture"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE"],
},
{
"topic": "computational_pipeline",
"triples": [
("input", "drives", "computation"),
("computation", "yields", "output"),
("output", "supports", "decision"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE"],
},
{
"topic": "psychological_development",
"triples": [
("sensation", "grounds", "perception"),
("perception", "supports", "memory"),
("memory", "yields", "learning"),
("learning", "shapes", "behavior"),
("behavior", "reveals", "character"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE", "SEQUENCE", "ELABORATE"],
},
{
"topic": "economic_flow",
"triples": [
("labor", "yields", "value"),
("value", "supports", "exchange"),
("exchange", "drives", "growth"),
],
"moves": ["ASSERT", "SEQUENCE", "ELABORATE"],
},
]
# Common discourse markers the realizer emits per RhetoricalMove
# (see generate.templates._MOVE_TEMPLATES).
_MARKERS_BY_MOVE: dict[str, str] = {
"ASSERT": "",
"ELABORATE": "furthermore",
"CONTRAST": "in contrast",
"SEQUENCE": "next",
"CORRECT": "correction:",
}
def _build_case(prefix: str, idx: int, topic: dict) -> dict:
triples = topic["triples"]
moves = topic["moves"]
assert len(triples) == len(moves), f"length mismatch in {topic['topic']}"
nodes = [
{
"node_id": f"n{i+1}",
"subject": s,
"predicate": p,
"obj": o,
}
for i, (s, p, o) in enumerate(triples)
]
steps = [
{"node_id": f"n{i+1}", "move": m}
for i, m in enumerate(moves)
]
must_contain_subjects = [t[0] for t in triples]
discourse_markers = sorted(
{_MARKERS_BY_MOVE[m] for m in moves if _MARKERS_BY_MOVE[m]}
)
return {
"id": f"{prefix}_{idx:03d}",
"topic": topic["topic"],
"graph": {"nodes": nodes, "edges": []},
"steps": steps,
"min_sentences": len(triples),
"must_contain_subjects": must_contain_subjects,
"discourse_markers": discourse_markers,
"max_sentences": len(triples) + 2, # tolerate small over-runs from
# downstream wrapping
}
def _emit(prefix: str, topics: list[dict], out_path: Path) -> int:
out_path.parent.mkdir(parents=True, exist_ok=True)
lines = [
json.dumps(_build_case(prefix, i + 1, t), ensure_ascii=False)
for i, t in enumerate(topics)
]
out_path.write_text("\n".join(lines) + "\n")
return len(lines)
if __name__ == "__main__":
root = Path(__file__).resolve().parent.parent
lane = root / "evals" / "discourse_paragraph"
n_pub = _emit("DP-PUB", PUBLIC_TOPICS, lane / "public" / "v1" / "cases.jsonl")
n_hold = _emit("DP-HOLD", HOLDOUT_TOPICS, lane / "holdouts" / "v1" / "cases.jsonl")
n_dev = _emit("DP-DEV", PUBLIC_TOPICS[:1], lane / "dev" / "cases.jsonl")
print(f"discourse_paragraph public={n_pub} holdouts={n_hold} dev={n_dev}")