Skip to content

Fix ten findings from the v0.1.1 release review - #40

Open
Deep-Sixed wants to merge 8 commits into
mainfrom
claude/contextmesh-graph-system-9t08pf
Open

Deep-Sixed wants to merge 8 commits into
mainfrom
claude/contextmesh-graph-system-9t08pf

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

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.

# Finding Commit
1, 2 Publish workflow uploaded an untested rebuild; the job holding the PyPI token ran mutable action refs a141d08
3 reject(replacement=…) could half-write, or rewrite an existing record; assume() silently dropped evidence_ids 327bbf7
4, 6 Recheck swallowed an evidence-contract violation as a task failure; an auditor outage permanently failed finished work, silently 304321c
5 0.1.1 reported __version__ 0.1.0 3dad969
7 Explicit-id decisions could make a correct graph unloadable 8f6dda1
8 GRAPH.md installed loose at site-packages/GRAPH.md 0b684b1
9 Oversized LLM responses retried as transport failures c089038
10 Evidence text stripped before its size bound 97605bc

Decisions 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_recheck reported only audited: 0. Now:

  • The task stays DONE.
  • The error is recorded as a new AUDIT_ERROR ledger event and reported in a new audit_errors field of the mesh_recheck payload.
  • The next recheck simply asks again.

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 EvidenceRecheckError raised inside ctx.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_edge now refuses one on an explicit-id decision and points to decide(). Unaffected:

  • depends_on into another decision (the Runner's task ordering)
  • auto-minted decisions
  • repeating an existing edge

as_of_graph rewinds 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 own test_edge_binding fixture built exactly this pattern, and on 5836a79 its graph fails to load with SnapshotError. The fixture now passes the dependency to decide().

The schema moved (8). GRAPH.md now lives at contextmesh/GRAPH.md. ONTOLOGY_FILE, package-data and the README link follow it. The root GRAPH.md is 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-publish v1.14.2 is dc37677, exactly where release/v1 points 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. publish now needs three jobs:

  • build refuses a tag whose commit isn't on main, and pins build==1.6.1 and twine==7.0.0.
  • test runs the full suite at the tagged commit.
  • verify, on Linux and Windows, uses no checkout, so the source tree can't mask a packaging mistake. It installs the exact uploaded wheel and checks:
    • __version__ matches the tag
    • the schema is inside the package, with nothing loose
    • the core install has no MCP SDK
    • the CLI runs, and a session survives across two processes
    • the [mcp] extra's launcher works
    • the sdist builds and installs the same package

publish then uploads those same files.

Verification

  • 1009 tests pass (6 skipped, the same skips as main), both at the head and at 8f6dda1 on its own. ruff check . (0.16.5, matching CI) is clean, and ops/leak-check.sh passes against main.
  • Each new regression test was run against the pre-fix code and fails there.
  • The verify job's own run: scripts, executed locally from the YAML:
    • pass against a wheel and sdist of this branch
    • fail against the published 0.1.1 wheel, on its version and separately on its loose schema
  • The main-ancestry step passes the v0.1.1 commit and blocks an unmerged one.
  • Built a wheel, an sdist and a wheel-from-sdist: each carries contextmesh/GRAPH.md and nothing at the top level. Imports cleanly from a venv outside the repo.
  • CI on a141d08: 42/43 green, with the push-event copy of no private data skipped 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

  • Whether the pypi environment requires a reviewer. That setting isn't in the repo.
  • The Windows leg of the publish workflow's verify job has not run anywhere yet. That workflow triggers only on release, 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

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
Deep-Sixed marked this pull request as ready for review September 24, 2026 09:44

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants