Skip to content

feat(worker): flag-gated medspaCy assertion tagging + offline eval gate#615

Merged
BigSimmo merged 2 commits into
mainfrom
claude/medspacy-assertion-eval
Jul 13, 2026
Merged

feat(worker): flag-gated medspaCy assertion tagging + offline eval gate#615
BigSimmo merged 2 commits into
mainfrom
claude/medspacy-assertion-eval

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds clinical assertion tagging (negation / uncertainty / family / historical context) to the ingestion worker behind a new WORKER_MEDSPACY_ASSERTION flag — default off, fully inert on live ingestion. This is the eval-first foundation for eventually distinguishing "no suicidal ideation" from an asserted finding in the answer pipeline; nothing consumes the annotations yet and answer-verification.ts is deliberately untouched.

  • worker/python/analyze_assertions.py (medspaCy 1.3.1, MIT): ConText assertion detection over caller-supplied target terms, mirroring the extract_pdf_assets.py subprocess contract (file-first output, stdout fallback, distinct exit codes).
  • worker/assertion-tagging.ts: Zod-validated wrapper with an injectable runner; fail-open contract — any script/parse/validation failure logs a warning and the ingestion job proceeds unannotated.
  • When the flag is on, annotations merge into the existing document_chunks.metadata jsonb under a namespaced assertion key — no schema change, no migration, no drift-manifest churn.
  • medspaCy prerequisite check runs only when the flag is on; workers without the optional dependency keep starting cleanly.
  • AU ConText rules: the default (US-leaning) rule set missed AU clinical phrasing found by the eval — nil, query X, suspected, may represent, differential diagnosis includes, previous episode of. Added with bounded forward scopes.

Eval evidence (npm run eval:assertions — local Python only, no providers)

30-case AU community-mental-health golden fixture (scripts/fixtures/assertion-golden.json), 42 term-level checks:

category score
negated 12/12
uncertain 7/7
family 4/4
historical 3/3
asserted (no false flags) 14/16
overall 95.2% (threshold 80%)

The two remaining failures are a single known class — negation scope crossing a conjunction ("no neutropenia and the ANC remains within range" over-negates the second clause). Both err conservatively (over-suppression, never asserting a negated finding), which is the acceptable failure direction for a clinical-safety feature.

Verification

  • New tests/assertion-tagging.test.ts (8 tests, mocked subprocess — CI has no Python): payload validation, id-keyed mapping, fail-open on throw/garbage, subprocess skip on empty input, vocabulary target sourcing.
  • Full unit suite + lint + typecheck + runtime/pin/sitemap/brand/scale checks — green.
  • Prettier clean.

Acceptance gate before any consumption (future PR, not this one)

  1. Operator reviews npm run eval:assertions output against real corpus samples.
  2. Flag enabled on a test ingestion batch; spot-check metadata.assertion on real chunks.
  3. Only then may a separate PR wire metadata.assertion into answer verification/ranking.

Clinical governance preflight

Touches ingestion, gated off by default: flag unset ⇒ byte-identical worker behavior. With the flag on, the only write-path change is an additive namespaced jsonb key; search_tsv does not index metadata, so retrieval/ranking are unaffected. Fail-open ensures no new ingestion failure mode.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added optional clinical assertion tagging during document ingestion, labeling chunks for negation, uncertainty, family history, and historical references.
    • Worker now performs a medspaCy readiness check when enabled.
  • Configuration
    • Added WORKER_MEDSPACY_ASSERTION feature flag (off by default).
    • Tagging is fail-open: ingestion continues if tagging fails or is unavailable.
  • Tests
    • Added golden fixture and contract tests covering assertion tagging and the evaluation reporting.
    • Added eval:assertions script to run offline tagging accuracy checks.

Add WORKER_MEDSPACY_ASSERTION (default off): when enabled, chunk texts are
batched through worker/python/analyze_assertions.py (medspaCy ConText with
clinical-vocabulary target terms) and per-chunk negation/uncertainty/family/
historical annotations land under document_chunks.metadata.assertion — the
existing jsonb column, so no schema change or drift-manifest churn.

Fail-open contract throughout: a broken or missing medspaCy install degrades
to unannotated chunks and can never fail an ingestion job. The medspacy
prerequisite check runs only when the flag is on, so existing workers keep
starting cleanly. Nothing consumes the annotations yet — answer-verification
is deliberately NOT wired to them.

