Repository navigation
Review fixes for #2391: cross-corpus historical reads, unactionable review state, CI - #2398
Conversation
Bumps [strawberry-graphql](https://github.com/sponsors/strawberry-graphql) from 0.327.3 to 0.327.7. - [Commits](https://github.com/sponsors/strawberry-graphql/commits) --- updated-dependencies: - dependency-name: strawberry-graphql dependency-version: 0.327.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [posthog](https://github.com/posthog/posthog-python) from 7.47.1 to 7.53.0. - [Release notes](https://github.com/posthog/posthog-python/releases) - [Changelog](https://github.com/PostHog/posthog-python/blob/main/CHANGELOG.md) - [Commits](PostHog/posthog-python@posthog-v7.47.1...posthog-v7.53.0) --- updated-dependencies: - dependency-name: posthog dependency-version: 7.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [django](https://github.com/django/django) from 5.2.16 to 5.2.17. - [Commits](django/django@5.2.16...5.2.17) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [psycopg2](https://github.com/psycopg/psycopg2) from 2.9.12 to 2.9.13. - [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS) - [Commits](psycopg/psycopg2@2.9.12...2.9.13) --- updated-dependencies: - dependency-name: psycopg2 dependency-version: 2.9.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…mation-tokens Allow self-service automation tokens without cross-user issuance
…erry-graphql-0.327.7 chore(deps): bump strawberry-graphql from 0.327.3 to 0.327.7
…g-7.53.0 chore(deps): bump posthog from 7.47.1 to 7.53.0
…g2-2.9.13 chore(deps): bump psycopg2 from 2.9.12 to 2.9.13
…-5.2.17 chore(deps): bump django from 5.2.16 to 5.2.17
Review follow-ups for the versioned annotation review in #2391. - Annotation reads now require a DocumentPath into the requested corpus even when the currency check is relaxed for a historical version. Effective permissions are MIN(document, corpus) and structural annotations are shared across corpuses via structural_set, so skipping the linkage check exposed a superseded document's parsed structure under any corpus the caller could read. - One _successor() definition now backs the versionState badge, the stale count and the review mutations, so an annotation stranded two versions back no longer reports STALE with no target any mutation would accept. Recorded decisions stay visible as history. - staleAnnotationCount returns 0 for an unreachable corpus instead of nulling the whole document through the non-null field. - carry_forward parses the target's text/token layer once instead of up to three times while holding the review locks. - The cited-version link routes through openSafeUrl; an absolute external link_url navigated the tab away and tore down the SPA. - A failed review mutation releases the pending placement, which otherwise swallowed every later annotation drawn on that document. - getDocumentVersionUrl rewrites the document slug only on document routes; the viewer is a route-independent overlay. - Drop the standby GetDocumentAnnotationsOnly refetch and the unused AnnotationVersionReviewService.pending(). - The new versioning tests use the project's own to_global_id; the removed graphql_relay import was failing CI.
|
Note on what this PR's own checks can and cannot prove: The backend fix is verified locally instead (PostgreSQL 16 + pgvector, Python 3.12): the two modules that were erroring and the Generated by Claude Code |
ReviewI focused on the PR's own commit ( Backend
Frontend
Nits (non-blocking)
Test coverageCoverage looks solid and matches the "fails without the fix, passes with it" discipline described in the PR body — I spot-checked several of these tests by reading rather than executing (backend suite wasn't run in this environment), and the logic each one exercises matches the corresponding code change. No gaps I'd ask to be filled in before merge. Nice work — this is a tightly scoped fix-up PR with good regression coverage for a genuine cross-corpus permission leak. |
|
Thanks — both nits read correctly. Your trace of the Generated by Claude Code |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Component Tests caught this: AnnotationVersionReview.ct.tsx's "a rejected
placement remains pending and never appears as a saved annotation" asserts
that a rejected carryForwardAnnotation keeps the placement armed. My earlier
commit released it, breaking that contract.
On re-reading, the existing behaviour is right and the review finding that
prompted the change was wrong on its premise. A failed placement is not
silent: the role="status" banner stays visible ("Select the corrected passage
on this document to save the review"), an error toast names the failure, no
phantom local annotation is created, and a "Cancel placement" button is the
explicit way out. Staying armed lets the reviewer retry the same stale
annotation after a transient failure instead of silently converting their
next selection into an unrelated new annotation.
Reverts AnnotationHooks.tsx and its unit test to the PR head, and drops the
corresponding changelog bullet. Every other fix in this PR stands.
|
Component Tests was red on The failing test was Re-reading it, the original behaviour is correct and the review finding I acted on was wrong on its premise. A failed placement is not silent:
Staying armed is what lets a reviewer retry the same stale annotation after a transient failure, instead of having their next selection silently become an unrelated new annotation on the document. That's the better design, and it was deliberate. My process error: I changed behaviour a test in this very feature pinned, without opening that test file first. I'd only run the CT file I had touched ( Every other fix in this PR stands — they're backed by tests I confirmed fail without them. Re-verified locally on the new head: Generated by Claude Code |
|
The Those lines are in fact covered. Two further signs the diff Codecov is scoring isn't this PR's: Not re-running these checks: a re-run recomputes the same threshold from the same absent upload. Patch coverage gets a real measurement on #2391, where the full backend suite runs and uploads. Generated by Claude Code |
ReviewReviewed the actual content of this PR (the two Security fix --
|
Two reviewers independently traced whether the decisions query's
order_by("target_document_id") is load-bearing. It is not, and the code
should say so rather than making a third reader re-derive it: a decision
row is unique per (annotation, target_document), and a document has at
most one child, because Document.parent is set only by the version-up in
documents/versioning.py -- which supersedes the current version -- while
corpus add/fork roots a new content tree with parent=None.
|
On the
Combined with the row being unique per Since you and the previous review both had to derive that independently, the code was the problem, not the logic — pushed Re-verified on the new head with a fresh database: Generated by Claude Code |
d8913d4
into
implementation/2389-annotation-versioning
Summary
Addresses the review findings and the three CI failures on #2391, and merges current
maininto that branch. Targetsimplementation/2389-annotation-versioningso the fixes land inside #2391 rather than beside it.All three CI failures share one root cause: the two new test modules imported
to_global_idfromgraphql_relay, a package the strawberry migration removed. That erroredtest_reference_versioningcollection, failedtest_annotation_version_review::test_graphql_exposes_review_state_and_refreshes_the_stale_countand tripped thetest_graphql_dependenciesguard that exists to catch exactly this.Changes
Security — historical reads skipped the corpus-linkage check (
opencontractserver/annotations/services/annotation_service.py)check_current_version=root.is_current(config/graphql/document_types.py:309) skippedget_document_annotations'DocumentPathcheck entirely for a superseded version. That check is not about currency alone:_compute_effective_permissionsenforces onlyMIN(document, corpus), and structural annotations are shared across corpuses throughstructural_setand are explicitly kept by the corpus filter. READ on a historical document plus READ on any corpus therefore exposed that document's parsed structure under the foreign corpus. The linkage requirement is now unconditional;check_current_versiondecides only which path counts — the live one, or any surviving one for historical reads. Deleted-document recovery reads (check_current_version=False) still work, because a soft delete adds a new path node and leaves the prior oneis_deleted=False.Correctness — review state that no mutation would accept (
opencontractserver/annotations/services/version_review.py)state_for_annotationresolved the "next version" with no currency filter while_lock_reviewrequired a current, immediately-following target. After a second re-import, a v1 annotation renderedSTALEforever with no reachable way to review or dismiss it. A single_successor()definition now backs theversionStatebadge,stale_countand both mutations, so the three can't disagree. Recorded decisions are history and are still reported after later versions land; only the unactionableSTALEcall-to-action goes away.Robustness —
staleAnnotationCountnulled its own document (config/graphql/document_types.py)The field is
Int!, so thePermissionDeniedfrom_scopefor an unreachable corpus nulled the entiredocumentobject rather than the count. It returns0.Performance — placement source parsed up to 3× under the review locks (
version_review.py)carry_forwardcalledpropose()(full PAWLS parse + PlasmaPDF layer) and then_manual_placement()loaded the same source again. One_load_placement_sourcenow serves both, halving the work done whileSELECT … FOR UPDATEis held on the annotation and target document.Frontend
DocumentReferencesPanel.tsx— the cited-version link went through a routerLink.Annotation.link_urlmay be an absolute external URL (LAW citations), so clicking one navigated the tab away and tore down the SPA. It now goes throughopenSafeUrl, like every other link in the panel.navigationUtils.ts—getDocumentVersionUrlrewrote the last path segment unconditionally. The knowledge-base viewer is a route-independent overlay, so picking a version from a corpus route replaced the corpus slug. It now rewrites only on aparseRoute(...).type === "document"path.annotationVersionReview.ts— dropped"GetDocumentAnnotationsOnly"fromREVIEW_REFETCH_QUERIES; its only observer isskip: true, so Apollo parks it in standby and the refetch never fired (it only logged a warning per decision).AnnotationVersionReviewService.pending().Docs —
docs/architecture/querying_annotations_on_versioned_docs.md(P3: linkage is separate from currency) anddocs/features/document_versioning.md(review is a single hop). Changelog fragmentchangelog.d/2391-versioned-annotation-review-review.fixed.md.Reviewed and not changed
bd006c3—AnnotationVersionReview.ct.tsx:55("a rejected placement remains pending and never appears as a saved annotation") pins the opposite, deliberately. A rejected placement is not silent: therole="status"banner stays up, an error toast names the failure, no phantom local annotation is created, and "Cancel placement" is the explicit way out. Staying armed is what lets a reviewer retry the same stale annotation after a transient failure instead of having their next selection become an unrelated new annotation.AnnotationHooks.tsxand its unit test are byte-identical to the base branch.mainthe default prefetch runs through_get_queryset_AnnotationType→visible_to_user, which hides all annotations on a superseded version. The newPrefetchrestores the human ones; analyzer endpoints were already invisible there.pin_existing_mention_linksis nondeterministic for multi-reference annotations. Mirrors existing behaviour; no action.state_for_annotationsee two decisions for one annotation? No —783a5b8records why at the call site.Document.parentis assigned only by the version-up indocuments/versioning.py:540, which supersedes the current version, while corpus add/fork roots a new content tree withparent=None. With the row already unique per(annotation, target_document), at most one decision exists and the ordering is defensive.Test plan
Run against a local PostgreSQL 16 + pgvector and Python 3.12.
pytest opencontractserver/tests/permissioning/ opencontractserver/tests/architecture/ test_annotation_version_review.py test_reference_versioning.py test_corpus_fork_round_trip.py test_structural_annotations_graphql_backwards_compat.py test_analysis_annotation_import.py test_get_document_knowledge_optimizations.py test_query_optimizer_structural_sets.py test_annotation_privacy.py test_corpus_annotations_query.py test_doc_annotations_prefetch_n_plus_one.py test_schema_parity.py -n 4 --dist loadscope— all pass, including the two modules and the dependency guard that were red in CI. Re-run on the current head with--create-db.vitest run navigationUtils.test.ts AnnotationHooks.test.tsx— 157 pass.playwright test -c playwright-ct.config.ts tests/AnnotationVersionReview.ct.tsx tests/DocumentReferencesPanel.ct.tsx— 11 pass.tsc --noEmit -p frontend/tsconfig.json— clean.pre-commit run --files <changed>(black, isort, flake8, mypy, changelog validation) — pass; prettier clean on the changed frontend files.Every new test was confirmed to fail with its fix reverted and pass with it applied:
test_historical_reads_still_require_a_path_into_the_corpustest_a_further_version_retires_state_no_target_would_accepttest_a_corpus_the_document_does_not_belong_to_counts_zero_stalegetDocumentVersionUrl()× 3?vremoval with other params kept, corpus route untouchedroutes an external cited version through the app's URL safety gatewindow.openreceives the external URL, panel stays mountedNote on this PR's own checks:
Backend CIis scoped topull_request: branches: [master, main, v*], so thepytestjob — the one that was red on #2391 — does not run here, and Codecov has no backend upload to score the diff against. Frontend CI has no branch filter and does run. The backend suite gets a real run on #2391 once this merges into it.Checklist
pre-commitpasses on the changed files (black, isort, flake8, mypy, prettier)changelog.d/Contributor License Agreement
By submitting this pull request, you agree to license your contribution
under the project's Contributor License Agreement. First-time
contributors: a bot will comment on this PR asking you to confirm by
replying with a short sign phrase — no separate signup required.
Generated with Claude Code
https://claude.ai/code/session_01Mfn98w6aBkJFqMUSK1e8Vi