fix: align context_peek selector contract with projected schema #376

Closed
nsaspy wants to merge 3 commits from fix/312-peek-schema-contract into main
Owner

Fixes #312.

Problem

The projected context_peek tool schema and the native direct-mode validator disagreed. Auto-Dig's model-emitted call {"selector":{"count":20,"index":0,"type":"head"}} was rejected in native preflight (unexpected_fields([index])), and the previously inspected PR head still accepted {"type":"metadata","index":-1} through native validation even though the schema advertises selector.index with minimum: 0.

Approach: one authoritative local selector contract

prolog/rlm_direct.pl now describes the context_peek selector contract once — peek_selector_types/1, peek_selector_fields/1, peek_selector_required/1, peek_selector_minimum/2, peek_selector_default/2, peek_default_count/1 — and both consumers derive from it:

  • the projected tool schema (peek_selector_schema/1, projected through context_schema(peek, ...)) builds the selector object schema (closed field set, required type enum, integer minima) from those facts;
  • native validation (peek_selector/2) reads the same facts for the allowed field set, enum membership, minima, and defaults.

A conformance test pins the advertised contract and walks the projected schema to probe native validation in both directions, so schema/validator drift fails the deterministic gate instead of diverging at runtime.

Selector contract

  • Allowed selector fields (only these; unknown fields fail closed on every type): type, index, count.
  • type is required; enum metadata | head | tail | item.
  • Numeric bounds whenever a field is present: index >= 0, count >= 1. No selector type bypasses the advertised bounds.
  • Defaults: head/tail with omitted count use the bounded peek_default_count(128); item with omitted index uses 0. metadata ignores index/count semantics; head/tail never treat index as an offset; item never treats count as slicing.
  • Fault surface: boundary violations throw direct_error{phase:schema, kind:malformed_arguments, detail:invalid_selector_field(Field, Minimum)} carrying the advertised minimum; shape violations throw unsupported_selector / invalid_selector / unexpected_fields([...]) as before.

Regression coverage

  • Boundary matrix: every selector type accepts index=0, count=1, and minimal {type} shapes, and rejects index=-1, count=0 (10 accept + 8 reject cases, fault detail asserted against the advertised minimum).
  • Fail-closed regressions: unknown selector type, non-text type value, unknown selector field, missing required type, non-dict selector, non-integer field value.
  • Defaults: head/tail default to head(128)/tail(128) and item to item(0); defaults co-exist with validated present fields; peek_default_count(128) pinned.
  • Schema/validator conformance: projected schema equals the contract facts; documented enum/minima pinned; validator field set equals projected properties; every advertised type is minimally accepted natively; every advertised field's minimum is accepted and minimum - 1 is rejected; boundary faults carry the advertised minimum.
  • End-to-end native direct mode: the exact reported Auto-Dig shape {"count":20,"index":0,"type":"head"} executes through the real direct loop — context_peek is advertised on the first request, the tool-result observation is returned with "truncated":true proving count:20 actually bounded the read (no over-read of the payload) — and the omitted-count default path executes with the full read.

Rebase note

Rebased onto current main (e9b0c18). PR #290 (merged) touches different files; no mechanical or semantic conflict. The replayed commit's duplicate deterministic-corpus entries (main already registers both test files via e474692) were dropped; the corpus inventory diff vs main is now empty.

Verification (local deterministic gate, SWI-Prolog 9.2.9 via the repo flake)

  • test/check_runtime.pl, test/load_all.pl: exit 0
  • scripts/validate_research_approval.pl: PASS (19 tracked files)
  • scripts/design_gate.pl: ALL CHECKS PASSED
  • static load of all live integration test definitions: exit 0
  • test/runner_main_ownership_probe.pl, test/run_runner_integrity_tests.pl: OK
  • test/run_tests.pl: 92 suites, 1092/1092 passed, 0 failed / timeout / blocked
  • benchmark/run.pl -- deterministic: 16/16 passed
  • benchmark/run.pl -- deep-experiment: 15/15 passed
  • bin/prolog-rlm.pl -- demo --json + graph trace smoke: pass
  • fresh-process graph and artifact restart fixtures: pass
  • git diff --check: clean

