grounding-gate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,31 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest]
15
+ python-version: ["3.9", "3.13"]
16
+ include:
17
+ - os: windows-latest
18
+ python-version: "3.13"
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ - name: Install
25
+ run: pip install -e ".[dev]"
26
+ - name: Test suite
27
+ run: pytest -q
28
+ - name: Zero-dependency check (tests run on bare python)
29
+ run: python tests/test_gate.py
30
+ - name: Demo
31
+ run: python examples/demo.py
@@ -0,0 +1,44 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.13"
15
+ - name: Test
16
+ run: |
17
+ pip install -e ".[dev]"
18
+ pytest -q
19
+ - name: Build sdist and wheel
20
+ run: |
21
+ pip install build twine
22
+ python -m build
23
+ twine check dist/*
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ publish:
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ # Trusted Publishing (OIDC): configure this repo + workflow + environment
33
+ # as a (pending) publisher for the project on pypi.org — no API token.
34
+ environment:
35
+ name: pypi
36
+ url: https://pypi.org/p/grounding-gate
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .idea/
10
+ .vscode/
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-07-10
4
+
5
+ Initial release.
6
+
7
+ - `classify_observation` — novelty ∧ relevance ∧ consequence-tier observation
8
+ classifier (corrected C1/C3 behavior preserved from review).
9
+ - `boundary_check` / `turn_loop` — submit-boundary choke point and reference
10
+ loop wiring (latched flags, qualifying-only halt clear, starving refusals).
11
+ - `GateState.for_model_class` with `skipper` / `diverger` / `default` presets,
12
+ including strict-G enforcement for the skipper class (assertions require
13
+ verified-tier grounding).
14
+ - 19-case acceptance suite, runnable on bare Python with zero dependencies.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ciphemon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: grounding-gate
3
+ Version: 0.1.0
4
+ Summary: Zero-token structural verifier for agent loops: one choke point at the submit boundary enforcing grounding and budget invariants. No LLM calls, stdlib only.
5
+ Project-URL: Homepage, https://github.com/CiphemonJY/grounding-gate
6
+ Project-URL: Repository, https://github.com/CiphemonJY/grounding-gate
7
+ Project-URL: Issues, https://github.com/CiphemonJY/grounding-gate/issues
8
+ Project-URL: Changelog, https://github.com/CiphemonJY/grounding-gate/blob/main/CHANGELOG.md
9
+ Author: Ciphemon
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-loop,agents,grounding,guardrails,hallucination,llm,verification
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # grounding-gate
28
+
29
+ [![ci](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml)
30
+
31
+ **Zero-token structural verifier for agent loops.** One choke point at the
32
+ submit boundary decides whether an agent is allowed to say "X is true" or
33
+ "I did X" — using hash, set, and integer operations only. No LLM calls, no
34
+ per-turn prompt injection, no dependencies.
35
+
36
+ ```
37
+ pip install grounding-gate # stdlib only, Python >= 3.9
38
+ ```
39
+
40
+ The demo ships in the repo (not the wheel):
41
+
42
+ ```
43
+ git clone https://github.com/CiphemonJY/grounding-gate && cd grounding-gate
44
+ python examples/demo.py # the whole idea in 30 seconds
45
+ ```
46
+
47
+ ## The problem
48
+
49
+ Agents fail in two characteristic ways, and both ship *confident* wrong answers:
50
+
51
+ - **Skip-and-hallucinate** — emit a terminal claim ("done, config fixed")
52
+ without ever observing reality after acting on it.
53
+ - **Reason-and-diverge** — loop in closed context, burning steps on
54
+ reasoning about stale beliefs, until a confident wrong answer ships.
55
+
56
+ The standard fix is prose: "remember to verify your work" injected into every
57
+ turn. Prose costs tokens on every turn, behaves differently per model, and —
58
+ critically — is *skippable*. A reminder is not an invariant.
59
+
60
+ ## The idea
61
+
62
+ Move enforcement out of the prompt and into **control flow**. A single gate
63
+ wraps the submit/conclude boundary, and a terminal output is emitted only if
64
+ both invariants hold:
65
+
66
+ - **G (grounding)** — a *qualifying* observation happened this turn, or the
67
+ output makes no factual claim. Qualifying means **novel** (result hash not
68
+ seen before, after stripping timestamps/ids) **∧ relevant** (touches the
69
+ identifiers the claim is about) **∧ consequence-tier-correct** (see below).
70
+ - **B (budget)** — reasoning rope remains. Qualifying observations *refill*
71
+ the budget (up to a cap); pure reasoning steps decrement it. Grounded work
72
+ runs effectively unbounded; closed-loop reasoning hits a hard floor.
73
+
74
+ Fail either → the terminal is **rejected** and the agent is told its only
75
+ legal moves: make a qualifying tool call, or exit with a typed **`unverified`**
76
+ terminal. `unverified` is a first-class, always-legal escape hatch — the gate
77
+ never traps an agent, it only forbids *confident* ungrounded claims.
78
+
79
+ ### Consequence tiers
80
+
81
+ The gate distinguishes what kind of claim an observation can support:
82
+
83
+ | Claim type | Example | Requires |
84
+ |--------------|--------------------------|----------|
85
+ | `assertion` | "X is true" | a novel, relevant, read-only observation this turn |
86
+ | `completion` | "I changed X" | a novel, relevant read taken **after** the mutation — a mutating call never self-grounds its own effect |
87
+ | `unverified` | "couldn't confirm X" | nothing — always legal |
88
+ | `none` | no factual claim | nothing — exempt |
89
+
90
+ That second row is the heart of it: *writing a file and claiming success is
91
+ not verification; reading it back afterwards is.*
92
+
93
+ ## Quickstart
94
+
95
+ ```python
96
+ from grounding_gate import GateState, classify_observation, boundary_check
97
+
98
+ state = GateState.for_model_class("default", claim_surface={"app.cfg"})
99
+
100
+ # after EVERY tool call in your agent loop:
101
+ state.current_step += 1
102
+ obs = classify_observation(tool, args, result, state, read_only=not mutating)
103
+ state.grounded_this_turn |= obs["grounds_assertion"]
104
+ state.verified_this_turn |= obs["grounds_completion"]
105
+ if mutating:
106
+ state.last_mutation_step = state.current_step # a completion now needs a read AFTER this
107
+
108
+ # at every submit/conclude attempt — this must be the ONLY path to output:
109
+ verdict = boundary_check({"claim_type": "completion", "content": answer}, state)
110
+ if verdict["verdict"] == "REJECT":
111
+ ... # surface verdict["legal_next"] to the model and continue the loop
112
+ ```
113
+
114
+ Note the mutation bookkeeping: without `last_mutation_step` ever being set, no
115
+ read can reach the verified tier and a `completion` can never be accepted —
116
+ that is the gate working as designed, not a bug.
117
+
118
+ `turn_loop` in [boundary.py](https://github.com/CiphemonJY/grounding-gate/blob/main/src/grounding_gate/boundary.py)
119
+ is the complete reference wiring (budget refill, mutation tracking, halt
120
+ semantics, signal mapping) — use it as the integration template. The
121
+ [demo](https://github.com/CiphemonJY/grounding-gate/blob/main/examples/demo.py)
122
+ runs the same scripted agent through an ungated and a gated loop, side by side.
123
+
124
+ ## Model-class presets
125
+
126
+ Fleet variance is absorbed as integers, not prose. Pick the preset matching
127
+ how your model fails:
128
+
129
+ | Preset | CAP | REFILL | Strict G | For |
130
+ |------------|-----|--------|----------|-----|
131
+ | `skipper` | 5 | 2 | yes | models that hallucinate-and-skip |
132
+ | `diverger` | 4 | 1 | no | models that reason forever |
133
+ | `default` | 6 | 2 | no | everything else |
134
+
135
+ Strict G means even plain *assertions* require verified-tier grounding (a
136
+ post-mutation observation) — an observed-tier read is not enough. In a task
137
+ that never mutates anything, a strict-G agent can only exit via the typed
138
+ `unverified` terminal; that hard line is the point of the skipper preset, so
139
+ pick `default` for read-only/Q&A workloads.
140
+
141
+ ## Declarative rails
142
+
143
+ A task can declare signals that must be verified before any completion is
144
+ accepted (`state.goal_predicates = ["tests_passed"]`). The gate never
145
+ interprets meaning — it only checks that a signal named `tests_passed` was
146
+ registered by a mapped, real command outcome. Semantic judgment stays out of
147
+ the floor by design.
148
+
149
+ ## What the gate does NOT do
150
+
151
+ Honest scope, from the design's leak audit:
152
+
153
+ - **No semantic correctness.** A grounded claim can still be wrong (the model
154
+ can misread a real result). That is punted to a declared verifier tier
155
+ (`verify_with`), not smuggled into the floor.
156
+ - **Relevance can be spoofed** by a model that deliberately mentions the right
157
+ identifiers in an irrelevant call. The floor defends against *lazy*
158
+ ungroundedness, which is the overwhelmingly common failure; adversarial
159
+ self-deception needs the verifier tier.
160
+ - **Nondeterministic tools** need per-tool `normalize()` extensions or the
161
+ audited `novelty_exempt` allowlist — too-weak normalization quietly disables
162
+ the no-op defense.
163
+
164
+ ## How this was built
165
+
166
+ The modules were drafted by different LLMs and adversarially reviewed before
167
+ assembly; the final behavior is pinned by a 19-case acceptance suite
168
+ ([tests/test_gate.py](https://github.com/CiphemonJY/grounding-gate/blob/main/tests/test_gate.py))
169
+ that runs on bare Python with zero dependencies. Two review findings shaped
170
+ the method and are preserved in the docstrings:
171
+
172
+ - A drafting model shipped a consequence-tier bug **and authored the test that
173
+ ratified it** — since then, expected outcomes are authored by the reviewer,
174
+ never by the generator
175
+ ([docs/module-2-classifier.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-2-classifier.md)).
176
+ - The remaining leaks lived *between* individually-passing test cases —
177
+ latch-vs-assignment, halt cleared by non-qualifying calls
178
+ ([docs/module-4-boundary.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-4-boundary.md)).
179
+
180
+ Full design spec:
181
+ [docs/spec.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/spec.md).
182
+
183
+ ## Status & roadmap
184
+
185
+ This is the reference implementation — correct, minimal, and framework-free.
186
+ Planned next:
187
+
188
+ - Adapters: Claude Agent SDK hook, LangGraph middleware, OpenAI Agents SDK.
189
+ - A real signal-mapper module (command exit code → declared signal).
190
+ - Empirical preset tuning across model classes.
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,168 @@
1
+ # grounding-gate
2
+
3
+ [![ci](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml)
4
+
5
+ **Zero-token structural verifier for agent loops.** One choke point at the
6
+ submit boundary decides whether an agent is allowed to say "X is true" or
7
+ "I did X" — using hash, set, and integer operations only. No LLM calls, no
8
+ per-turn prompt injection, no dependencies.
9
+
10
+ ```
11
+ pip install grounding-gate # stdlib only, Python >= 3.9
12
+ ```
13
+
14
+ The demo ships in the repo (not the wheel):
15
+
16
+ ```
17
+ git clone https://github.com/CiphemonJY/grounding-gate && cd grounding-gate
18
+ python examples/demo.py # the whole idea in 30 seconds
19
+ ```
20
+
21
+ ## The problem
22
+
23
+ Agents fail in two characteristic ways, and both ship *confident* wrong answers:
24
+
25
+ - **Skip-and-hallucinate** — emit a terminal claim ("done, config fixed")
26
+ without ever observing reality after acting on it.
27
+ - **Reason-and-diverge** — loop in closed context, burning steps on
28
+ reasoning about stale beliefs, until a confident wrong answer ships.
29
+
30
+ The standard fix is prose: "remember to verify your work" injected into every
31
+ turn. Prose costs tokens on every turn, behaves differently per model, and —
32
+ critically — is *skippable*. A reminder is not an invariant.
33
+
34
+ ## The idea
35
+
36
+ Move enforcement out of the prompt and into **control flow**. A single gate
37
+ wraps the submit/conclude boundary, and a terminal output is emitted only if
38
+ both invariants hold:
39
+
40
+ - **G (grounding)** — a *qualifying* observation happened this turn, or the
41
+ output makes no factual claim. Qualifying means **novel** (result hash not
42
+ seen before, after stripping timestamps/ids) **∧ relevant** (touches the
43
+ identifiers the claim is about) **∧ consequence-tier-correct** (see below).
44
+ - **B (budget)** — reasoning rope remains. Qualifying observations *refill*
45
+ the budget (up to a cap); pure reasoning steps decrement it. Grounded work
46
+ runs effectively unbounded; closed-loop reasoning hits a hard floor.
47
+
48
+ Fail either → the terminal is **rejected** and the agent is told its only
49
+ legal moves: make a qualifying tool call, or exit with a typed **`unverified`**
50
+ terminal. `unverified` is a first-class, always-legal escape hatch — the gate
51
+ never traps an agent, it only forbids *confident* ungrounded claims.
52
+
53
+ ### Consequence tiers
54
+
55
+ The gate distinguishes what kind of claim an observation can support:
56
+
57
+ | Claim type | Example | Requires |
58
+ |--------------|--------------------------|----------|
59
+ | `assertion` | "X is true" | a novel, relevant, read-only observation this turn |
60
+ | `completion` | "I changed X" | a novel, relevant read taken **after** the mutation — a mutating call never self-grounds its own effect |
61
+ | `unverified` | "couldn't confirm X" | nothing — always legal |
62
+ | `none` | no factual claim | nothing — exempt |
63
+
64
+ That second row is the heart of it: *writing a file and claiming success is
65
+ not verification; reading it back afterwards is.*
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ from grounding_gate import GateState, classify_observation, boundary_check
71
+
72
+ state = GateState.for_model_class("default", claim_surface={"app.cfg"})
73
+
74
+ # after EVERY tool call in your agent loop:
75
+ state.current_step += 1
76
+ obs = classify_observation(tool, args, result, state, read_only=not mutating)
77
+ state.grounded_this_turn |= obs["grounds_assertion"]
78
+ state.verified_this_turn |= obs["grounds_completion"]
79
+ if mutating:
80
+ state.last_mutation_step = state.current_step # a completion now needs a read AFTER this
81
+
82
+ # at every submit/conclude attempt — this must be the ONLY path to output:
83
+ verdict = boundary_check({"claim_type": "completion", "content": answer}, state)
84
+ if verdict["verdict"] == "REJECT":
85
+ ... # surface verdict["legal_next"] to the model and continue the loop
86
+ ```
87
+
88
+ Note the mutation bookkeeping: without `last_mutation_step` ever being set, no
89
+ read can reach the verified tier and a `completion` can never be accepted —
90
+ that is the gate working as designed, not a bug.
91
+
92
+ `turn_loop` in [boundary.py](https://github.com/CiphemonJY/grounding-gate/blob/main/src/grounding_gate/boundary.py)
93
+ is the complete reference wiring (budget refill, mutation tracking, halt
94
+ semantics, signal mapping) — use it as the integration template. The
95
+ [demo](https://github.com/CiphemonJY/grounding-gate/blob/main/examples/demo.py)
96
+ runs the same scripted agent through an ungated and a gated loop, side by side.
97
+
98
+ ## Model-class presets
99
+
100
+ Fleet variance is absorbed as integers, not prose. Pick the preset matching
101
+ how your model fails:
102
+
103
+ | Preset | CAP | REFILL | Strict G | For |
104
+ |------------|-----|--------|----------|-----|
105
+ | `skipper` | 5 | 2 | yes | models that hallucinate-and-skip |
106
+ | `diverger` | 4 | 1 | no | models that reason forever |
107
+ | `default` | 6 | 2 | no | everything else |
108
+
109
+ Strict G means even plain *assertions* require verified-tier grounding (a
110
+ post-mutation observation) — an observed-tier read is not enough. In a task
111
+ that never mutates anything, a strict-G agent can only exit via the typed
112
+ `unverified` terminal; that hard line is the point of the skipper preset, so
113
+ pick `default` for read-only/Q&A workloads.
114
+
115
+ ## Declarative rails
116
+
117
+ A task can declare signals that must be verified before any completion is
118
+ accepted (`state.goal_predicates = ["tests_passed"]`). The gate never
119
+ interprets meaning — it only checks that a signal named `tests_passed` was
120
+ registered by a mapped, real command outcome. Semantic judgment stays out of
121
+ the floor by design.
122
+
123
+ ## What the gate does NOT do
124
+
125
+ Honest scope, from the design's leak audit:
126
+
127
+ - **No semantic correctness.** A grounded claim can still be wrong (the model
128
+ can misread a real result). That is punted to a declared verifier tier
129
+ (`verify_with`), not smuggled into the floor.
130
+ - **Relevance can be spoofed** by a model that deliberately mentions the right
131
+ identifiers in an irrelevant call. The floor defends against *lazy*
132
+ ungroundedness, which is the overwhelmingly common failure; adversarial
133
+ self-deception needs the verifier tier.
134
+ - **Nondeterministic tools** need per-tool `normalize()` extensions or the
135
+ audited `novelty_exempt` allowlist — too-weak normalization quietly disables
136
+ the no-op defense.
137
+
138
+ ## How this was built
139
+
140
+ The modules were drafted by different LLMs and adversarially reviewed before
141
+ assembly; the final behavior is pinned by a 19-case acceptance suite
142
+ ([tests/test_gate.py](https://github.com/CiphemonJY/grounding-gate/blob/main/tests/test_gate.py))
143
+ that runs on bare Python with zero dependencies. Two review findings shaped
144
+ the method and are preserved in the docstrings:
145
+
146
+ - A drafting model shipped a consequence-tier bug **and authored the test that
147
+ ratified it** — since then, expected outcomes are authored by the reviewer,
148
+ never by the generator
149
+ ([docs/module-2-classifier.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-2-classifier.md)).
150
+ - The remaining leaks lived *between* individually-passing test cases —
151
+ latch-vs-assignment, halt cleared by non-qualifying calls
152
+ ([docs/module-4-boundary.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-4-boundary.md)).
153
+
154
+ Full design spec:
155
+ [docs/spec.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/spec.md).
156
+
157
+ ## Status & roadmap
158
+
159
+ This is the reference implementation — correct, minimal, and framework-free.
160
+ Planned next:
161
+
162
+ - Adapters: Claude Agent SDK hook, LangGraph middleware, OpenAI Agents SDK.
163
+ - A real signal-mapper module (command exit code → declared signal).
164
+ - Empirical preset tuning across model classes.
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,75 @@
1
+ # Module 2 — Observation classifier (corrected)
2
+
3
+ The classifier decides whether one completed tool call `(tool, args, result)` grounds a
4
+ claim. A call qualifies only if **novel ∧ relevant ∧ consequence-tier-correct**.
5
+
6
+ ## Review history — and why it shaped the method
7
+
8
+ The first draft of this module was produced by one model and reviewed by another. The
9
+ review found:
10
+
11
+ - **(C1)** The consequence tier let a plain read ground a **completion** ("I changed X")
12
+ when nothing had ever mutated: with `last_mutation_step = 0`, the step comparison was
13
+ trivially true. The fix requires a mutation to have *occurred*
14
+ (`last_mutation_step > 0`) before any read can count as post-mutation verification.
15
+ - **(C2)** The drafting model had also **authored the test case that ratified its own
16
+ bug** — its test #1 expected `{true, true}` for a read with no prior mutation. The
17
+ expectation was corrected to `{true, false}`.
18
+ - **(C3)** The draft recorded the novelty hash before the relevance check — a
19
+ novel-but-irrelevant read burned its hash and was wrongly denied credit if the same
20
+ read later became relevant. The hash is now recorded only after the relevance gate
21
+ passes.
22
+
23
+ C2 is the important lesson, and it became a standing rule for this project: **expected
24
+ outcomes are authored by the reviewer, never by the generator.** A model grading its own
25
+ work will happily certify its own bug.
26
+
27
+ ## Corrected pseudocode
28
+
29
+ ```text
30
+ function classify_observation(tool, args, result, state) -> {grounds_assertion, grounds_completion}:
31
+ ret = {grounds_assertion: false, grounds_completion: false}
32
+
33
+ # 1. NOVELTY (check only; do not record yet — see C3)
34
+ h = null
35
+ if tool not in state.novelty_exempt:
36
+ h = hash(tool + normalize(args) + normalize(result))
37
+ if h in state.recent_result_hashes:
38
+ return ret
39
+
40
+ # 2. RELEVANCE
41
+ identifiers = extract_identifiers(args, result)
42
+ if not intersects(identifiers, state.claim_surface):
43
+ return ret
44
+
45
+ # record novelty only for calls that actually qualify (C3)
46
+ if h != null:
47
+ state.recent_result_hashes.add(h)
48
+
49
+ # 3. CONSEQUENCE TIER
50
+ if is_read_only(tool):
51
+ ret.grounds_assertion = true
52
+ # a completion needs a mutation to have OCCURRED and this read to follow it (C1)
53
+ if state.last_mutation_step > 0 and state.current_step > state.last_mutation_step:
54
+ ret.grounds_completion = true
55
+
56
+ return ret
57
+ ```
58
+
59
+ ## Acceptance cases
60
+
61
+ | # | Case | Expected |
62
+ |---|------|----------|
63
+ | T1 | read with NO prior mutation | `{true, false}` ← corrected (C1/C2) |
64
+ | T2 | repeated identical read (novelty defeat) | `{false, false}` |
65
+ | T3 | unrelated file touched (relevance defeat) | `{false, false}` |
66
+ | T4 | mutating call claiming its own effect (consequence defeat) | `{false, false}` |
67
+ | T5 | read predating the mutation | `{true, false}` |
68
+ | T6 | post-mutation re-read (valid verified) | `{true, true}` |
69
+
70
+ ## Open implementation risks (flagged, not resolved here)
71
+
72
+ - `normalize()` must strip timestamps/rng or novelty never fires on nondeterministic
73
+ tools; `novelty_exempt` is a small audited allowlist.
74
+ - `extract_identifiers()` must be conservative — over-extraction leaks relevance,
75
+ under-extraction false-rejects cross-cutting work.
@@ -0,0 +1,76 @@
1
+ # Module 4 — Submit-boundary choke point (corrected)
2
+
3
+ `boundary_check` is the single decision that matters: it must be the **sole path to any
4
+ terminal output**. `turn_loop` is the reference wiring of the whole gate around an agent.
5
+
6
+ ## Review history — leaks live BETWEEN the test cases
7
+
8
+ The draft of this module passed all 8 authored boundary cases (R1–A3) on the first try —
9
+ `boundary_check` itself survived review unchanged. Every bug was in `turn_loop`, in the
10
+ seams the per-case suite didn't cover:
11
+
12
+ - **(C1)** The draft assigned grounding flags from the *last* call
13
+ (`verified_this_turn = obs.grounds_completion`), so any later non-completion-grade call
14
+ overwrote a valid verification back to `false` — false-rejecting legitimate work.
15
+ Flags are **latches** within a turn (`|=`), cleared only at turn boundaries.
16
+ - **(C2)** The draft cleared `halted` on *any* tool call — a halted model could escape via
17
+ a novelty-defeated no-op read, reintroducing the no-op gaming vector at loop level.
18
+ Halt is cleared **only by a qualifying observation** (or a typed `unverified` exit).
19
+ - **(C3)** The draft invented an `obs.signals` field that Module 2 does not return.
20
+ Signal population goes through the declarative-rails signal mapper (declared command
21
+ outcome → signal name), never through the classifier.
22
+ - **(C4)** Refused reasoning while halted must surface `legal_next` to the agent
23
+ (re-prompt) or the loop livelocks; refusals also decrement budget, so a halted spinner
24
+ still starves its way to the typed `unverified` exit.
25
+
26
+ The takeaway generalizes: a module can score 100% on its authored acceptance cases and
27
+ still leak — integration cases (L1–L3 below) exist precisely to cover the seams.
28
+
29
+ ## Corrected pseudocode
30
+
31
+ ```text
32
+ function boundary_check(terminal_attempt, state):
33
+ ct = terminal_attempt.claim_type # none | assertion | completion | unverified
34
+
35
+ if ct == unverified: # universal escape hatch (typed, not content-matched)
36
+ state.halted = false
37
+ return { verdict: ACCEPT, legal_next: [] }
38
+
39
+ is_claim_bearing = (ct == assertion) OR (ct == completion)
40
+
41
+ if is_claim_bearing AND state.budget <= 0:
42
+ state.halted = true
43
+ return { verdict: REJECT, legal_next: [qualifying_tool_call, unverified_terminal] }
44
+
45
+ if ct == completion:
46
+ if NOT state.verified_this_turn:
47
+ reject as above
48
+ for sig in state.goal_predicates:
49
+ if sig NOT in state.verified_signals:
50
+ reject as above
51
+ state.halted = false
52
+ return { verdict: ACCEPT, legal_next: [] }
53
+
54
+ if ct == assertion:
55
+ grounded = state.verified_this_turn if state.strict_g # strict G (module 6):
56
+ else state.grounded_this_turn # skipper preset requires
57
+ if NOT grounded: # verified tier even here
58
+ reject as above
59
+ state.halted = false
60
+ return { verdict: ACCEPT, legal_next: [] }
61
+
62
+ return { verdict: ACCEPT, legal_next: [] } # ct == none: exempt
63
+ ```
64
+
65
+ `turn_loop` wiring (see [boundary.py](../src/grounding_gate/boundary.py) for the runnable
66
+ version): reasoning decrements budget (refused or not); tool calls are classified, latch
67
+ the flags, refill budget and clear halt only when qualifying, and record mutation steps;
68
+ terminal attempts go through `boundary_check`, and an ACCEPT is the loop's only emit.
69
+
70
+ ## Integration cases the corrections cover
71
+
72
+ | # | Case | Verdict |
73
+ |---|------|---------|
74
+ | L1 | edit → re-read (verified) → novelty-defeated repeat read → completion | ACCEPT (was falsely REJECTED pre-C1) |
75
+ | L2 | halted → novelty-defeated repeat read → reasoning | reasoning still refused (was allowed pre-C2) |
76
+ | L3 | halted → reasoning × N | budget starves to typed `unverified` (was free spin pre-C4) |