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