core/docs/adr/ADR-0265-negation-in-the-proposition-graph.md
Shay 892e503347 docs(adr): ratify ADR-0265 — Accepted, Joshua Shay 2026-07-27
Serving-path authorization for the negation fix in this PR. The ADR is
Accepted rather than Proposed because the defect it removes was LIVE behind a
shipped flag (realizer_grounded_authority), not latent: CORE served the
affirmative of propositions users denied.
2026-07-27 13:17:57 -07:00

8.9 KiB
Raw Blame History

ADR-0265 — Negation belongs in the proposition graph, and clause grammar has one owner

  • Status: Accepted — ratified by Joshua Shay 2026-07-27. This ADR changes served output. It fixes a truth defect on a serving path; it flips no flag and changes no default. Implemented and merged to main in the same unit that proposed it, because the defect it removes was live behind a shipped flag rather than latent.
  • Date: 2026-07-27 · Ratified: 2026-07-27
  • Implemented by: #137. Enforcement pins: tests/test_negation_survives_articulation.py (registered in the smoke pre-push gate in the same PR).
  • Arc: grammar-unification Phase 5 (docs/plans/grammar-unification-2026-07-26.md), following Phase 4 (#135, #136).
  • Governs: GraphNode.negated, graph_from_intent, ground_graph, plan_articulation (generate/graph_planner.py), generate/intent_bridge.py, the depth-enrichment rebuild in core/cognition/pipeline.py, and the division of labour between generate/semantic_templates.py (discourse framing) and generate/templates.py (clause grammar).
  • Supersedes: the implicit claim in core/cognition/surface_resolution.py that the realize_semantic path, "granted supremacy by the Shadow Coherence Gate", was safe to grant supremacy to. It was not, on any turn carrying a denial.
  • Builds on: ADR-0261 §5.1 (refuse, don't drop) — this is the same failure in a different organ.

1. Context

Phase 4 (#136) recorded that render_semantic has no negated parameter and pinned the consequence as a defect. That pin was correct and incomplete. The defect is not one missing parameter; it is two independent drops in series, and the first one is in the graph.

generate/intent.py has always recovered denial from the user's text (intent.negated, set from the does|do|did not group of _DECLARATIVE_RELATION_RE). Downstream, every stage discarded it:

stage what it did with the denial
graph_from_intent dropped it — GraphNode had no field for it
ground_graph rebuilt nodes field-by-field; nothing to carry
pipeline depth enrichment rebuilt nodes field-by-field; nothing to carry
plan_articulation could not carry what the node did not hold
realize_semantic never read step.negated
render_semantic had no parameter for it

Measured on main @ 536d6e55, with realizer_grounded_authority=True:

"evidence does not support truth"  -> 'Evidence is verified: what supports truth.'
"evidence supports truth"          -> 'Evidence is verified: what supports truth.'

Byte-identical. CORE affirmed what the user denied.

1.1 Why every existing gate was green

On the default config the realizer runs on an ungrounded graph, emits ..., and _is_useful_surface rejects it — so the runtime's echo wins the resolver, and the echo happens to contain the user's own "does not". The truth path was correct by accident. ADR-0088 Phase B removes the accident by grounding the graph before realizing, which is precisely when the realizer's surface becomes useful enough to win.

So the defect sat behind a shipped flag, invisible to every property-of-one- surface test, because no test compared a denial to its assertion. The failure mode is feedback-ask-what-if-the-thing-were-absent in its purest form: the measurement would have looked identical with the mechanism removed, because the mechanism was never there.

2. Decision

R1 — A proposition graph must be able to represent denial. GraphNode gains negated: bool = False, threaded from intent.negated through graph_from_intent, ground_graph, the pipeline's depth enrichment, intent_bridge, and into ArticulationStep via plan_articulation.

Serialized only when True, so every pre-existing as_dict — and every trace_hash folded from one — stays byte-identical. This is why the change lands without touching a lane pin.

R2 — Clause grammar has exactly one owner. Four of the eight intent "templates" were never frames; they were plain clauses ({subject} {predicate_h} {obj}, optionally with a pinned predicate or a prefix). Those now delegate to generate.templates.render_step, the single owner of English clause grammar.

Writing a second copy of negation into semantic_templates.py was the tempting fix and is rejected: Phase 2A spent a whole unit giving every linguistic fact one owner, and a second negation implementation rebuilds that disease one level up. The rejected design would have grown quantifier agreement, then tense, then aspect, until semantic_templates.py was a second grammar.

The delegation is provably output-preserving: 192 of 192 surfaces are byte-identical across all four delegated intents × every predicate in PREDICATE_DISPLAY (plus an unknown one) × every object sentinel. The only surfaces this ADR moves are ones that were wrong.

R3 — A frame that cannot deny must not pretend to assert. Frames retaining a finite verb (VERIFICATION, PROCEDURE, COMPARISON) get an explicit negated form. RECALL — a speech act, with no proposition in it to deny — falls back to the clause path rather than emit a frame that silently drops the denial. Losing the framing is the correct trade against asserting the opposite of the graph (ADR-0261 §5.1).

R4 — An intent with no frame of its own falls back to the UNKNOWN clause, not the UNKNOWN template. The old default sent unframed intents to a format string that cannot say "not". Five live intents were sitting on it — TRANSITIVE_QUERY, FRAME_TRANSFER, NARRATIVE, EXAMPLE, DEDUCTION — each serving a denial as its own assertion. They were found by the exhaustive control in R5, not by inspection. The default must be the capable path so the next intent added inherits correctness.

R5 — The pins assert a difference, and one of them is structural.

  • The load-bearing test compares a denial's served surface to its assertion's, end to end through the real pipeline under realizer_grounded_authority. Asserting a difference is the only shape that catches a writer collapsing a distinction; every property-of-one-surface test was green throughout.
  • An exhaustive control requires every IntentTag to distinguish the two.
  • A structural invariant requires every GraphNode(...) construction in serving code to name negated explicitly or be recorded in an allowlist with a reason. The defect was five separate constructors each defaulting to False; a per-site test must be written per site, and a site added without one is invisible. Only recognition/connector.py is exempt, because an EpistemicNode has no polarity to carry.

3. Consequences

What changes for a user. On turns where the realizer's surface wins the resolver and the input carried a denial, CORE now says "not". Previously it said the opposite. No other turn changes: lane pins are 11/11 byte-identical and smoke/deductive counts move only by the new tests.

What this does not do. quantifier, tense and aspect remain unexpressed on the serving path. That is deliberate, not an oversight: no producer sets them. plan_articulation has no source for them, and no parser populates them, so threading them now would be speculative machinery with no caller — the honest-placeholder discipline. They are expressible the moment a producer exists, because the clause owner already handles all three.

What it costs. render_semantic now imports render_step, so semantic_templates depends on templates. That is the correct direction: framing composes grammar, not the reverse.

Thesis check (decoding, not generating): a graph that cannot represent denial cannot decode a denial — it can only generate an affirmative that resembles the input. Carrying polarity through the graph makes the articulation a function of what was understood. This ADR strengthens the thesis rather than straining it.

4. Alternatives rejected

Add negation handling to semantic_templates.py directly. Smallest diff, and it rebuilds the two-grammar disease Phase 2A removed. Rejected under R2.

Promote realize_target wholesale to the serving path (Phase 4 option (a) in its raw form). Maximum hash churn, and it loses the intent framing and depth notes that realize_semantic genuinely has. The delegation gets the grammar without discarding the framing. Rejected as strictly worse.

Leave it pinned as a defect (the Phase 4 resting state). A documented truth defect with a green test beside it is exactly the comfortable state this arc exists to make uncomfortable. Rejected once the exposure was measured as live rather than latent.

Fail closed — refuse to realize a step carrying a denial. Consistent with ADR-0261, and strictly worse here: the clause owner can already express the denial correctly, so refusing would discard information CORE holds and understands. Refusal is right when expression is impossible, not when it is one delegation away.