Eval-first acceptance gate: scripts/eval-assertions.ts + a 30-case AU
community-mental-health golden fixture (npm run eval:assertions, local Python
only, no providers). Baseline run scored 95.2% (negation 12/12, uncertainty
7/7, family 4/4, historical 3/3) after adding AU ConText cues the default
US-leaning rules miss (nil, query, suspected, may represent, differential
diagnosis includes, previous episode of; forward cues scope-bounded). The two
remaining known failures are conjunction-scope over-negation — conservative
for a safety feature (suppresses evidence rather than asserting a negated
finding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabase Bot commented Jul 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dc2cd4d-3856-4028-a0b3-fbfa9427df57

📥 Commits

Reviewing files that changed from the base of the PR and between e16691f and 0ebe88b.

📒 Files selected for processing (4)
  • scripts/eval-assertions.ts
  • scripts/fixtures/assertion-golden.json
  • worker/assertion-tagging.ts
  • worker/python/analyze_assertions.py

📝 Walkthrough

Walkthrough

Adds an optional, fail-open medspaCy assertion-tagging path for worker ingestion, including Python analysis, TypeScript orchestration, prerequisite checks, metadata integration, configuration, tests, and an offline golden-fixture evaluator.

Changes

Assertion tagging

Layer / File(s) Summary
Assertion contracts and analyzer
worker/assertion-tagging.ts, worker/python/analyze_assertions.py, worker/python/requirements.txt
Defines assertion payload types and parsing, derives vocabulary targets, and classifies clinical terms with medspaCy ConText.
TypeScript runner and fail-open mapping
worker/assertion-tagging.ts, tests/assertion-tagging.test.ts
Executes the Python analyzer, maps results by chunk ID, normalizes metadata, and returns empty results on failures.
Worker configuration and ingestion integration
src/lib/env.ts, .env.example, worker/prerequisites.ts, worker/main.ts
Adds the feature flag, checks medspaCy availability, and merges assertion metadata during chunk insertion.
Offline evaluation and golden cases
package.json, scripts/eval-assertions.ts, scripts/fixtures/assertion-golden.json
Adds the evaluation command, fixture cases, accuracy checks, threshold handling, and optional JSON reporting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant AssertionTagger
  participant PythonAnalyzer
  participant MedspaCy
  participant ChunkMetadata
  Worker->>AssertionTagger: Submit chunks and vocabulary targets
  AssertionTagger->>PythonAnalyzer: Write input and spawn analyzer
  PythonAnalyzer->>MedspaCy: Build ConText pipeline and classify targets
  MedspaCy-->>PythonAnalyzer: Return assertion categories
  PythonAnalyzer-->>AssertionTagger: Return JSON payload
  AssertionTagger-->>Worker: Return assertions by chunk ID
  Worker->>ChunkMetadata: Merge assertion metadata
Loading

Possibly related PRs

  • BigSimmo/Database#488: Adds or changes the TypeScript runner wrapper used by the new assertion evaluation command.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: flag-gated medspaCy assertion tagging with an offline evaluation gate.
Description check ✅ Passed The description covers the feature, evaluation evidence, verification, and governance notes, with only minor deviations from the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/medspacy-assertion-eval

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e16691fef7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/eval-assertions.ts
Comment thread scripts/eval-assertions.ts
@BigSimmo

Copy link
Copy Markdown
Owner Author

@codex resolve actionable Codex review findings for this pull request and current head using the repository instructions. This is the pull request's single automatic repair pass: do not perform a fresh review, create new standalone findings, or request another review. Work only the existing unresolved Codex threads on the current head. Always fix P0 and P1 findings. For P2 and lower findings, fix only clear, scoped, low-risk issues; otherwise disposition them with a concise reason. After fixing or dispositioning a thread, reply in that thread with as the first line, followed by a concise summary; that marker authorizes the workflow to close that exact thread. If human input or new authorization is required, do not use the marker and leave the thread open with the blocker. Finish only after every actionable thread is fixed or dispositioned and closed, or explicitly left open for a human decision. Do not update the branch from main, address unrelated reviews, broaden scope, or create more than one scoped fix commit. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
worker/python/analyze_assertions.py (1)

47-52: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Model reload per document — no warm pipeline reuse.

build_nlp (spaCy/medspaCy load + AU context rules) runs fresh for every analyze_assertions.py invocation, and the TS caller invokes it once per document. Once WORKER_MEDSPACY_ASSERTION is enabled at scale, this adds a full model-load cost (typically seconds) per document on top of ingestion. Worth considering a long-lived process (persistent daemon/IPC) or caching if this feature graduates beyond eval-only use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@worker/python/analyze_assertions.py` around lines 47 - 52, Update the
analyze_assertions workflow around build_nlp to reuse a loaded medspaCy pipeline
across documents or invocations instead of rebuilding it per document. Cache the
initialized pipeline, including target matcher and AU context rules, while
preserving target-specific matcher configuration and existing assertion-analysis
behavior.
worker/main.ts (1)

1432-1443: 🧹 Nitpick | 🔵 Trivial

Verify production-readiness check before merge.

This wires a new subprocess-based enrichment path into clinical ingestion. As per coding guidelines, changes to clinical ingestion should run the smallest relevant domain check plus npm run check:production-readiness before merge — please confirm this was run (with the flag both on and off).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@worker/main.ts` around lines 1432 - 1443, Before merging the clinical
ingestion change around WORKER_MEDSPACY_ASSERTION and annotateChunkAssertions,
run the smallest relevant domain check and npm run check:production-readiness
with the flag enabled and disabled, then address any failures.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/eval-assertions.ts`:
- Around line 79-112: The clinical acceptance gate only checks required
memberships and must also reject forbidden category labels. In
scripts/eval-assertions.ts, update the membership checks to generate failures
for forbidden buckets while preserving intentional family/historical overlap;
extend scripts/fixtures/assertion-golden.json at lines 2-2 to define expected
and forbidden category buckets, and at lines 173-176 require tremor to remain
asserted or explicitly forbid negation and uncertainty.
- Around line 51-53: Update the minAccuracy parsing and validation in the
script’s argument-handling flow to reject non-finite values, including NaN, and
any threshold outside the inclusive [0, 1] range before evaluating or reporting
assertion results. Fail conservatively with a clear validation error instead of
allowing evaluation to print PASS; apply the same validation wherever
minAccuracy is used.

In `@worker/assertion-tagging.ts`:
- Around line 77-115: Update runAssertionScript’s Python subprocess promise to
enforce a short timeout, terminating the spawned child with child.kill() and
rejecting with an appropriate error when exceeded. Clear the timer when the
process exits or fails to start, while preserving the existing stdout, stderr,
exit-code handling, cleanup, and fail-open behavior.

---

Nitpick comments:
In `@worker/main.ts`:
- Around line 1432-1443: Before merging the clinical ingestion change around
WORKER_MEDSPACY_ASSERTION and annotateChunkAssertions, run the smallest relevant
domain check and npm run check:production-readiness with the flag enabled and
disabled, then address any failures.

In `@worker/python/analyze_assertions.py`:
- Around line 47-52: Update the analyze_assertions workflow around build_nlp to
reuse a loaded medspaCy pipeline across documents or invocations instead of
rebuilding it per document. Cache the initialized pipeline, including target
matcher and AU context rules, while preserving target-specific matcher
configuration and existing assertion-analysis behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 879b7ae9-6f4e-4870-b65b-280c6ed6a23a

📥 Commits

Reviewing files that changed from the base of the PR and between e5bcba8 and e16691f.

📒 Files selected for processing (11)
  • .env.example
  • package.json
  • scripts/eval-assertions.ts
  • scripts/fixtures/assertion-golden.json
  • src/lib/env.ts
  • tests/assertion-tagging.test.ts
  • worker/assertion-tagging.ts
  • worker/main.ts
  • worker/prerequisites.ts
  • worker/python/analyze_assertions.py
  • worker/python/requirements.txt

Comment thread scripts/eval-assertions.ts
Comment thread scripts/eval-assertions.ts
Comment thread worker/assertion-tagging.ts
@BigSimmo
BigSimmo enabled auto-merge (squash) July 13, 2026 17:51
…threshold validation, subprocess timeout

Review findings from PR #615 (Codex + CodeRabbit), all verified valid:
- The eval only scored positive expectations, so an over-broad ConText rule
  flagging extra targets negated/uncertain passed silently. Every target not
  expected in an existence-changing category must now be absent from it
  (family/historical stay positive-only: contextual qualifiers may co-occur).
  The stricter gate immediately caught two real cases: "rather than agitation"
  (correct exclusion semantics — encoded in the fixture with a note) and
  "Possible NMS; cease antipsychotic" scope leaking across a semicolon (fixed
  with a TERMINATE rule at clause boundaries).
- --min-accuracy now rejects non-finite or out-of-range values instead of
  silently disabling the gate (accuracy < NaN is always false → false PASS).
- runAssertionScript enforces a 120s timeout with child.kill() so a hung
  Python process rejects into the fail-open path instead of wedging ingestion.

Eval after hardening: 98.0% on 101 checks (was 95.2% on 42) — negated 13/13,
uncertain 8/8, zero false uncertainty; the two remaining failures are the
documented conjunction over-negation limitation (conservative direction).
Deliberately NOT terminating scope at "and": that would under-negate
"no fever and rigors", the clinically unsafe direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BigSimmo
BigSimmo disabled auto-merge July 13, 2026 18:08
@BigSimmo
BigSimmo enabled auto-merge (squash) July 13, 2026 18:08
@BigSimmo
BigSimmo merged commit 84cebf3 into main Jul 13, 2026
50 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ebe88bdc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


async function main() {
const fixturePath = argValue("--fixture") ?? path.join("scripts", "fixtures", "assertion-golden.json");
const minAccuracy = Number(argValue("--min-accuracy") ?? "0.8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the assertion gate exact by default

With the default --min-accuracy of 0.8, npm run eval:assertions exits 0 even when up to 20 of the 101 current fixture checks fail; the later fail condition only looks at aggregate accuracy. Fresh evidence after the earlier assertion-flag comment is that the new false-positive checks are now counted, but a ConText change that misses one or several golden negation/uncertainty cases can still print PASS and bless incorrect clinical assertion metadata before WORKER_MEDSPACY_ASSERTION is enabled. Make the default gate require zero failures, or require an explicit non-default threshold for experimental runs.

Useful? React with 👍 / 👎.

@BigSimmo
BigSimmo deleted the claude/medspacy-assertion-eval branch July 14, 2026 11:15
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.

1 participant