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

171 lines
8.9 KiB
Markdown
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.

# 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.