Non-goals

  • No per-call batch recovery/isolation here (issue #313 / PR #316) — untouched.
  • Typed-plan (rlm_plan.pl) selector validation operates on already-normalized Prolog selectors — separate surface, unchanged.
  • No changes to native tool-call batch recovery policy, effect accounting, context/tool-call budgets, scheduling, model retry behavior, or PR #290 design work.

GitHub Actions on this head is the canonical full gate; the credentialed REAL OpenRouter lane is a live-provider check, not the correctness proof for this deterministic native contract.

Fixes #312. ## Problem The projected `context_peek` tool schema and the native direct-mode validator disagreed. Auto-Dig's model-emitted call `{"selector":{"count":20,"index":0,"type":"head"}}` was rejected in native preflight (`unexpected_fields([index])`), and the previously inspected PR head still accepted `{"type":"metadata","index":-1}` through native validation even though the schema advertises `selector.index` with `minimum: 0`. ## Approach: one authoritative local selector contract `prolog/rlm_direct.pl` now describes the `context_peek` selector contract once — `peek_selector_types/1`, `peek_selector_fields/1`, `peek_selector_required/1`, `peek_selector_minimum/2`, `peek_selector_default/2`, `peek_default_count/1` — and both consumers derive from it: - the projected tool schema (`peek_selector_schema/1`, projected through `context_schema(peek, ...)`) builds the selector object schema (closed field set, required `type` enum, integer minima) from those facts; - native validation (`peek_selector/2`) reads the same facts for the allowed field set, enum membership, minima, and defaults. A conformance test pins the advertised contract and walks the projected schema to probe native validation in both directions, so schema/validator drift fails the deterministic gate instead of diverging at runtime. ## Selector contract - Allowed selector fields (only these; unknown fields fail closed on every type): `type`, `index`, `count`. - `type` is required; enum `metadata | head | tail | item`. - Numeric bounds whenever a field is present: `index >= 0`, `count >= 1`. No selector type bypasses the advertised bounds. - Defaults: `head`/`tail` with omitted `count` use the bounded `peek_default_count(128)`; `item` with omitted `index` uses `0`. `metadata` ignores `index`/`count` semantics; `head`/`tail` never treat `index` as an offset; `item` never treats `count` as slicing. - Fault surface: boundary violations throw `direct_error{phase:schema, kind:malformed_arguments, detail:invalid_selector_field(Field, Minimum)}` carrying the advertised minimum; shape violations throw `unsupported_selector` / `invalid_selector` / `unexpected_fields([...])` as before. ## Regression coverage - Boundary matrix: every selector type accepts `index=0`, `count=1`, and minimal `{type}` shapes, and rejects `index=-1`, `count=0` (10 accept + 8 reject cases, fault detail asserted against the advertised minimum). - Fail-closed regressions: unknown selector type, non-text type value, unknown selector field, missing required type, non-dict selector, non-integer field value. - Defaults: head/tail default to `head(128)`/`tail(128)` and item to `item(0)`; defaults co-exist with validated present fields; `peek_default_count(128)` pinned. - Schema/validator conformance: projected schema equals the contract facts; documented enum/minima pinned; validator field set equals projected properties; every advertised type is minimally accepted natively; every advertised field's minimum is accepted and `minimum - 1` is rejected; boundary faults carry the advertised minimum. - End-to-end native direct mode: the exact reported Auto-Dig shape `{"count":20,"index":0,"type":"head"}` executes through the real direct loop — `context_peek` is advertised on the first request, the tool-result observation is returned with `"truncated":true` proving `count:20` actually bounded the read (no over-read of the payload) — and the omitted-count default path executes with the full read. ## Rebase note Rebased onto current `main` (`e9b0c18`). PR #290 (merged) touches different files; no mechanical or semantic conflict. The replayed commit's duplicate deterministic-corpus entries (main already registers both test files via `e474692`) were dropped; the corpus inventory diff vs `main` is now empty. ## Verification (local deterministic gate, SWI-Prolog 9.2.9 via the repo flake) - `test/check_runtime.pl`, `test/load_all.pl`: exit 0 - `scripts/validate_research_approval.pl`: PASS (19 tracked files) - `scripts/design_gate.pl`: ALL CHECKS PASSED - static load of all live integration test definitions: exit 0 - `test/runner_main_ownership_probe.pl`, `test/run_runner_integrity_tests.pl`: OK - `test/run_tests.pl`: 92 suites, **1092/1092 passed**, 0 failed / timeout / blocked - `benchmark/run.pl -- deterministic`: **16/16** passed - `benchmark/run.pl -- deep-experiment`: **15/15** passed - `bin/prolog-rlm.pl -- demo --json` + graph trace smoke: pass - fresh-process graph and artifact restart fixtures: pass - `git diff --check`: clean ## Non-goals - No per-call batch recovery/isolation here (issue #313 / PR #316) — untouched. - Typed-plan (`rlm_plan.pl`) selector validation operates on already-normalized Prolog selectors — separate surface, unchanged. - No changes to native tool-call batch recovery policy, effect accounting, context/tool-call budgets, scheduling, model retry behavior, or PR #290 design work. GitHub Actions on this head is the canonical full gate; the credentialed REAL OpenRouter lane is a live-provider check, not the correctness proof for this deterministic native contract.
Issue #312: the projected context_peek schema marks only selector.type as
required while advertising index (minimum 0) and count (minimum 1) as
optional shared fields, but native validation rejected calls that omitted
them. Schema projection and native validation now share one contract:

- head/tail: count is optional and defaults to a bounded whole (128)
- item: index is optional and defaults to 0
- metadata: count/index remain optional but are validated when present
- index on head/tail/item rejects negative values matching the schema
- unexpected selector fields still fail closed on every selector type

Also register two deterministic test files that were missing corpus
entries, which made the fail-closed aggregate inventory abort the whole
deterministic gate on main (CI failure issue #303):
- test/rlm_native_any_schema_test.pl
- test/rlm_direct_context_peek_contract_test.pl

Regression coverage: contract tests for defaults, advertised bounds,
schema/validator conformance, and native end-to-end peek execution of the
exact head+count+index shape reported by Auto-Dig consumers.
main (e474692) already registers rlm_direct_context_peek_contract_test.pl
and rlm_native_any_schema_test.pl in the sole deterministic corpus
inventory; the replayed PR commit re-added identical entries. Keep main's
canonical placement and drop the duplicates.
fix(312): derive context_peek schema and validation from one selector contract
Some checks failed
Tree-sitter FFI / Direct SWI-Prolog Tree-sitter FFI (pull_request) Successful in 3m50s
CI / REAL OpenRouter integration (pull_request) Has been skipped
Nix flake / Flake package and clean runtime load (pull_request) Failing after 5s
Clean SWI pack install / Install and load copied pack (pull_request) Successful in 1m10s
CI / Deterministic unit and load checks (pull_request) Successful in 2m40s
Paid OpenRouter / Pinned paid OpenRouter integration (pull_request) Failing after 1m22s
2ce4a5035b
The rebased PR head still left a hole: the metadata selector validated
count when present but not index, so {"type":"metadata","index":-1}
passed native validation while the projected schema advertises
selector.index with minimum 0.

The selector contract is now one authoritative local description in
rlm_direct.pl (types, allowed fields, required list, per-field minima,
bounded defaults) from which both the projected tool schema
(peek_selector_schema/1 via context_schema(peek,...)) and native
validation (peek_selector/2) are derived:

- metadata/head/tail/item validate index >= 0 and count >= 1 whenever
  the field is present, with the advertised minimum carried in the
  invalid_selector_field(Field, Minimum) fault detail
- head/tail keep the bounded peek_default_count(128) default for omitted
  count and never treat index as an offset
- item keeps index default 0 and never treats count as slicing
- unknown selector fields, unknown types, wrong primitive types, missing
  type, and non-dict selectors all fail closed

Contract tests pin the advertised schema, walk the projected schema to
probe native enforcement in both directions, and cover the full selector
boundary matrix plus the direct-mode round trip of the exact Auto-Dig
shape {count:20,index:0,type:"head"} and the omitted-count default.
Author
Owner

CI triage note: the failing Pinned paid OpenRouter integration lane is the recurring live-provider failure documented in #291, #287, #281, #293, #302, and #307. The failing jobs are live planner/managed-turn tests (real_planner_uses_opaque_context_and_registered_tool_without_fixed_plan → plan_error{tool_result_envelope_field(content,runtime)}, and the 40000-message needle retrieval test), neither of which touches the context_peek selector contract or the deterministic corpus changed here.

The repository-required deterministic gate is green on this PR: Deterministic unit and load checks pass, matching the local run (1077/1077, exit 0). The paid lane failure also reproduces on other branches and main, so it is not introduced by this change; it is tracked by the automated failure reports (this branch: #315).

CI triage note: the failing `Pinned paid OpenRouter integration` lane is the recurring live-provider failure documented in #291, #287, #281, #293, #302, and #307. The failing jobs are live planner/managed-turn tests (`real_planner_uses_opaque_context_and_registered_tool_without_fixed_plan` → `plan_error{tool_result_envelope_field(content,runtime)}`, and the 40000-message needle retrieval test), neither of which touches the `context_peek` selector contract or the deterministic corpus changed here. The repository-required deterministic gate is green on this PR: `Deterministic unit and load checks` pass, matching the local run (1077/1077, exit 0). The paid lane failure also reproduces on other branches and `main`, so it is not introduced by this change; it is tracked by the automated failure reports (this branch: #315).
Author
Owner

Rebased onto current main and completed. Final state:

  • HEAD SHA: 2ce4a5035b (fix/312-peek-schema-contract; commits: replayed 255f65d → corpus-dedup 55ad2dc → contract completion 2ce4a50)
  • base/main SHA: e9b0c187fd
  • RED regression observed (pre-fix branch state, recorded before the production change): every_advertised_field_bound_is_natively_enforced failed with \+selector_outcome(_{index:-1,type:"metadata"},ok) — i.e. {"type":"metadata","index":-1} was accepted by native validation although the projected schema advertises index minimum:0; 9 plunit tests failed in that run (log retained locally). The metadata-index hole was live on the rebased PR head.
  • Focused tests: test/rlm_direct_context_peek_contract_test.pl (26 units: 10 accept + 8 reject boundary instances, 6 fail-closed shapes, defaults incl. peek_default_count(128), 7 schema/validator conformance tests) and test/rlm_direct_test.pl rlm_direct suite (27 units incl. the end-to-end Auto-Dig shape {"count":20,"index":0,"type":"head"} round trip and the omitted-count default round trip) — all green.
  • Full deterministic gate (all exit 0, SWI-Prolog 9.2.9 via repo flake): check_runtime.pl, load_all.pl, validate_research_approval.pl (PASS, 19 files), design_gate.pl (ALL CHECKS PASSED), static load of all live definitions, runner_main_ownership_probe.pl, run_runner_integrity_tests.pl, test/run_tests.pl — 92 suites, 1092/1092 passed, 0 failed/timeout/blocked; fresh-process graph and artifact restart fixtures pass.
  • Benchmark/conformance result: benchmark/run.pl -- deterministic 16/16 passed; benchmark/run.pl -- deep-experiment 15/15 passed.
  • git diff --check: clean (no whitespace errors).
  • GitHub reports PR #314 MERGEABLE against main at this head (mergeStateStatus reflects CI in flight on the new head).

Scope guard: #313/PR #316 (per-call batch recovery/isolation) is not touched; rlm_plan.pl, effect accounting, budgets, scheduling, retry policy, and PR #290 artifacts are unchanged. The deterministic corpus diff vs main is empty (duplicate replayed entries removed).

Rebased onto current `main` and completed. Final state: - **HEAD SHA**: 2ce4a5035b106f9bef78ef903d8727fab38fc5f9 (`fix/312-peek-schema-contract`; commits: replayed 255f65d → corpus-dedup 55ad2dc → contract completion 2ce4a50) - **base/main SHA**: e9b0c187fd0f776d2b511f06685029ab436771d0 - **RED regression observed** (pre-fix branch state, recorded before the production change): `every_advertised_field_bound_is_natively_enforced` failed with `\+selector_outcome(_{index:-1,type:"metadata"},ok)` — i.e. `{"type":"metadata","index":-1}` was **accepted** by native validation although the projected schema advertises `index` `minimum:0`; 9 plunit tests failed in that run (log retained locally). The metadata-index hole was live on the rebased PR head. - **Focused tests**: `test/rlm_direct_context_peek_contract_test.pl` (26 units: 10 accept + 8 reject boundary instances, 6 fail-closed shapes, defaults incl. `peek_default_count(128)`, 7 schema/validator conformance tests) and `test/rlm_direct_test.pl` rlm_direct suite (27 units incl. the end-to-end Auto-Dig shape `{"count":20,"index":0,"type":"head"}` round trip and the omitted-count default round trip) — all green. - **Full deterministic gate** (all exit 0, SWI-Prolog 9.2.9 via repo flake): `check_runtime.pl`, `load_all.pl`, `validate_research_approval.pl` (PASS, 19 files), `design_gate.pl` (ALL CHECKS PASSED), static load of all live definitions, `runner_main_ownership_probe.pl`, `run_runner_integrity_tests.pl`, `test/run_tests.pl` — 92 suites, **1092/1092 passed**, 0 failed/timeout/blocked; fresh-process graph and artifact restart fixtures pass. - **Benchmark/conformance result**: `benchmark/run.pl -- deterministic` **16/16 passed**; `benchmark/run.pl -- deep-experiment` **15/15 passed**. - **git diff --check**: clean (no whitespace errors). - GitHub reports PR #314 `MERGEABLE` against `main` at this head (mergeStateStatus reflects CI in flight on the new head). Scope guard: #313/PR #316 (per-call batch recovery/isolation) is **not** touched; `rlm_plan.pl`, effect accounting, budgets, scheduling, retry policy, and PR #290 artifacts are unchanged. The deterministic corpus diff vs `main` is empty (duplicate replayed entries removed).
nsaspy closed this pull request 2026-09-04 22:55:57 +00:00
Some checks failed
Tree-sitter FFI / Direct SWI-Prolog Tree-sitter FFI (pull_request) Successful in 3m50s
CI / REAL OpenRouter integration (pull_request) Has been skipped
Nix flake / Flake package and clean runtime load (pull_request) Failing after 5s
Clean SWI pack install / Install and load copied pack (pull_request) Successful in 1m10s
CI / Deterministic unit and load checks (pull_request) Successful in 2m40s
Paid OpenRouter / Pinned paid OpenRouter integration (pull_request) Failing after 1m22s

Pull request closed

Sign in to join this conversation.
No description provided.