personal memory agent

feat(schemas): pre-send provider schema preparation, bounds ratchet, local eval harness master

Foundation for the KG schema enrichment arc. Canonical *.schema.json files may now carry maxItems/maxLength generation bounds; providers that cannot accept a given keyword receive a deterministic, pre-send reduced copy. No shipped schema is bounded in this lode — the framework is proven by fixture and by the guardrail ratchet. Entry point: solstone/think/schema_prep.py::prepare_provider_schema Eval harness: make eval-schemas D1 — per-provider support matrix (deviates from a blanket cloud strip). Current provider docs say OpenAI and Google ACCEPT minItems/maxItems/ minimum/maximum; only minLength/maxLength are outside their supported subset. Anthropic rejects numeric/string/array constraints beyond minItems 0/1, so all six bounds are stripped there. pattern is never stripped for any provider. $schema/$comment are stripped for all three cloud providers. local and any unregistered provider name receive a canonical deep copy; prep never raises on an unknown provider. STRICT_UNSUPPORTED_KEYWORDS carries the doc citations and is the single source of truth — the portability test imports it rather than restating the list. Because today's 22 discovered generation schemas carry zero bounds, every outgoing payload is byte-identical to before this change; a parametrized test asserts that over all schemas x all providers. D2 — the transform drops only validation-only keywords, recursively. It is pure, deep-copying, idempotent, and structure-preserving: required, additionalProperties, type/nullability, enum, const, pattern, format, items and descriptions are never touched. No runtime try-then-fallback was added; c78f9ff5 deleted exactly that, and the reduction here is deterministic and pre-send. local.py::_normalize_schema_patterns stays where it is as a llama.cpp transport detail. D4 — response validation stays CANONICAL. All three models.py call sites (generate, generate_with_result, agenerate) hand the prepared schema to the provider and keep validating the response against the canonical schema. HAZARD for follow-on lodes: for anthropic, bounds are stripped from the request but still enforced on the response, so once shipped schemas carry maxItems/maxLength an over-long Anthropic response raises SchemaValidationError. That is a deliberate fail-loudly choice (CLAUDE.md §8), and it is covered by test_generate_validates_response_against_canonical_schema. Also in this commit: - tests/test_schema_strict_portability.py rewritten. violations() now checks only the structural rules prep does not and must not fix (root object, additionalProperties:false, all properties required, no oneOf). The banned-key assertion moved to the provider-facing schema, per provider, so maxItems legitimately survives for openai/google. Zero-facet hydration guard retained (hydrate, then prep). Negative self-check strengthened. - solstone/think/schema_bounds.py::unbounded_nodes — reusable deny-by-default helper: arrays without maxItems, and free-text strings (no enum/const/ pattern/format) without maxLength. - scripts/check_schema_bounds.py — ratchet wired into make install-checks, following the check_call_http_only.py evaluate()->(new, stale, tracked) idiom. 22 reasoned allowlist entries; stale entries that no longer violate fail the gate so the allowlist cannot rot. daily_schedule.schema.json is already clean and is deliberately absent. Each follow-on lode deletes its entry. - tests/eval_schemas.py + make eval-schemas — opt-in local strict-JSON eval over tests/fixtures/schema_eval/cases.jsonl, including a deliberately bounded case. Reports per-case schema-validity and content-preservation, writes results atomically to the gitignored tmp/schema-eval/, and exits non-zero naming `journal install-provider local` when the local model is not ready. Not collected by pytest; scoring lives in solstone/think/schema_eval.py with offline unit tests. Existing test_local/test_models/test_anthropic/test_openai/test_google/ test_sense_schema/test_schedule_schema pass unmodified. No external network call in any collected test; no new pytest marker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>