feat(queryspace): Batch F dual-surface integration - #219
Merged
Conversation
…ead gate `graph_neighborhood` and `graph_path` ship as bounded PostgreSQL functions (design §3.4, bounds from §4.3). With no instants they read `graph_edges_current`, so a traversal and a direct read of the current graph cannot disagree about what is current; with `valid_at` or `believed_at` they read the history view under both D41 clocks, each half-open. Every bound is clamped inside the function and each walk is ordered before it is cut, so a bound means one thing rather than a different subgraph per run. Traversal is undirected — a relation is an assertion about two entities, and answering only from the subject side would silently answer half the question — and a relation is walked at most once per branch, paths never revisit an entity, and the start is not reported as its own neighbour. The Cypher gate is the mandatory pre-engine control. `read_only=True` blocks writes but not `COPY`/`LOAD`/`INSTALL`-class file and extension actions, so every mutation and external-action construct is rejected by name before the engine sees the text — including one hidden in a UNION arm, behind a comment boundary, or inside a subquery. Quoted prose and comments contribute nothing to the scan, because a keyword inside a string is data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
`query_cypher` and `explain_cypher` run one read statement against the published graph snapshot and answer with grade `snapshot_graph` and the `built_at` of the generation they ran against — a snapshot answer is exactly correct for its cut and says nothing about what has happened since, so the cut travels with it. The reader surfaces `built_at`, which it previously read from the registry and dropped. No published snapshot fails `p2_unavailable`: an empty graph and an absent graph are different answers. `confirm=true` checks live membership of top-level Entity and RELATES ids and drops failing rows as units, without re-running the plan or re-grounding an aggregate; asking for it with no PostgreSQL connection is refused rather than ignored. Structural values lose the engine's physical offsets, which are stable only within one build. The engine spells those keys in upper case, so the first implementation published every one of them and confirmed nothing — the test that asserts a node carries no offsets is what caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
What the graph surface accepts and what it refuses is part of the public contract, not an implementation detail: an agent needs to know this surface reads and only reads, and which types `confirm=true` can check. A change to either now rolls `surface_manifest_hash`, which is what makes the hash an identity rather than a description of the SQL side alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The process-isolated engine worker and `question_context` v4 are Batch D scope and are not in this slice. The isolation gap is worth naming precisely: we have observed the pinned engine fault mid-traversal rather than merely imagined it, so the case for a supervisor that can outlive it is measured, not speculative. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
Batch C and Batch D each claimed revision p9_03_0024 while developing in parallel. C merged first, so the graph helpers become p9_04_0025 on top of it, and the quickstart gate's expected head follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
… paths whole The gate skipped `--` to end of line as a comment. The pinned engine does not treat `--` as a comment at all, so the scan went blind to text the engine goes on to parse — the one direction a gate must never be wrong in. Every statement of that shape happens to fail the engine's own parser today, which is why it had not shown up as a leak, but that is a property of this dialect rather than something to depend on. The scan now recognises exactly `//` and `/* */`, which is what the engine recognises, verified against it rather than assumed. `graph_path` could also return a path cut in half. Paths were chosen as units, their steps unnested, and only then was the edge bound applied as a row limit, so a later path came back reporting `path_length = 3` with a single step — a route that does not connect, presented as one that does. The bound is now spent on whole paths, and a path that cannot fit is omitted rather than truncated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The engine runs `*` and `*1..` quite happily — its binder only refuses an explicit upper bound above its own 30-hop limit — so the gate's hop cap was a cap in name only, and `*1..` was worse than useless: the lower bound was read as the maximum. A pattern that states no finite upper bound is now refused. The `*` is only read as a traversal inside a relationship pattern's brackets, so `count(*)` and multiplication are untouched, and §3.5's recursive modes (SHORTEST, ALL SHORTEST, WSHORTEST(property), TRAIL, ACYCLIC) are stepped over so the range after them is the one that gets read. The note also records what `confirm=true` does not do: a scalar projection of `Entity.id` comes back unconfirmed with all three counts at zero. Doing that correctly needs the parsed Cypher AST to know which UUID column derives from `Entity.id`; guessing from names or value shape would drop `Document` ids, which §3.5 says are never confirmed. Reporting zero is honest, but a caller reading zero as "all clear" would be misreading it, so it is written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The note explained that a scalar `RETURN e.id` is not confirmed; the module a caller actually reads did not. It now says so, and says what to do instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
…ally bind `built_at` came from the registry row's `DEFAULT now()`, written before the repeatable-read export opened. The instant every Cypher answer is scoped to therefore preceded the data it described — and that instant is the entire basis of the `snapshot_graph` grade. It is now the export transaction's own `transaction_timestamp()`, captured when the cut is taken and carried through publication. `max_rows=-1` returned 1,499 rows against a 1,000 cap: `min(-1, 1000)` is -1 and `rows[:-1]` keeps almost everything. A row bound is clamped into the range it describes before it is used as one, and the byte cap now enforces the tier's DEFAULT rather than its hard ceiling, which was letting an unremarkable query return eight times what the tier says it returns. A reader serves one deployment, so it says which, and pairing it with a different deployment id is refused. Taking the two as independent arguments let a mismatched pair serve one deployment's graph labelled as another's. Confirmation runs in one REPEATABLE READ transaction: under READ COMMITTED a commit between the entity check and the relation check could keep a row whose two halves were never simultaneously live at the disclosed instant. It also reaches PostgreSQL even when nothing was confirmable, so zeros mean "nothing to check" rather than "never asked", and `requested` counts confirmable ids rather than rows. The manifest no longer implies more than the code does: naming the confirmable types read as a promise about any projection of them, so the projections that ARE checked and the scalar one that is not are both named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
Over-refusing is its own failure: it makes legal queries impossible, and four
shapes the pinned engine accepts were being turned away.
A `*` now counts as a hop bound only in the relationship pattern itself. Inside
a property map or an inline recursive predicate it is multiplication, so
`[r:RELATES {confidence: 2 * 31}]` and `(r,n | WHERE r.confidence * 100 > 50)`
were both being read as unbounded traversals. A comment may sit between the `*`
and its range. A terminal semicolon followed only by a comment is one
statement, which is how the engine reads it.
`UNINSTALL` is extension management the engine ACCEPTS, so it is refused by
name rather than falling through to `cypher_parse_error`, which made a refused
construct look like bad syntax.
Each case was checked against the pinned engine rather than reasoned about, and
the tests record the engine as the authority.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
… open The helpers' history branch required a non-null lower endpoint on both clocks, so a relation with no recorded start was hidden from every as-of question. A null start means "as long as anyone knows", not "after every instant" — the opposite reading — and both clocks now treat a null endpoint as open, matching what the design says and what `facts_as_of` already did. Naming one clock and letting the other default to now() answered a question the caller did not ask: "as the world was then, as we believe it now" is a legitimate third query, but returning it under a one-clock request misreports what the rows mean. A guard refuses the half-specified form by name rather than substituting a default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
…e from The connection and its provenance were read separately, so a refresh landing between them produced rows from one generation labelled with another's snapshot id, version, and cut. Provenance is the entire basis of the `snapshot_graph` grade, so a result that misdescribes its own generation is worse than one that returns nothing. Both are now taken as one act under the reader's refresh lock. §4.4's Cypher contract is honoured: `query_space_schema` is null, because a Cypher answer did not read the memory_v1 views and crediting them would tell a caller their rows came from somewhere they never queried; and the statement's referenced projection types and properties are published, so a caller can see which part of the graph contract their answer depends on. `max_rows=0` asks for no rows — only an absent bound takes the tier default — and the disclosed row cap is the one that actually applied rather than the tier ceiling, which told a caller nothing about where their rows were cut. Only `-[` opens a relationship pattern: every other `[` opens a list, so `RETURN [2 * 31]` is arithmetic rather than a 62-hop traversal. A confirmed result also carries its confirmation instant at the top level. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
…stant The publication guard compared a candidate against `mine.built_at`, which at that moment is still the registry-insert time — the export cut is written by the publish immediately after. A genuinely newer snapshot could therefore be superseded by an older one. It now compares against the cut being recorded. The helpers disclosed `now()`, which in PostgreSQL is transaction start, while `graph_edges_current` evaluates at statement time. Rows selected by one instant were labelled with an earlier one, so both now use `statement_timestamp()`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
…versal `MATCH (a)-/* gap */[r:RELATES*]->(b)` runs unbounded on the pinned engine and the gate accepted it: the relationship-bracket check read the raw preceding character, which the comment displaced. Classification now uses what the SCAN saw, which steps over comments, and requires the pattern arrow on BOTH sides — `1 - [2 * 31][1]` is subtraction of a list element, not a traversal. Graph references are read by walking the statement rather than matching over raw text, so quoted prose contributes nothing and a property map's keys count as the properties they are. This holds the line until the gate is replaced: the classification is still a hand-rolled reading of Cypher syntax, which is the approach being retired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The hand-rolled character scanner tried to understand Cypher well enough to classify constructs and read variable-length hop bounds. Three review rounds found seven defects in it, four of them introduced by fixes to earlier ones. It was a lexer doing a parser's job, and it enforced a resource limit (hop count) through syntax analysis. Measured against ladybug 0.18.2: - Database(read_only=True) refuses SET/DELETE/CREATE/MERGE with "Connection exception: Cannot execute write operations in a read-only database!" so the gate need not detect mutations to be safe. - read_only does not block COPY/LOAD/INSTALL/UNINSTALL/ATTACH/IMPORT/ EXPORT/CALL — those die in a token scan before the engine sees them. - The engine does not treat -- as a comment; the scan still does not. What remains: a deny-scan for the file/network/extension family only, one statement per request, and the 32 KiB text cap. Mutations that reach the engine map through a pinned READ_ONLY_REFUSAL string to cypher_not_allowed. No EXPLAIN compile step — it cost 49–74% of a query's own runtime and bought nothing once read_only refuses writes. Deleted: the hop-range parser, relationship-bracket classifier, recursive-mode keyword stepper, and graph-reference walker. Tests that encoded those (hop-bound parsing, bracket classification, reference extraction) are gone because the new design makes them obsolete. referenced_graph_types/properties stay empty (§4.4 gap). The hop cap moves to timeout + row/byte caps — a DEVIATION from design §3.5 that makes the unbuilt process-isolated worker load-bearing, recorded in the Batch D implementation note.
`UPDATE fts` updates an extension and RUNS on a read-only database — verified against the pinned engine. It is the same family as INSTALL and exactly what `read_only=True` does not stop, so it has to die in the deny-scan. It was missing from the eight keywords I specified; the rewrite flagged it rather than quietly adding it, which is the right way round. Also records the process-isolated worker as a decision rather than debt. The fault that motivated it raises and is mapped like any other engine error; it does not wedge the process, and a runaway traversal is bounded by the query timeout and the row and byte caps. A supervisor would defend against a failure mode nobody has observed. Two observations would reopen it, and they are named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
…ords A deny list only refuses what somebody remembered. The first one missed `UPDATE`; the second still missed `BEGIN TRANSACTION READ ONLY` — which leaves the shared connection in an open transaction across requests — along with `ANALYZE`, `CHECKPOINT`, `USE` and `DETACH`, all of which the engine runs. The same list also refused ordinary reads that merely used a denied word as an alias, so `RETURN 1 AS update` failed. A read statement can only BEGIN one of five ways. Requiring that refuses every session, maintenance, extension, attachment and file construct at once — including the ones nobody here has thought of — and stops punishing a caller for their choice of alias. This is the third attempt at this gate and the first that does not depend on having enumerated the danger. The row and byte caps now bound the ENGINE, not just the answer. Draining every row and trimming afterwards made them disclosure numbers: `UNWIND RANGE(1, 100000) AS n RETURN n` with max_rows=1 pulled a hundred thousand rows into memory to return one. Reading stops at the cap, keeping one row past it so truncation is still reported honestly. That matters more since the hop bound moved to runtime — it was the bound I said was already there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The refusal was detected by substring, so `RETURN error('Connection exception:
Cannot execute write operations in a read-only database!')` was reported as
`cypher_not_allowed` — the surface telling a caller their own error was it
declining to run a write. The pinned engine prefixes a user-raised error with
"Runtime exception: ", and a genuine refusal is the exact string, so the
comparison is now equality.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Keep the full-v9 tool catalog frozen, correct the P2-edge deletion matrix coordinate, and bound planning for current-context live evidence hydration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
A saved query is something other people's work comes to depend on, so editing one adds a version rather than changing what an earlier caller ran. Identities and versions are separate tables, versions are append-only, and every version pins the `surface_manifest_hash` it was validated against. `publish_surface_hash` moves every active version to `pending_revalidation` in the caller's transaction, so the suspension and the publication of a new hash are one act: there is no instant at which a version is executable while claiming validation against a surface that has been replaced. That state is non-executable on purpose, and `resolve` names the reason for every refusal — not found, disabled, awaiting revalidation, incompatible — because a caller who gets nothing back from a query that was silently not run cannot tell that from an empty answer. Authoring and approving are separate acts: an agent may draft within quotas, activation records an approver, and the approver may not be the author. §5's deployment and per-principal bounds are enforced with `quota_exceeded`. Still to come in this batch: the validation fixtures, the automatic revalidation path with its compare-and-swap on the manifest hash, and the shipped `examples.*` queries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
§3.1 maps seventeen `examples.*` names; this ships a body for every one, and each parses through the same grammar an ad-hoc statement does. An example that could not be run would be worse than shipping none. They are deliberately plain SQL over the memory_v1 views. An example needing a trick the surface does not otherwise support would teach the wrong thing. A test cross-checks the shipped names against the bodies behind them: two lists that can drift apart will, and mine already had — I wrote bodies for two queries the design does not map and omitted two it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
§5 names four fixture classes a saving validation must execute. "It validated" is not something a later reader can check, so the report records each fixture by name and passes only when every one of them ran and passed — a validator that ran three does not get to call that a pass, and a report that simply omits a fixture is not a pass either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
The registry, the pending_revalidation machine, the compare-and-swap revalidation, the validation-report gate and the seventeen examples are built. The validator that actually executes the four fixtures, discovery exposure, and run_saved_query are not, and saying so here is cheaper than a reader finding it in the diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKENhTLJg1HqhbdwCmmkbc
Execute all four operator-owned validation fixtures through the existing sandbox, expose active saved-query discovery without drafts, and advance the migration gate after Batch D. Keep run_saved_query and adapter work in Batch F. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Bind validation to database-authoritative surface state, execute real revalidation fixtures, enforce immutable registry content, and verify all shipped examples through the four required fixture classes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
fazpu
enabled auto-merge (squash)
August 6, 2026 10:08
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: handoff commit dc85cf0f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Batch F completes the accepted Open Query Space dual-surface integration: the existing SQL, Cypher, discovery, and saved-query authorities need one deployment-bound facade and consistent HTTP, SDK, CLI, and MCP access while the legacy recipe surface remains frozen. The consumption guidance also needs to present the accepted two-layer retrieval model without starting the paid cutover benchmark.
Scope
OpenQueryFacadeover the existing Batch B–E authorities.examples.*behind saved-query operations.Batches D and E have merged as #211 and #215. This branch is aligned with their squash merges, so this PR's effective delta is Batch F only.
Important decisions
GET /query/spacewhether the open surface is composed. It validates only the stablememory_v1identity discriminants and fails closed on missing/malformed discovery; it does not duplicate the full discovery schema.{query_id, namespace, name, version, query_hash}.Sources inspected
plan/designs/open_query_space_design.md§3.1, §4, §6, §8, §9, and §11plan/implementation_notes/open_query_space_batch_f.mdValidation and review
git diff --check, manifest identity, legacy-recipe freeze, no-RLS, and base-wheel safety checks passed.8121fd3b..6fd3fea7: APPROVE, no findings.8121fd3b..6fd3fea7: APPROVE, no findings.6fd3fea7..04eca84e: APPROVE, no findings; Pyright 3.12/3.13/3.14 and Ruff passed.6fd3fea7..04eca84e: APPROVE, no findings; targeted Pyright passed.04eca84e..1bdd5e27: APPROVE, no findings; F contract paths remained byte-identical to the first parent.04eca84e..1bdd5e27: APPROVE, no findings; merge blobs matched the corrected E parent exactly.origin/mainwas joined with an ancestry-only merge; the resulting tree is byte-identical to1bdd5e27with zero content delta.1bdd5e27..5fe926d2: APPROVE, no findings.1bdd5e27..5fe926d2: APPROVE, no findings.5fe926d2passed Python 3.12 but exposed a test-harness connection leak before the next module's schema reset on 3.13/3.14; Grok implemented deterministic test-only closure of raw registry connections.5fe926d2..29bbeef4: APPROVE, no findings; targeted Ruff, format, and Pyright passed.5fe926d2..29bbeef4: APPROVE, no findings; scope and teardown ordering verified.31098112042passed Compose, Python 3.12/3.13/3.14, coverage, CLA, and docs gates; the connection teardown correction closed the former 3.13/3.14 stall.29bbeef4f416f416203d7fdd4b1e1b30af575a38.Limitations and follow-up
Contributor agreement