Repository navigation
Fix ten findings from the v0.1.1 release review - #40
Open
Deep-Sixed wants to merge 8 commits into
Open
Deep-Sixed wants to merge 8 commits into
Deep-Sixed wants to merge 8 commits into
Conversation
reject(replacement=...) minted the replacement through assume(), whose same-statement call is a documented no-op returning the existing record. So a replacement statement that already existed came back as that record -- even a rejected one -- and reject() rewrote its version and supersedes in place, erasing its own lineage. With the rejected statement itself as the replacement, the self-edge error fired only after REJECTED and the invalidation had been committed, leaving the assumption rejected, invalidated and superseding itself. The replacement id is now checked before the first write, and a statement that already names any assumption is refused with the graph untouched. The same no-op also silently discarded evidence_ids: re-asking an existing statement with new evidence returned the record without writing the justified_by edges the caller asked for. That is refused now too; repeating evidence the record already carries stays a no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two opposite mistakes in the same except clause. An auditor that breaks the evidence contract inside ctx.disproved() -- evidence_id="" -- raised EvidenceRecheckError into `except Exception`, which recorded it as an auditor outage and failed the task, and the MCP write committed that. The identical violation detected after the auditor returns was already fatal. It is now re-raised, so the staged MCP transaction is discarded either way. An auditor that genuinely could not answer -- a provider outage during mesh_recheck -- moved finished, already-audited work from DONE to FAILED. Nothing re-runs a FAILED task, so recovery took a mesh_repair and a full re-execution, and the recheck payload reported only audited: 0. The exception proves nothing about the work, so the task now stays DONE, the error is recorded as a new AUDIT_ERROR ledger event, mesh_recheck reports it under audit_errors, and the next recheck simply asks again. Runner.recheck gets the same change. An auditor that raises during the initial run still fails its task: that work was never verified. This deliberately revises the recheck half of #29/#33; their tests are updated to the new contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
contextmesh-graph 0.1.1 shipped with contextmesh.__version__ and contextmesh_mcp.__version__ both still "0.1.0": the release bumped pyproject.toml, and the publish workflow's identity check compares the tag against pyproject alone. Both now say 0.1.1, and a test compares each against pyproject's [project] version, so the next bump that misses one fails CI on the version-bump pull request rather than after publication. The test reads pyproject with a regex because the suite still runs on 3.9, where tomllib does not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_bounded_read raised LLMTransportError for a body over max_response_bytes, and _send treats LLMTransportError as transient. So a deterministic oversize was re-sent max_attempts times -- re-billing the completion each time -- and the final "transport failed after N attempt(s)" was raised `from None`, hiding the limit. Error bodies go through the same read, so an oversized 400 was retried too, and its status lost. An oversized successful body now raises LLMResponseTooLargeError, a non-retryable LLMProviderError carrying the status and the limit, on the first attempt. An oversized error body is still read no further than limit + 1 bytes, then dropped: nothing reads an error body, so the status is left to decide as it does for any other error -- a 4xx fails closed at once, a 429/5xx keeps its retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#34 moved external_id's byte check ahead of strip(), because stripping first allocates a temporary proportional to whatever the caller sent before the limit can refuse it. Evidence text -- the larger field, at 64 KiB -- was still stripped first. It is now bounded first too; blank text within the limit is refused exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An explicit-id decision's digest covers the edges decide() writes and is checked against the node's actual edges, on load and on retry. Three ways to make a correct graph fail that check: - An edge of a fingerprinted kind added after decide() -- the documented AssumptionLedger.depends(decision, assumption), say. It was accepted, and then the saved graph refused to load as "tampered with or corrupted", and the original decide() could no longer be retried. add_edge now refuses it where it happens, naming decide() as the way to record it. A depends_on into another decision (the Runner's task ordering) is left out of the fingerprint and still added freely, and repeating an edge the decision already has remains a no-op. - decide(assumptions=[<a decision id>]) hashed the target into depends_on, while the structure check counts depends_on only into assumptions, so the stored digest could never match. Non-assumption targets are refused before anything is written. - as_of_graph keeps a decision by its own date and drops edges whose other end came later, then loads the projection through from_dict -- which rejected the stale digest, so the time-travel query crashed. The digest is now wound back to the projected edges, reusing _decision_structure, so the projection is still one the ordinary loader accepts rather than a special case of it. No shipped path reached these -- the Runner passes its assumption to decide(), its decisions are undated, and the demo uses auto-minted ids -- but a library caller who did lost the ability to restore their graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
package-data "../GRAPH.md" put the schema at the root of the wheel, so 0.1.1 installed it as a loose top-level site-packages/GRAPH.md. The ontology reads it at import time from parent.parent, so any other distribution that shipped or removed a file of that generic name would break `import contextmesh`. GRAPH.md moves to contextmesh/GRAPH.md, package-data names it in place, and ONTOLOGY_FILE reads it from the package directory -- the same path in a checkout and in an install. Verified by building a wheel, an sdist and a wheel from that sdist (each carries contextmesh/GRAPH.md and nothing at the top level), then importing from a clean venv outside the repo. A root GRAPH.md stays as a short pointer so existing links to it still land somewhere; nothing reads it. The README link points at the new path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions publish.yml rebuilt from the tag with unpinned build/twine, ran twine check and a filename check, and uploaded. Nothing installed or ran what it uploaded, nothing checked the tag was on main, and the release- candidate gate only ever tests its own build on pull requests. 0.1.1 shipped two defects a single install would have shown: it reported version 0.1.0, and it put the schema loose in site-packages. The workflow now builds once and gates publication on two jobs: - test: the full suite at the tagged commit. - verify (Linux and Windows): installs the exact uploaded wheel into a fresh venv, with no checkout so the source tree cannot mask a packaging mistake, and asserts contextmesh/contextmesh_mcp report the tag's version, the schema is inside the package with nothing loose in site-packages, and the core install has no MCP SDK; runs the CLI and a session across two processes; installs the same wheel with [mcp] and runs the launcher; and installs the sdist and checks it the same way. publish needs build, test and verify, and uploads the same files verify installed. The build job also refuses a tag whose commit is not on main. build and twine are pinned (1.6.1, 7.0.0). Every action is pinned to a full commit SHA, with the release in a comment. The publish job holds the id-token PyPI Trusted Publishing accepts; pypa/gh-action-pypi-publish@release/v1 was a branch and download-artifact@v4 a movable tag. Pins stay on the majors in use (v1.14.2 is where release/v1 points today, so nothing it runs changes). Checked locally: the verify steps, run from this file against a wheel and sdist of this branch, pass; run against the published 0.1.1 wheel, they fail on its version and, separately, on its loose schema. The main-ancestry step passes the v0.1.1 commit and blocks an unmerged one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deep-Sixed
marked this pull request as ready for review
September 24, 2026 09:44
This branch has not been deployed
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.
Fixes the ten findings from a review of what shipped in v0.1.1 (
fb8af1e..5836a79). Each finding was reproduced before it was fixed. Eight commits, each self-contained with its own regression tests; reviewing commit-by-commit is the easiest path.a141d08reject(replacement=…)could half-write, or rewrite an existing record;assume()silently droppedevidence_ids327bbf7304321c__version__0.1.03dad9698f6dda1GRAPH.mdinstalled loose atsite-packages/GRAPH.md0b684b1c08903897605bcDecisions a reviewer should weigh
Recheck semantics change (4, 6). #29/#33 made a recheck auditor exception move a DONE task to FAILED. Nothing re-runs a FAILED task, so a transient provider outage permanently failed verified work, and
mesh_recheckreported onlyaudited: 0. Now:AUDIT_ERRORledger event and reported in a newaudit_errorsfield of themesh_recheckpayload.An auditor that raises during the initial run still fails its task, because that work was never verified. The #29/#33 tests are updated to the new contract. An
EvidenceRecheckErrorraised insidectx.disproved()is now fatal, the same as when it's detected after the auditor returns.Explicit-id decisions are fixed after
decide()(7). GRAPH.md checks the digest against the node's actual edges, "not against whatever its attrs claim", so a fingerprinted edge added later genuinely changes the decision.add_edgenow refuses one on an explicit-id decision and points todecide(). Unaffected:depends_oninto another decision (the Runner's task ordering)as_of_graphrewinds the digest to the projected edges rather than special-casing the loader, per its own "not a special case" rule. GRAPH.md gains one paragraph describing this. The repo's owntest_edge_bindingfixture built exactly this pattern, and on5836a79its graph fails to load withSnapshotError. The fixture now passes the dependency todecide().The schema moved (8).
GRAPH.mdnow lives atcontextmesh/GRAPH.md.ONTOLOGY_FILE,package-dataand the README link follow it. The rootGRAPH.mdis now a short pointer, so external links still land somewhere; nothing reads it.Workflow pins stay on current majors (2). Every action is pinned to a full SHA at the latest release within the major already in use.
pypa/gh-action-pypi-publishv1.14.2 isdc37677, exactly whererelease/v1points today, so nothing it runs changes. Upstream has newer majors (checkout v7, artifact actions v7/v8); upgrading to those is left as a separate change. There's no Dependabot config, so the pins won't refresh themselves.The publish workflow (1)
The artifact is built once.
publishnow needs three jobs:main, and pinsbuild==1.6.1andtwine==7.0.0.__version__matches the tag[mcp]extra's launcher workspublishthen uploads those same files.Verification
main), both at the head and at8f6dda1on its own.ruff check .(0.16.5, matching CI) is clean, andops/leak-check.shpasses againstmain.run:scripts, executed locally from the YAML:contextmesh/GRAPH.mdand nothing at the top level. Imports cleanly from a venv outside the repo.a141d08: 42/43 green, with the push-event copy ofno private dataskipped as designed (it runs on PRs only; the PR copy passed). That includes the release-candidate gate installing the built wheel on Linux and Windows, with and without[mcp].Not verified
pypienvironment requires a reviewer. That setting isn't in the repo.verifyjob has not run anywhere yet. That workflow triggers only onrelease, so it first executes on the next release; the release-candidate gate's Windows wheel jobs, which cover the same install path, are green.This branch was reset from
main. It previously held #15's commits, which were superseded by #16 and closed; they remain under #15's refs.🤖 Generated with Claude Code