diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f18b735719..dcfc55fbe0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -51,3 +51,15 @@ updates: groups: github-actions: patterns: ["*"] + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Australia/Perth" + open-pull-requests-limit: 2 + groups: + docker-images: + patterns: ["*"] diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e9642cda59..a9fe7728ef 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -30,9 +30,9 @@ permissions: contents: read jobs: - app-image: + build-and-verify: runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -42,38 +42,82 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - - name: Build app image (no push) + - name: Build app image (load, no push) uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: Dockerfile + tags: clinical-kb-app:ci push: false - # The publishable key is public-by-design and only inlines into the - # client bundle, so the CI placeholder is enough to validate the build. - # Real production images are built with the real key at deploy time. + load: true build-args: | NEXT_PUBLIC_SUPABASE_URL=https://sjrfecxgysukkwxsowpy.supabase.co NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=placeholder-ci-publishable-key - # Exercise a matched lowered limit, not only the 150 MB defaults. MAX_UPLOAD_MB=50 NEXT_PUBLIC_MAX_UPLOAD_MB=50 ALLOW_LOW_RAM_BUILD=1 + cache-from: type=gha,scope=clinical-kb-app + cache-to: type=gha,scope=clinical-kb-app,mode=max - worker-image: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - - - name: Build worker image (no push) + - name: Build worker image (load, no push) uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: Dockerfile.worker + tags: clinical-kb-worker:ci push: false + load: true + cache-from: type=gha,scope=clinical-kb-worker + cache-to: type=gha,scope=clinical-kb-worker,mode=max + + - name: Verify image content contract + run: node scripts/check-image-content-contract.mjs + + - name: Run app container smoke (provider-free, cap-drop) + run: node scripts/app-container-smoke.mjs + + - name: Run worker runtime validation (provider-free, network-isolated) + run: docker run --rm --network=none --entrypoint node clinical-kb-worker:ci dist/worker/validate-runtime.mjs + + - name: Read-only runtime check (advisory) + run: | + docker run --rm --read-only --cap-drop=ALL --security-opt no-new-privileges:true \ + -e NODE_ENV=production \ + -e NEXT_PUBLIC_DEMO_MODE=false \ + -e NEXT_PUBLIC_SUPABASE_URL=https://sjrfecxgysukkwxsowpy.supabase.co \ + -e SUPABASE_SERVICE_ROLE_KEY=smoke-test-placeholder \ + -e RAG_QUERY_HASH_SECRET=smoke-test-hash-secret \ + -e RAG_PROVIDER_MODE=offline \ + -e NEXT_TELEMETRY_DISABLED=1 \ + clinical-kb-app:ci \ + node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>r.json()).then(j=>{if(j.status!=='ok')throw new Error(JSON.stringify(j));console.log(j)}).catch(e=>{console.error(e);process.exit(1)})" || true + continue-on-error: true + + - name: Generate SBOMs + # Advisory: docker-save of both loaded images can ENOSPC on small runners. + # Free BuildKit cache first; do not fail the required gate on scanner disk pressure. + continue-on-error: true + run: | + docker builder prune -af || true + df -h || true + node scripts/trivy-image-scan.mjs clinical-kb-app:ci --sbom sbom-app.cdx.json + node scripts/trivy-image-scan.mjs clinical-kb-worker:ci --sbom sbom-worker.cdx.json + + - name: Upload SBOMs + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: sboms + path: | + sbom-app.cdx.json + sbom-worker.cdx.json + if-no-files-found: warn + + - name: Vulnerability scan (HIGH,CRITICAL) + # Non-blocking by design — findings are reported, not merge-gating. + continue-on-error: true + run: | + docker builder prune -af || true + node scripts/trivy-image-scan.mjs clinical-kb-app:ci + node scripts/trivy-image-scan.mjs clinical-kb-worker:ci diff --git a/Dockerfile b/Dockerfile index 428ecb9473..5c457947c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,9 @@ # NEVER baked into the image — inject them at run time from the host's # secret store. -FROM node:24-bookworm-slim AS deps +FROM node:24-bookworm-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS node-base + +FROM node-base AS deps WORKDIR /app # check-node-engine.cjs runs as the npm preinstall hook and # install-git-hooks.mjs as the postinstall hook, so both must be in place @@ -28,13 +30,14 @@ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs # Registry blips (ECONNRESET) have failed CI app-image builds mid-install; retry # the whole `npm ci` rather than relying only on per-request fetch retries. -RUN for attempt in 1 2 3; do \ +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + for attempt in 1 2 3; do \ npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done -FROM node:24-bookworm-slim AS build +FROM node-base AS build WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 COPY --from=deps /app/node_modules ./node_modules @@ -60,18 +63,19 @@ ENV ALLOW_LOW_RAM_BUILD=${ALLOW_LOW_RAM_BUILD} # build argument for a runtime value. RUN UPLOAD_LIMIT_PARITY_SERVER_MB="${MAX_UPLOAD_MB}" env -u MAX_UPLOAD_MB npm run build -FROM node:24-bookworm-slim AS prod-deps +FROM node-base AS prod-deps WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs -RUN for attempt in 1 2 3; do \ +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + for attempt in 1 2 3; do \ npm ci --omit=dev --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done -FROM node:24-bookworm-slim AS runner +FROM node-base AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 @@ -84,6 +88,11 @@ COPY --from=build /app/src/lib/supabase/project.ts ./src/lib/supabase/project.ts COPY package.json next.config.ts ./ USER node EXPOSE 3000 +LABEL org.opencontainers.image.source="https://github.com/BigSimmo/Database" +LABEL org.opencontainers.image.title="Clinical KB app tier" +LABEL org.opencontainers.image.description="Next.js 16 app tier for the Clinical KB medical guideline RAG knowledge base" +LABEL org.opencontainers.image.licenses="UNLICENSED" +STOPSIGNAL SIGTERM # /api/health is the app's own ops health route. HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" diff --git a/Dockerfile.worker b/Dockerfile.worker index 3f2a848a5c..83825d536a 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -19,13 +19,16 @@ # `server-only` marker to the standalone stub at build time (the job # run-tsx.mjs previously did at runtime) and keeps npm packages external, # so the bundle resolves them from the runner's production node_modules. -FROM node:24-bookworm-slim AS build +FROM node:24-bookworm-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS node-base + +FROM node-base AS build WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs # Same install-retry contract as the app Dockerfile (registry ECONNRESET flakes). -RUN for attempt in 1 2 3; do \ +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + for attempt in 1 2 3; do \ npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ @@ -35,25 +38,28 @@ RUN node scripts/build-worker.mjs # Production-only node_modules for the runtime stage. The preinstall/ # postinstall lifecycle scripts must be present in every npm-ci stage. -FROM node:24-bookworm-slim AS prod-deps +FROM node-base AS prod-deps WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs -RUN for attempt in 1 2 3; do \ +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + for attempt in 1 2 3; do \ npm ci --omit=dev --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ done -FROM node:24-bookworm-slim AS runner +FROM node-base AS runner RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-venv tesseract-ocr \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY worker/python/requirements.txt worker/python/requirements.txt -RUN python3 -m venv /opt/ocr-venv \ - && /opt/ocr-venv/bin/pip install --no-cache-dir -r worker/python/requirements.txt +RUN python3 -m venv /opt/ocr-venv +RUN --mount=type=cache,target=/root/.cache/pip \ + /opt/ocr-venv/bin/pip install --upgrade --require-hashes -r worker/python/requirements.txt \ + && /opt/ocr-venv/bin/pip check ENV PATH="/opt/ocr-venv/bin:${PATH}" ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -70,6 +76,14 @@ RUN python -m compileall -q worker/python \ && rm -f worker/python/test_*.py COPY package.json ./package.json USER node +# The runtime validator proves the bundle, Node, Python and module resolution +# before the image is promoted. It never calls Supabase/OpenAI or claims jobs. +RUN node dist/worker/validate-runtime.mjs +LABEL org.opencontainers.image.source="https://github.com/BigSimmo/Database" +LABEL org.opencontainers.image.title="Clinical KB ingestion worker" +LABEL org.opencontainers.image.description="Node + Python OCR ingestion worker for the Clinical KB medical guideline RAG knowledge base" +LABEL org.opencontainers.image.licenses="UNLICENSED" +STOPSIGNAL SIGTERM # Long-poll worker; WORKER_* env vars control claim batch size, concurrency, # and the stale-claim window (see src/lib/env.ts). # diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md index bf6c9a772b..a853147929 100644 --- a/docs/deployment-architecture.md +++ b/docs/deployment-architecture.md @@ -206,8 +206,10 @@ comparable (~200 ms) from Singapore or Sydney and does not favour either host. ### Image contract (`Dockerfile`) -- `node:24-bookworm-slim` in all stages — respects `engines`/`engine-strict` - and the `preinstall` engine guard. +- `node:24-bookworm-slim` is pinned by multi-platform SHA-256 digest in a + shared `node-base` stage and used by every stage. BuildKit cache mounts + speed `npm ci` and the worker Python venv install without bloating final + images. - The build stage runs the repo's own `npm run build` (`guard-next-build.mjs` + `next build --webpack` + the client-bundle secret scan) — **the image build fails exactly where a local build would**. The @@ -235,8 +237,9 @@ comparable (~200 ms) from Singapore or Sydney and does not favour either host. differ. - Runtime is a non-root `node` user, prod-only `node_modules`, direct `next start -H 0.0.0.0 -p $PORT` (Railway injects `$PORT`; the local - port-picker script is deliberately bypassed), and a `HEALTHCHECK` against - `/api/health`. + port-picker script is deliberately bypassed), a `HEALTHCHECK` against + `/api/health`, an explicit `STOPSIGNAL SIGTERM`, and OCI source/title + labels for supply-chain traceability. - No secret is ever baked into a layer. `SUPABASE_SERVICE_ROLE_KEY`, `OPENAI_API_KEY`, etc. are injected at run time by Railway's variable store. - Request bodies are bounded twice: Next Proxy buffers at most 151 MiB, and @@ -279,9 +282,10 @@ check and watch patterns rather than relying on dashboard defaults. ### Decision: containerized worker (recommended) over completing the edge-agent migration -Ship the existing worker as a container (`Dockerfile.worker`: Node 24 + a -prebuilt esbuild bundle over production-only `node_modules` + -Tesseract + a Python venv with `worker/python/requirements.txt`) and run **one +Ship the existing worker as a container (`Dockerfile.worker`: pinned Node 24 + +a prebuilt esbuild bundle over production-only `node_modules` + +Tesseract + a Python venv with a hashed `worker/python/requirements.txt` + +a provider-free `dist/worker/validate-runtime.mjs` gate) and run **one always-on worker instance** co-located in Railway Singapore (`worker` service), using Railway's `ALWAYS` restart policy so repeated bootstrap failures cannot exhaust a finite retry allowance and leave the queue undrained. @@ -355,6 +359,10 @@ Operational rules that follow: generation/chunk-key, and completion is gated by the strict completion RPCs plus the edge agent. Railway's always-restart policy brings the worker back and it reclaims stale jobs automatically. +- The worker handles `SIGTERM`/`SIGINT` gracefully: it drains the + already-claimed active batch, stops claiming new jobs, and exits `0`. A + `STOPSIGNAL SIGTERM` directive is in both final images. Fatal runtime errors + still exit `1` and dispatch the existing failure webhook. - **Backlog improvement (not in this change):** a heartbeat that refreshes `locked_at` could ride the existing throttled progress updates (`WORKER_PROGRESS_UPDATE_MIN_INTERVAL_MS`, 60 s), which would let the stale diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 326ee8e9b0..e2ba275afe 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -130,7 +130,7 @@ removed after current-main verification; it is not missing recommended work. | 81 | `#204` | A2 | High — install/CI integrity | Next dependency or verify:pr-local change | 1–3 hours | npm 11.6.2 regenerates a lockfile its own `npm ci` rejects, reddening every CI job — add `npm ci --dry-run` (or lockfile-sync assertion) to `verify:pr-local` when package.json/lock change; do not regenerate with `npm install` to "fix". Distinct from #149. | - + ## Open items > **Merged-main canary update (2026-07-23, run `30018289898`):** the new structured report correctly recorded evaluated tree `c24f2e8f2d30d0c59fc1eba025d3dcd63478137e`, run/attempt identity and `cross-region-runner` latency context. Golden retrieval remained 36/36 with document/content recall 1.0 and no failed cases. The 44-case answer gate had grounded-supported and unsupported-correct rates of 1.0, but failed because `neuroleptic-side-effect-escalation` again returned one citation where two are required (citation-failure rate 0.0227). `admission-discharge-comparison` again omitted the specific AKG admission document after `comparison_source_extractive_fallback`; `admission-discharge-coverage-paraphrase` was advisory-only at 24,870 ms. Answer cost was reported as `$0.234736`. Do not retry immediately: retain this as the first structured datapoint, compare it with the scheduled 2026-07-26 report, and keep retrieval/ranking unchanged. @@ -142,6 +142,8 @@ removed after current-main verification; it is not missing recommended work. > **Final RAG verification update (2026-07-27):** the protected retrieval canary passed 36/36 with document/content recall 1.0 and zero per-case reciprocal-rank regressions; content MRR improved from 0.9389 to 0.9406 and no case regressed. The final 44-case answer gate then passed with 30/30 supported answers substantive and grounded, 14/14 unsupported answers correct, citation and numeric failure rates 0, source-backed review fallback rate 0, no route-ceiling failures, and p95 7,494 ms. The final report used two provider requests; pricing telemetry was not configured, so it does not claim an exact dollar value. #019 and #029 are resolved without weakening grounding, citation, numeric, or route gates. Detailed evidence is in `docs/evidence/rag-reliability-evidence-2026-07-27.md`. > > **Exact-head release confirmation (2026-07-28):** after final review narrowed expanded chunk selection to the measured clozapine blood-count action shape, `output/rag-retrieval-post-exact-head.json` repeated all 36 cases with document/content recall 1.0, MRR 0.8921, content MRR 0.9406, nDCG 0.9308, irrelevant-at-10 0.0917, zero failures and zero per-case document/content reciprocal-rank regressions versus `rag-retrieval-post-final.json`. Median latency rose 13,563 -> 19,729 ms while p90 improved 56,765 -> 55,660 ms; neither run had a latency-failed case, so no ranking or latency gate changed. Cache-bypassed exact-head answer probes for both admission/discharge cases and the clozapine threshold case were substantive, grounded, expected-source-backed and free of citation/numeric/route failures; all used zero provider requests and $0 generation cost. This was the protected behavior-change merge gate, not a rerun for #023. +> +> **Ledger sweep 2026-08-02 (second pass):** no new outstanding items found after the Docker delivery hardening commit. The two untracked reports remain source documents; their follow-ups are captured as #210-#215 (renumbered on main sync after DS V2 PR-E claimed #206-#209). | ID | Pri | Type | Summary | Detail / next action | Source | Added | @@ -228,6 +230,12 @@ removed after current-main verification; it is not missing recommended work. | #207 | P1 | task | DS V2 PR 13 blocker: AnswerState has no ungrounded-answer channel | answerStateFromRetrieval() maps a grounded:false / confidence:'unsupported' answer over current sources to 'ready'. The live product already gates on grounded/confidence/unverifiedNumericTokens (evidence-panels.tsx, answer-thread-turn.tsx) to show 'Review source match', so adopting AnswerCard as-is would silently retire a warning shipped today. Needs a fifth state or companion flag; wording is a clinical-owner decision. | clinical-governance-reviewer P1-2 on PR 6 (claude/ds-v2-answer-safety); recorded in docs/design-system/SPEC.md PR 6 clinical review note and COMPONENTS.md 9.13 | 2026-08-02 | | #208 | P2 | task | DS V2: answerClipboardText must not replace formatAnswerRenderCopyText in PR 13 | PR 6 strengthened answerClipboardText (unconditional attribution + verify line, enumerated sources, provenance suppressed where it would contradict the caveat). It is still narrower than formatAnswerRenderCopyText (src/lib/answer-render-policy.ts), which carries the render policy's own warnings. PR 13 must compose the two or extend answerClipboardText with clinical-owner review, never swap it in. | clinical-governance-reviewer P1-1 on PR 6; recorded in docs/design-system/SPEC.md and COMPONENTS.md 9.13 | 2026-08-02 | | #209 | P3 | task | DS V2 Gate 1: add contrast pair for --warning used as body text | VerificationNotice's caution variant and DoseLine's overdue label use --warning at text tier — the only place a status hue is used as body-text colour rather than a --text-* token. Gate 1's contrast checking must add that pair explicitly rather than assuming the text tiers cover it. Also note: the logged-once Sets in missing-value, date-display, verification-notice, answer-state and retrieval-state-banner are module-level, so on the server they are per-process and unbounded; a persistent data defect logs once at boot then is swallowed. Acceptable while unregistered. | clinical-governance-reviewer P3 findings on PR 6; recorded in docs/design-system/SPEC.md PR 6 clinical review note | 2026-08-02 | +| #210 | P2 | task | Restore the npm run typecheck gate | Clear stale .next generated types or update validator references so npm run typecheck reflects source health. Evidence: 14 errors in .next/dev/types/validator.ts referencing removed mockup pages; source-only typecheck is clean. See docs/review-findings-2026-08-02.md sections 2.1 and 9. | session 2026-08-02 /ledger sweep; docs/review-findings-2026-08-02.md | 2026-08-02 | +| #211 | P2 | task | Plan and start the noUncheckedIndexedAccess migration | Enable noUncheckedIndexedAccess in a branch and remediate the 1,266 errors, starting with the 15-20 highest-risk source files. Hot spots include worker/main.ts:901-942, src/lib/rag/rag-extractive-answer.ts, and src/lib/answer-verification.ts. Prefer ?. or ?? guards, or non-null assertions only where invariants are provable. Re-run npm run test and npm run typecheck before merge. See docs/review-findings-2026-08-02.md section 6. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | +| #212 | P2 | task | Replace as unknown as casts and unvalidated JSON.parse with Zod or runtime guards | 48 as unknown as casts and ~24 unvalidated JSON.parse calls across src/ trust Supabase, OpenAI, localStorage, file metadata and extraction boundaries. Start with src/lib/rag/rag.ts and src/app/api/* routes, mirroring existing Zod use in src/lib/validation/body.ts and src/lib/extractors/document.ts. See docs/review-findings-2026-08-02.md sections 2.2, 2.3 and 8. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | +| #213 | P2 | task | Stop swallowing fetch and stream errors with empty catch handlers | 51 call sites use response.json().catch(() => ({})) or void fetch(...).catch(() => undefined), hiding API, JSON, auth, telemetry and stream failures. Add explicit response.ok checks and a typed safeFetch wrapper that preserves the failure signal for telemetry and user feedback. See docs/review-findings-2026-08-02.md sections 3.1-3.4 and 8. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | +| #214 | P2 | task | Make PDF viewer wheel listener passive by default | src/components/document-viewer/use-viewer-gestures.ts registers wheel with passive:false, blocking compositor scroll and hurting INP. Attach the non-passive listener only when unmodified wheel zoom is active; otherwise keep the default passive scroll path. Measure with Chrome DevTools Performance/INP during PDF wheel/pinch. See docs/audit/performance-image-cwv-audit-2026-08-02.md INP-1 and fix backlog #8. | session 2026-08-02 /ledger sweep — docs/audit/performance-image-cwv-audit-2026-08-02.md | 2026-08-02 | +| #215 | P3 | task | Add image-optimization basics for lightbox, PWA lifecycle and demo PNGs | image-lightbox.tsx and pwa-lifecycle.tsx lack decoding=async; public/demo-documents/*.png are ~80 KB each and not served in WebP/AVIF; SignedImage lacks a priority prop for above-fold evidence images. Apply decoding=async, add an optional priority prop, and convert demo PNGs with a PNG fallback. Most performance image findings are already tracked under #016, #013, #117 and #147; this row captures the new image-only observations from the 2026-08-02 audit. | session 2026-08-02 /ledger sweep — docs/audit/performance-image-cwv-audit-2026-08-02.md | 2026-08-02 | ## Resolved / archive diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 371a20c7b2..3b1be36ae1 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (204 files) and the `package.json` script surface (215 entries), +Curated map of `scripts/` (210 files) and the `package.json` script surface (218 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/docs/worker-deploy-runbook.md b/docs/worker-deploy-runbook.md index df1b695e76..1c36e296cd 100644 --- a/docs/worker-deploy-runbook.md +++ b/docs/worker-deploy-runbook.md @@ -47,8 +47,8 @@ If migrations are still outstanding, stop here and apply them first. The worker image build is validated in CI by [`.github/workflows/docker-image.yml`](../.github/workflows/docker-image.yml) -→ the **`worker-image`** job. It runs `docker build -f Dockerfile.worker` -(`push: false`) on: +→ the **`build-and-verify`** job. It runs `docker build -f Dockerfile.worker` +(`load: true`, `push: false`) on: - every push to `main` / `release/**`, - pull requests and merge-queue commits whose CI change classifier detects a @@ -68,7 +68,9 @@ Status: ✅ **CI covers the worker image build.** All build inputs referenced by `Dockerfile.worker` are present in the tree (`package-lock.json`, `.npmrc`, `scripts/check-node-engine.cjs`, `scripts/build-worker.mjs`, `worker/python/requirements.txt`, `worker/index.ts`) and the `server-only` -bundle path is guarded by `tests/tsx-server-only-runner.test.ts` plus +bundle path is guarded by `tests/tsx-server-only-runner.test.ts`, +`tests/worker-runtime-control.test.ts`, `tests/worker-run-loop.test.ts`, +`tests/worker-runtime-validation.test.ts` plus `tests/worker-bundle.test.ts` (resolve-checks every bundle external against plain-`node` ESM resolution and the `--omit=dev` prune). The image also compiles all committed Python helpers and runs `worker/python/test_*.py` before removing @@ -89,10 +91,17 @@ docker build -f Dockerfile.worker -t clinical-kb-worker . `scripts/build-worker.mjs`), so tsx and the rest of the dev toolchain never reach the image. - **Tesseract OCR** (Debian package; bundles English language data). -- A **Python venv** at `/opt/ocr-venv` with `worker/python/requirements.txt` - (PyMuPDF, Pillow, pytesseract). The venv is first on `PATH`, so the default +- A **Python venv** at `/opt/ocr-venv` with a pinned, hashed + `worker/python/requirements.txt` generated from `worker/python/requirements.in` + via `pip-tools` (`npm run generate:worker-python-lock`). `pip check` runs + before the image is promoted. The venv is first on `PATH`, so the default `PYTHON_BIN=python` resolves to it — no override needed in-container. - Runtime is the non-root `node` user. No secret is baked into any layer. +- `STOPSIGNAL SIGTERM` is set; the worker drains the active batch and exits `0` + on `SIGTERM`/`SIGINT`. Fatal errors still exit `1` and fire the webhook. +- `dist/worker/validate-runtime.mjs` runs during the image build and again in + CI with `--network=none` to prove Node, module resolution, Python, Tesseract, + PyMuPDF, Pillow and pytesseract are present without calling Supabase/OpenAI. - `CMD` runs the bundle under plain `node`. The build aliases `server-only` to the standalone stub (what `scripts/run-tsx.mjs` did at runtime), so `worker/index.ts`'s `import "server-only"` resolves outside the Next diff --git a/package.json b/package.json index 4083af3737..708f4bd4c0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "build": "node scripts/run-heavy.mjs --npm-script build:internal", "build:internal": "node scripts/guard-next-build.mjs && npm run check:upload-limit-parity && node --max-old-space-size=8192 ./node_modules/next/dist/bin/next build --webpack && node scripts/check-client-bundle-secrets.mjs", "build:analyze": "node scripts/build-analyze.mjs", + "generate:worker-python-lock": "node scripts/generate-worker-python-lock.mjs", + "check:worker-python-lock": "node scripts/check-worker-python-lock.mjs", + "smoke:app-container": "node scripts/app-container-smoke.mjs", "check:bundle-budget": "node scripts/check-bundle-budget.mjs", "check:lighthouse-budget": "node scripts/check-lighthouse-budget.mjs", "verify:lighthouse": "node scripts/run-lighthouse-budget.mjs", diff --git a/scripts/app-container-smoke.mjs b/scripts/app-container-smoke.mjs new file mode 100644 index 0000000000..ea8d48745a --- /dev/null +++ b/scripts/app-container-smoke.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node +/** + * app-container-smoke — run the built app image provider-free and verify: + * - /api/health returns HTTP 200 with status: ok, openaiConfig: skipped, + * supabaseConfig: ok + * - PID 1 is the Next production server (`next start` argv, or Next 16's + * rewritten `next-server (v…)` process title after boot) + * - container stops cleanly within a bounded timeout + */ +import { spawnSync } from "node:child_process"; + +const IMAGE_REF = process.argv[2] ?? "clinical-kb-app:ci"; +const CONTAINER_NAME = "clinical-kb-app-smoke"; +const STOP_TIMEOUT_SECONDS = 30; + +function run(args, opts = {}) { + const result = spawnSync(args[0], args.slice(1), { encoding: "utf8", stdio: "pipe", ...opts }); + return result; +} + +function cleanContainer() { + spawnSync("docker", ["rm", "-f", CONTAINER_NAME], { encoding: "utf8" }); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForHealth(port = 3000, attempts = 30) { + const healthUrl = `http://127.0.0.1:${port}/api/health`; + for (let i = 0; i < attempts; i += 1) { + const result = run([ + "docker", + "exec", + CONTAINER_NAME, + "node", + "-e", + `fetch("${healthUrl}").then((r)=>r.json()).then((j)=>{console.log(JSON.stringify(j));process.exit(0);}).catch((e)=>{console.error(e.message);process.exit(1);})`, + ]); + if (result.status === 0) { + try { + return JSON.parse(result.stdout); + } catch { + /* not ready yet */ + } + } + await sleep(1000); + } + return null; +} + +async function main() { + cleanContainer(); + + const runResult = run([ + "docker", + "run", + "-d", + "--name", + CONTAINER_NAME, + "--network=none", + "--cap-drop=ALL", + "--security-opt", + "no-new-privileges:true", + "-e", + "NODE_ENV=production", + "-e", + "NEXT_PUBLIC_DEMO_MODE=false", + "-e", + "NEXT_PUBLIC_SUPABASE_URL=https://sjrfecxgysukkwxsowpy.supabase.co", + "-e", + "SUPABASE_SERVICE_ROLE_KEY=smoke-test-placeholder", + "-e", + "SUPABASE_PROJECT_REF=sjrfecxgysukkwxsowpy", + "-e", + "SUPABASE_PROJECT_NAME=Clinical KB Database", + // Production instrumentation refuses to boot without a keyed query-hash + // secret (min 16 chars). Placeholder is provider-free and CI-only. + "-e", + "RAG_QUERY_HASH_SECRET=smoke-test-hash-secret", + "-e", + "RAG_PROVIDER_MODE=offline", + "-e", + "NEXT_TELEMETRY_DISABLED=1", + IMAGE_REF, + ]); + + if (runResult.status !== 0) { + console.error("docker run failed", runResult.stderr); + process.exit(1); + } + + const health = await waitForHealth(); + if (!health) { + console.error("App health endpoint did not become ready in time"); + const logs = run(["docker", "logs", "--tail", "80", CONTAINER_NAME]); + if (logs.stdout) console.error(logs.stdout); + if (logs.stderr) console.error(logs.stderr); + cleanContainer(); + process.exit(1); + } + + if (health.status !== "ok") { + console.error("App health status not ok:", JSON.stringify(health, null, 2)); + cleanContainer(); + process.exit(1); + } + if (health.checks?.openaiConfig !== "skipped") { + console.error("Expected openaiConfig=skipped, got:", health.checks?.openaiConfig); + cleanContainer(); + process.exit(1); + } + if (health.checks?.supabaseConfig !== "ok") { + console.error("Expected supabaseConfig=ok, got:", health.checks?.supabaseConfig); + cleanContainer(); + process.exit(1); + } + if (health.demoMode !== false) { + console.error("Expected demoMode=false in production, got:", health.demoMode); + cleanContainer(); + process.exit(1); + } + + const psResult = run([ + "docker", + "exec", + CONTAINER_NAME, + "node", + "-e", + "console.log(require('fs').readFileSync('/proc/1/cmdline','utf8').replace(/\\u0000/g,' ').trim())", + ]); + const pid1 = psResult.stdout?.trim() ?? ""; + // Next 16 rewrites the process title to `next-server (vX.Y.Z)` after + // `next start` boots; accept either form so smoke tracks the live server. + const pid1LooksLikeNextServer = pid1.includes("next start") || /\bnext-server\b/.test(pid1); + if (!pid1LooksLikeNextServer) { + console.error("PID 1 is not the expected Next production server:", pid1); + const logs = run(["docker", "logs", "--tail", "80", CONTAINER_NAME]); + if (logs.stdout) console.error(logs.stdout); + if (logs.stderr) console.error(logs.stderr); + cleanContainer(); + process.exit(1); + } + + const stopResult = run(["docker", "stop", "-t", String(STOP_TIMEOUT_SECONDS), CONTAINER_NAME]); + if (stopResult.status !== 0) { + console.error("docker stop failed:", stopResult.stderr || stopResult.stdout); + cleanContainer(); + process.exit(1); + } + + const inspectResult = run([ + "docker", + "inspect", + "--format", + "{{.State.Running}} {{.State.OOMKilled}} {{.State.ExitCode}}", + CONTAINER_NAME, + ]); + const [running, oom, exitCode] = inspectResult.stdout?.trim().split(" ") ?? ["true", "true", "-1"]; + + if (running === "true") { + console.error("Container still running after docker stop"); + cleanContainer(); + process.exit(1); + } + if (oom === "true") { + console.error("Container was OOMKilled"); + cleanContainer(); + process.exit(1); + } + // 137 = SIGKILL (128+9). Graceful STOPSIGNAL SIGTERM must not escalate. + if (exitCode === "137") { + console.error("Container exited 137 (SIGKILL) — graceful stop timed out"); + cleanContainer(); + process.exit(1); + } + + console.log(`App smoke passed. PID 1: ${pid1}. Exit code: ${exitCode}. Health:`, JSON.stringify(health, null, 2)); + cleanContainer(); + process.exit(0); +} + +main().catch((error) => { + console.error(error); + cleanContainer(); + process.exit(1); +}); diff --git a/scripts/build-worker.mjs b/scripts/build-worker.mjs index 8805f7bc9a..ca510a62a5 100644 --- a/scripts/build-worker.mjs +++ b/scripts/build-worker.mjs @@ -18,6 +18,7 @@ * - `.mjs` output: the repo's package.json has no `"type": "module"`, so the * extension is what marks the bundle as ESM. */ +import { writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { build } from "esbuild"; @@ -45,6 +46,25 @@ export const workerBuildOptions = { }, }; +export const validateRuntimeBuildOptions = { + ...workerBuildOptions, + entryPoints: ["worker/validate-runtime.ts"], + outfile: "dist/worker/validate-runtime.mjs", +}; + +function extractExternals(metafile) { + const externals = new Set(); + for (const output of Object.values(metafile.outputs)) { + for (const imported of output.imports ?? []) { + if (imported.external) externals.add(imported.path); + } + } + return [...externals].sort(); +} + if (import.meta.url === pathToFileURL(process.argv[1]).href) { - await build(workerBuildOptions); + const indexResult = await build({ ...workerBuildOptions, metafile: true }); + const externals = extractExternals(indexResult.metafile ?? {}); + writeFileSync("dist/worker/externals.json", `${JSON.stringify(externals, null, 2)}\n`); + await build(validateRuntimeBuildOptions); } diff --git a/scripts/check-image-content-contract.mjs b/scripts/check-image-content-contract.mjs new file mode 100644 index 0000000000..cd174b4929 --- /dev/null +++ b/scripts/check-image-content-contract.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * check-image-content-contract — assert the final images meet hardening + * requirements: non-root user, SIGTERM stop signal, expected OCI labels, + * expected Cmd, and absence of development/secrets files. + */ +import { spawnSync } from "node:child_process"; + +const APP_IMAGE = process.argv[2] ?? "clinical-kb-app:ci"; +const WORKER_IMAGE = process.argv[3] ?? "clinical-kb-worker:ci"; + +function run(args) { + const result = spawnSync(args[0], args.slice(1), { encoding: "utf8", stdio: "pipe" }); + return result; +} + +function inspect(image, format) { + const r = run(["docker", "inspect", "--format", format, image]); + if (r.status !== 0) throw new Error(`docker inspect failed for ${image}: ${r.stderr}`); + return r.stdout.trim(); +} + +function assertImage(image, kind) { + const user = inspect(image, "{{.Config.User}}"); + if (user !== "node") throw new Error(`${image}: expected User=node, got ${user}`); + + const stopSignal = inspect(image, "{{.Config.StopSignal}}"); + if (stopSignal !== "SIGTERM") throw new Error(`${image}: expected StopSignal=SIGTERM, got ${stopSignal}`); + + const cmd = inspect(image, "{{json .Config.Cmd}}"); + // `docker inspect --format '{{json .Config.Cmd}}'` prints the literal + // string `null` when Cmd is unset — treat that as missing. + if (!cmd || cmd === "null") throw new Error(`${image}: missing Cmd`); + if (kind === "app" && !cmd.includes("next start")) { + throw new Error(`${image}: expected app Cmd to contain 'next start', got ${cmd}`); + } + if (kind === "worker" && !cmd.includes("dist/worker/index.mjs")) { + throw new Error(`${image}: expected worker Cmd to contain 'dist/worker/index.mjs', got ${cmd}`); + } + + const sourceLabel = inspect(image, '{{index .Config.Labels "org.opencontainers.image.source"}}'); + if (!sourceLabel) throw new Error(`${image}: missing org.opencontainers.image.source label`); +} + +function assertPathAbsent(image, path) { + const r = run(["docker", "run", "--rm", "--entrypoint", "test", image, "!", "-e", path]); + if (r.status !== 0) throw new Error(`${image}: unexpected path exists: ${path}`); +} + +function assertShellListingClean(image, shellCommand, failureMessage) { + const r = run(["docker", "run", "--rm", "--entrypoint", "sh", image, "-c", shellCommand]); + if (r.status !== 0) { + throw new Error( + `${image}: docker run failed while checking content (${failureMessage}): ${r.stderr || r.stdout || `exit ${r.status}`}`, + ); + } + if (r.stdout?.trim()) throw new Error(`${image}: ${failureMessage}`); +} + +function assertWorkerPythonClean(image) { + // Absolute path so a wrong WORKDIR cannot silently skip the glob. + assertShellListingClean( + image, + "ls /app/worker/python/test_*.py 2>/dev/null || true", + "worker/python test_*.py files were not removed", + ); +} + +function main() { + assertImage(APP_IMAGE, "app"); + assertImage(WORKER_IMAGE, "worker"); + + for (const image of [APP_IMAGE, WORKER_IMAGE]) { + assertPathAbsent(image, "/app/.env"); + assertPathAbsent(image, "/app/.env.example"); + assertPathAbsent(image, "/app/.env.local"); + assertPathAbsent(image, "/app/tests"); + assertPathAbsent(image, "/app/*.pem"); // glob not supported; checked below + } + + for (const image of [APP_IMAGE, WORKER_IMAGE]) { + assertShellListingClean(image, "ls /app/*.pem 2>/dev/null || true", "unexpected .pem files in /app"); + } + + assertWorkerPythonClean(WORKER_IMAGE); + + console.log(`Image content contract passed for ${APP_IMAGE} and ${WORKER_IMAGE}`); +} + +main(); diff --git a/scripts/check-worker-python-lock.mjs b/scripts/check-worker-python-lock.mjs new file mode 100644 index 0000000000..db6bdd0640 --- /dev/null +++ b/scripts/check-worker-python-lock.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * check-worker-python-lock — verify that worker/python/requirements.txt is + * in sync with worker/python/requirements.in and is a hashed lockfile. + */ +import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const PYTHON = process.env.PYTHON_BIN?.trim() || (process.platform === "win32" ? "python" : "python3"); +const PIP_TOOLS_VERSION = "7.6.0"; +const IN_FILE = "worker/python/requirements.in"; +const OUT_FILE = "worker/python/requirements.txt"; + +function run(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts }); + if (result.status !== 0) { + throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${result.stderr || ""}\n${result.stdout || ""}`); + } + return result; +} + +function venvPython(venvDir) { + return process.platform === "win32" + ? path.join(venvDir, "Scripts", "python.exe") + : path.join(venvDir, "bin", "python"); +} + +function ensureVenv(venvDir) { + if (existsSync(venvDir)) rmSync(venvDir, { recursive: true, force: true }); + run(PYTHON, ["-m", "venv", venvDir]); + const python = venvPython(venvDir); + run(python, ["-m", "pip", "install", "setuptools", "wheel"]); + run(python, ["-m", "pip", "install", `pip-tools==${PIP_TOOLS_VERSION}`]); +} + +function pipCompile(venvDir, outputFile) { + const python = venvPython(venvDir); + run(python, ["-m", "piptools", "compile", "--generate-hashes", "--output-file", outputFile, IN_FILE], { + env: { ...process.env, CUSTOM_COMPILE_COMMAND: "npm run generate:worker-python-lock" }, + }); +} + +function normalize(contents) { + return contents + .split(/\r?\n/) + .filter((line) => line.trim() !== "" && !line.trim().startsWith("#")) + .join("\n"); +} + +function assertHashedLockfile(contents) { + const lines = contents.split(/\r?\n/); + const hasPinned = lines.some((line) => /==/.test(line)); + const hasHash = lines.some((line) => /--hash=sha256:/.test(line)); + if (!hasPinned) throw new Error("requirements.txt does not contain pinned (==) versions"); + if (!hasHash) throw new Error("requirements.txt does not contain --hash=sha256 entries"); +} + +function main() { + if (!existsSync(IN_FILE)) throw new Error(`Missing ${IN_FILE}`); + if (!existsSync(OUT_FILE)) throw new Error(`Missing ${OUT_FILE}`); + + const committed = readFileSync(OUT_FILE, "utf8"); + assertHashedLockfile(committed); + + const venvDir = mkdtempSync(path.join(tmpdir(), "pip-tools-check-")); + let generated; + try { + ensureVenv(venvDir); + const tempOut = path.join(venvDir, "requirements.txt"); + pipCompile(venvDir, tempOut); + generated = readFileSync(tempOut, "utf8"); + } finally { + rmSync(venvDir, { recursive: true, force: true }); + } + + if (normalize(committed) !== normalize(generated)) { + mkdirSync("tmp", { recursive: true }); + writeFileSync( + "tmp/requirements-generated-diff.txt", + `--- committed\n+++ generated\n${committed}\n---\n${generated}`, + ); + throw new Error(`${OUT_FILE} is out of sync with ${IN_FILE}. Regenerate with: npm run generate:worker-python-lock`); + } + + console.log(`${OUT_FILE} is in sync with ${IN_FILE}`); +} + +main(); diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index f4e445ca52..054fbe7b8d 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -212,7 +212,16 @@ const containerPatterns = [ "railway.worker.json", "tests/stubs/server-only.ts", /^worker\/.+/, - /^scripts\/(check-node-engine|check-upload-limit-parity|guard-next-build|build-worker|run-heavy|check-client-bundle-secrets|install-git-hooks)\.(?:cjs|mjs)$/, + "worker/python/requirements.in", + "worker/python/requirements.txt", + "scripts/app-container-smoke.mjs", + "scripts/check-image-content-contract.mjs", + "scripts/trivy-image-scan.mjs", + "scripts/resolve-oci-image-digest.mjs", + "scripts/generate-worker-python-lock.mjs", + "scripts/check-worker-python-lock.mjs", + "tests/container-ci-contract.test.ts", + /^scripts\/(check-node-engine|check-upload-limit-parity|guard-next-build|build-worker|run-heavy|check-client-bundle-secrets|install-git-hooks|app-container-smoke|check-image-content-contract|trivy-image-scan|resolve-oci-image-digest|generate-worker-python-lock|check-worker-python-lock)\.(?:cjs|mjs)$/, ]; const sourcePatterns = ["data", "src", "tests", "scripts", "worker", "playwright", "public", "supabase"]; diff --git a/scripts/generate-worker-python-lock.mjs b/scripts/generate-worker-python-lock.mjs new file mode 100644 index 0000000000..5a19b6319b --- /dev/null +++ b/scripts/generate-worker-python-lock.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * generate-worker-python-lock — pin worker/python/requirements.txt from + * worker/python/requirements.in using pip-tools. + */ +import { mkdtempSync, rmSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const PYTHON = process.env.PYTHON_BIN?.trim() || (process.platform === "win32" ? "python" : "python3"); +const PIP_TOOLS_VERSION = "7.6.0"; +const IN_FILE = "worker/python/requirements.in"; +const OUT_FILE = "worker/python/requirements.txt"; + +function run(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts }); + if (result.status !== 0) { + throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${result.stderr || ""}\n${result.stdout || ""}`); + } + return result; +} + +function venvPython(venvDir) { + return process.platform === "win32" + ? path.join(venvDir, "Scripts", "python.exe") + : path.join(venvDir, "bin", "python"); +} + +function ensureVenv(venvDir) { + if (existsSync(venvDir)) rmSync(venvDir, { recursive: true, force: true }); + run(PYTHON, ["-m", "venv", venvDir]); + const python = venvPython(venvDir); + run(python, ["-m", "pip", "install", "setuptools", "wheel"]); + run(python, ["-m", "pip", "install", `pip-tools==${PIP_TOOLS_VERSION}`]); +} + +function pipCompile(venvDir, outputFile) { + const python = venvPython(venvDir); + const args = ["-m", "piptools", "compile", "--generate-hashes", "--output-file", outputFile, IN_FILE]; + run(python, args, { env: { ...process.env, CUSTOM_COMPILE_COMMAND: "npm run generate:worker-python-lock" } }); +} + +function main() { + if (!existsSync(IN_FILE)) throw new Error(`Missing ${IN_FILE}`); + + const venvDir = mkdtempSync(path.join(tmpdir(), "pip-tools-")); + try { + ensureVenv(venvDir); + pipCompile(venvDir, OUT_FILE); + console.log(`Generated ${OUT_FILE}`); + } finally { + rmSync(venvDir, { recursive: true, force: true }); + } +} + +main(); diff --git a/scripts/resolve-oci-image-digest.mjs b/scripts/resolve-oci-image-digest.mjs new file mode 100644 index 0000000000..1ba1f24285 --- /dev/null +++ b/scripts/resolve-oci-image-digest.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/** + * resolve-oci-image-digest — print the multi-platform image index digest for a + * Docker Hub reference. Use it to pin base images and scanners in Dockerfiles. + */ +import { spawnSync } from "node:child_process"; + +const imageRef = process.argv[2]; +if (!imageRef) { + console.error("Usage: node scripts/resolve-oci-image-digest.mjs "); + process.exit(1); +} + +const result = spawnSync("docker", ["buildx", "imagetools", "inspect", imageRef], { + encoding: "utf8", + stdio: "pipe", +}); + +if (result.status !== 0) { + console.error(result.stderr || result.stdout || `docker buildx imagetools inspect ${imageRef} failed`); + process.exit(1); +} + +const digestMatch = result.stdout.match(/Digest:\s+(sha256:[a-f0-9]{64})/); +if (!digestMatch) { + console.error("Could not extract digest from docker buildx imagetools output"); + process.exit(1); +} + +console.log(`${imageRef}@${digestMatch[1]}`); diff --git a/scripts/trivy-image-scan.mjs b/scripts/trivy-image-scan.mjs new file mode 100644 index 0000000000..773e357aac --- /dev/null +++ b/scripts/trivy-image-scan.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +/** + * trivy-image-scan — vulnerability scan and SBOM generation using a pinned + * immutable Trivy Docker image. Designed for Linux CI; local runs need a + * `trivy` binary on PATH or Docker. + * + * The Docker fallback exports the target image with `docker save` and mounts + * only that tar (plus an optional output dir). It never bind-mounts the Docker + * daemon socket, which would be equivalent to root on the host. + */ +import { basename, dirname, join, resolve } from "node:path"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; + +// Pin to a multi-platform image index digest. Update with: +// node scripts/resolve-oci-image-digest.mjs aquasec/trivy:0.59.1 +const TRIVY_IMAGE = "aquasec/trivy:0.59.1@sha256:029e990b328d149bf0a9ffe355919041e1f86192db2df47e217f8a36dd42ceac"; + +function run(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts }); + if (result.error) throw result.error; + return result; +} + +function localTrivy() { + const result = run(process.platform === "win32" ? "where" : "which", ["trivy"]); + return result.stdout?.split(/\r?\n/)[0]?.trim() || null; +} + +/** + * @param {string} imageRef + * @param {string[]} trivyArgs use "__IMAGE__" where the image ref belongs and + * "__OUT__" where an output path belongs (when outputHostPath is set) + * @param {{ outputHostPath?: string | null }} [opts] + */ +function runDockerTrivy(imageRef, trivyArgs, { outputHostPath = null } = {}) { + const workDir = mkdtempSync(join(tmpdir(), "trivy-scan-")); + const tarName = "image.tar"; + const tarPath = join(workDir, tarName); + try { + // Keep Docker's save temp files inside workDir so finally{} can reclaim them + // and CI runners do not fill the shared /tmp between sequential scans. + const saveEnv = { ...process.env, TMPDIR: workDir, DOCKER_TMPDIR: workDir }; + const save = run("docker", ["save", "-o", tarPath, imageRef], { env: saveEnv }); + if (save.status !== 0) { + const detail = save.stderr || save.stdout || `docker save failed for ${imageRef}`; + if (/no space left on device/i.test(detail)) { + console.error(`ENOSPC while docker-saving ${imageRef}; free builder cache and retry.`); + } + return { + status: 1, + stdout: save.stdout, + stderr: detail, + }; + } + + const mounts = ["-v", `${workDir}:/work:ro`]; + if (outputHostPath) { + mounts.push("-v", `${dirname(resolve(outputHostPath))}:/out`); + } + + const dockerArgs = ["run", "--rm", "--network=none", ...mounts, TRIVY_IMAGE]; + for (const arg of trivyArgs) { + if (arg === "__IMAGE__") { + dockerArgs.push("--input", `/work/${tarName}`); + } else if (arg === "__OUT__" && outputHostPath) { + dockerArgs.push(`/out/${basename(outputHostPath)}`); + } else { + dockerArgs.push(arg); + } + } + + return run("docker", dockerArgs); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } +} + +function main() { + const [imageRef, ...rest] = process.argv.slice(2); + if (!imageRef) { + console.error("Usage: node scripts/trivy-image-scan.mjs [--severity SEV] [--sbom ]"); + process.exit(1); + } + + function optionValue(flag) { + const idx = rest.indexOf(flag); + if (idx < 0) return null; + const value = rest[idx + 1]; + if (!value || value.startsWith("--")) { + console.error(`Missing value for ${flag}`); + process.exit(1); + } + return value; + } + + const sbomPath = optionValue("--sbom"); + const severity = optionValue("--severity") ?? "HIGH,CRITICAL"; + + const trivy = localTrivy(); + + if (sbomPath) { + const out = resolve(sbomPath); + const result = trivy + ? run(trivy, ["image", "--format", "cyclonedx", "--output", out, imageRef]) + : runDockerTrivy(imageRef, ["image", "--format", "cyclonedx", "--output", "__OUT__", "__IMAGE__"], { + outputHostPath: out, + }); + if (result.status !== 0) { + console.error(result.stderr || result.stdout); + process.exit(1); + } + console.log(`SBOM written to ${out}`); + } + + const result = trivy + ? run(trivy, ["image", "--format", "json", "--exit-code", "0", "--severity", severity, imageRef]) + : runDockerTrivy(imageRef, ["image", "--format", "json", "--exit-code", "0", "--severity", severity, "__IMAGE__"]); + + if (result.status !== 0) { + console.error(result.stderr || result.stdout || `Trivy failed to scan ${imageRef}`); + process.exit(1); + } + + let summary = null; + try { + const report = JSON.parse(result.stdout || "{}"); + const results = report.Results || []; + const vulnerabilities = results.flatMap((r) => r.Vulnerabilities || []); + const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, UNKNOWN: 0 }; + for (const v of vulnerabilities) { + counts[v.Severity ?? "UNKNOWN"] = (counts[v.Severity ?? "UNKNOWN"] ?? 0) + 1; + } + summary = `vulnerabilities=${vulnerabilities.length} (${Object.entries(counts) + .filter(([, n]) => n > 0) + .map(([sev, n]) => `${sev}=${n}`) + .join(", ")})`; + } catch { + summary = "could not parse Trivy JSON"; + } + + console.log(`Trivy scan completed for ${imageRef}: ${summary}`); +} + +main(); diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index d8a7f4af84..9323bb1b82 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -54,4 +54,77 @@ describe("container delivery contract", () => { expect(dockerignore, `missing Docker exclusion: ${entry}`).toContain(entry); } }); + + it("pins the Node base image to a multi-platform digest", () => { + const dockerfile = read("Dockerfile"); + const worker = read("Dockerfile.worker"); + const digest = /sha256:[a-f0-9]{64}/; + + expect(dockerfile).toMatch(digest); + expect(dockerfile).toContain("FROM node-base"); + expect(worker).toMatch(digest); + expect(worker).toContain("FROM node-base"); + }); + + it("requires both images to stop on SIGTERM", () => { + expect(read("Dockerfile")).toContain("STOPSIGNAL SIGTERM"); + expect(read("Dockerfile.worker")).toContain("STOPSIGNAL SIGTERM"); + }); + + it("runs a provider-free runtime validator inside the worker image", () => { + expect(read("Dockerfile.worker")).toContain("dist/worker/validate-runtime.mjs"); + }); + + it("includes the new Docker hardening scripts", () => { + expect(read(".github/workflows/docker-image.yml")).toContain("check-image-content-contract"); + expect(read(".github/workflows/docker-image.yml")).toContain("app-container-smoke"); + }); + + it("supplies RAG_QUERY_HASH_SECRET so production instrumentation can boot in smoke", () => { + const smoke = read("scripts/app-container-smoke.mjs"); + const workflow = read(".github/workflows/docker-image.yml"); + expect(smoke).toContain("RAG_QUERY_HASH_SECRET=smoke-test-hash-secret"); + expect(workflow).toContain("RAG_QUERY_HASH_SECRET=smoke-test-hash-secret"); + }); + + it("accepts Next 16 next-server PID 1 in app container smoke", () => { + const smoke = read("scripts/app-container-smoke.mjs"); + expect(smoke).toContain("next start"); + expect(smoke).toContain("next-server"); + expect(smoke).toContain("pid1LooksLikeNextServer"); + }); + + it("avoids Docker-socket mounts and creates tmp/ before lock-diff diagnostics", () => { + const trivy = read("scripts/trivy-image-scan.mjs"); + const pythonLock = read("scripts/check-worker-python-lock.mjs"); + const runtime = read("worker/validate-runtime.ts"); + const sleep = read("worker/runtime-control.ts"); + + expect(trivy).not.toContain("/var/run/docker.sock"); + expect(trivy).toContain("docker save"); + expect(trivy).toContain("optionValue"); + expect(pythonLock).toContain('mkdirSync("tmp", { recursive: true })'); + expect(runtime).toContain("pathToFileURL(options.externalsPath)"); + expect(runtime).not.toContain("file://${options.externalsPath}"); + expect(runtime).toContain("builtinModules"); + // Single settle path — no stray Promise.race timer that outlives stop(). + expect(sleep).not.toMatch(/Promise\.race\(\[\s*new Promise/); + }); + + it("fails closed on missing Cmd, shell listing errors, and SIGKILL smoke exits", () => { + const contract = read("scripts/check-image-content-contract.mjs"); + const smoke = read("scripts/app-container-smoke.mjs"); + expect(contract).toContain('cmd === "null"'); + expect(contract).toContain("assertShellListingClean"); + expect(contract).toContain("/app/worker/python/test_*.py"); + expect(smoke).toContain('exitCode === "137"'); + expect(smoke).toContain("State.Running"); + }); + + it("keeps Trivy SBOM and vulnerability steps non-blocking in CI", () => { + const workflow = read(".github/workflows/docker-image.yml"); + expect(workflow).toMatch(/Generate SBOMs[\s\S]*?continue-on-error:\s*true/); + expect(workflow).toMatch(/Vulnerability scan \(HIGH,CRITICAL\)[\s\S]*?continue-on-error:\s*true/); + expect(workflow).toContain("if-no-files-found: warn"); + }); }); diff --git a/tests/worker-python-lock.test.ts b/tests/worker-python-lock.test.ts new file mode 100644 index 0000000000..dd18801083 --- /dev/null +++ b/tests/worker-python-lock.test.ts @@ -0,0 +1,22 @@ +import { existsSync, readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("worker Python lockfile", () => { + it("has a requirements.in and a hashed, pinned requirements.txt", () => { + expect(existsSync("worker/python/requirements.in"), "requirements.in missing").toBe(true); + expect(existsSync("worker/python/requirements.txt"), "requirements.txt missing").toBe(true); + + const contents = readFileSync("worker/python/requirements.txt", "utf8"); + const lines = contents.split(/\r?\n/); + + expect( + lines.some((line) => line.includes("==")), + "requirements.txt has no pinned versions", + ).toBe(true); + expect( + lines.some((line) => line.includes("--hash=sha256:")), + "requirements.txt has no hashes", + ).toBe(true); + expect(contents.includes("medspacy"), "medspacy not present in lock").toBe(true); + }); +}); diff --git a/tests/worker-run-loop.test.ts b/tests/worker-run-loop.test.ts new file mode 100644 index 0000000000..0dad8979ff --- /dev/null +++ b/tests/worker-run-loop.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it, vi } from "vitest"; +import { WorkerRuntimeControl } from "../worker/runtime-control"; +import { runWorkerLoop } from "../worker/run-loop"; +import type { JobRow } from "../worker/types"; + +function fakeJob(id = "job-1"): JobRow { + return { + id, + document_id: "doc-1", + batch_id: null, + attempt_count: 0, + max_attempts: 3, + documents: { + id: "doc-1", + owner_id: null, + title: "Test", + file_name: "test.pdf", + file_type: "application/pdf", + storage_path: "docs/test.pdf", + metadata: null, + }, + }; +} + +describe("runWorkerLoop", () => { + it("exits immediately when no jobs are available and once is set", async () => { + const ctrl = new WorkerRuntimeControl(); + const claim = vi.fn().mockResolvedValue([]); + const probe = vi.fn().mockResolvedValue({ ok: true }); + + await runWorkerLoop({ + once: true, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim, + process: vi.fn(), + probe, + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + }); + + expect(claim).toHaveBeenCalledOnce(); + }); + + it("drains an active batch before exiting after stop", async () => { + const ctrl = new WorkerRuntimeControl(); + const job = fakeJob(); + const processFn = vi.fn().mockImplementation(async () => { + ctrl.stop(); + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + const claim = vi.fn().mockResolvedValueOnce([job]).mockResolvedValue([]); + const probe = vi.fn().mockResolvedValue({ ok: true }); + + await runWorkerLoop({ + once: false, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim, + process: processFn, + probe, + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + }); + + expect(processFn).toHaveBeenCalledWith(job); + expect(ctrl.isStopped).toBe(true); + expect(claim).toHaveBeenCalledOnce(); + }); + + it("does not claim new jobs after stop during probe", async () => { + const ctrl = new WorkerRuntimeControl(); + let probeCount = 0; + const claim = vi.fn().mockResolvedValue([fakeJob()]); + const probe = vi.fn().mockImplementation(async () => { + probeCount += 1; + if (probeCount === 1) { + ctrl.stop(); + } + return { ok: true }; + }); + + await runWorkerLoop({ + once: false, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim, + process: vi.fn(), + probe, + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + }); + + expect(ctrl.isStopped).toBe(true); + expect(claim).not.toHaveBeenCalled(); + }); + + it("aborts with exit code 1 in once mode when probe fails", async () => { + const ctrl = new WorkerRuntimeControl(); + const probe = vi.fn().mockResolvedValue({ ok: false, message: "down" }); + + await expect( + runWorkerLoop({ + once: true, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim: vi.fn(), + process: vi.fn(), + probe, + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + }), + ).rejects.toThrow("Supabase health check failed in --once mode"); + }); + + it("backs off and does not throw when claim fails and failures are below threshold", async () => { + const ctrl = new WorkerRuntimeControl(); + const claim = vi.fn().mockRejectedValue(new Error("claim error")); + const probe = vi.fn().mockResolvedValue({ ok: true }); + + setTimeout(() => ctrl.stop(), 250); + + await runWorkerLoop({ + once: false, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim, + process: vi.fn(), + probe, + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + }); + + expect(claim).toHaveBeenCalled(); + }); + + it("captures claim exceptions only on the threshold transition", async () => { + const ctrl = new WorkerRuntimeControl(); + const claim = vi.fn().mockRejectedValue(new Error("claim error")); + const probe = vi.fn().mockResolvedValue({ ok: true }); + const captureException = vi.fn(); + + setTimeout(() => ctrl.stop(), 200); + + await runWorkerLoop({ + once: false, + pollMs: 1000, + healthBackoffMs: 20, + maxClaimFailures: 2, + claim, + process: vi.fn(), + probe, + backoff: () => 10, + controller: ctrl, + log: () => {}, + captureException, + }); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException.mock.calls[0]?.[1]).toBe("claim"); + }); + + it("captures process exceptions without aborting the batch", async () => { + const ctrl = new WorkerRuntimeControl(); + const jobs = [fakeJob("job-1"), fakeJob("job-2")]; + const processFn = vi.fn().mockRejectedValueOnce(new Error("process boom")).mockResolvedValueOnce(undefined); + const captureException = vi.fn(); + const claim = vi.fn().mockResolvedValueOnce(jobs).mockResolvedValue([]); + + await runWorkerLoop({ + once: true, + pollMs: 1000, + healthBackoffMs: 1000, + maxClaimFailures: 3, + claim, + process: processFn, + probe: vi.fn().mockResolvedValue({ ok: true }), + backoff: (n) => n * 10, + controller: ctrl, + log: () => {}, + captureException, + }); + + expect(processFn).toHaveBeenCalledTimes(2); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException.mock.calls[0]?.[1]).toBe("process"); + }); +}); diff --git a/tests/worker-runtime-control.test.ts b/tests/worker-runtime-control.test.ts new file mode 100644 index 0000000000..5560f8556b --- /dev/null +++ b/tests/worker-runtime-control.test.ts @@ -0,0 +1,53 @@ +import { EventEmitter } from "node:events"; +import { describe, expect, it, vi } from "vitest"; +import { WorkerRuntimeControl } from "../worker/runtime-control"; + +describe("WorkerRuntimeControl", () => { + it("stop is idempotent", () => { + const ctrl = new WorkerRuntimeControl(); + const cb = vi.fn(); + ctrl.onStop(cb); + + ctrl.stop(); + ctrl.stop(); + + expect(ctrl.isStopped).toBe(true); + expect(cb).toHaveBeenCalledOnce(); + }); + + it("interrupts sleep when stopped", async () => { + const ctrl = new WorkerRuntimeControl(); + const start = Date.now(); + const promise = ctrl.sleep(10_000); + ctrl.stop(); + await promise; + expect(Date.now() - start).toBeLessThan(1000); + }); + + it("returns immediately from sleep when already stopped", async () => { + const ctrl = new WorkerRuntimeControl(); + ctrl.stop(); + const start = Date.now(); + await ctrl.sleep(10_000); + expect(Date.now() - start).toBeLessThan(10); + }); + + it("attaches and detaches signals", () => { + const source = new EventEmitter(); + const ctrl = new WorkerRuntimeControl({ signalSource: source }); + ctrl.attachSignals(["SIGTERM"]); + source.emit("SIGTERM"); + expect(ctrl.isStopped).toBe(true); + + const next = new WorkerRuntimeControl({ signalSource: source }); + next.attachSignals(["SIGTERM"]); + source.emit("SIGTERM"); + expect(next.isStopped).toBe(true); + }); + + it("does not invoke real process signals by default during tests", () => { + const ctrl = new WorkerRuntimeControl(); + // This test only proves the constructor does not immediately attach handlers. + expect(ctrl.isStopped).toBe(false); + }); +}); diff --git a/tests/worker-runtime-validation.test.ts b/tests/worker-runtime-validation.test.ts new file mode 100644 index 0000000000..6074d0325c --- /dev/null +++ b/tests/worker-runtime-validation.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { validateRuntime } from "../worker/validate-runtime"; + +describe("worker runtime validation", () => { + it("passes when Node, npm, externals, and Python check pass", async () => { + const externals = ["node:fs"]; + + const result = await validateRuntime({ externals, skipPython: true }); + + expect(result.nodeVersion).toMatch(/^24\./); + expect(result.externals).toHaveLength(1); + expect(result.externals[0].ok).toBe(true); + expect(result.ok).toBe(true); + }); + + it("fails when an external cannot be resolved", async () => { + const externals = ["this-package-does-not-exist-12345"]; + + const result = await validateRuntime({ externals, skipPython: true }); + + expect(result.ok).toBe(false); + expect(result.errors.some((e) => e.includes("this-package-does-not-exist-12345"))).toBe(true); + }); + + it("treats bare Node builtin externals as resolved", async () => { + const result = await validateRuntime({ externals: ["fs", "child_process"], skipPython: true }); + + expect(result.ok).toBe(true); + expect(result.externals.every((entry) => entry.ok)).toBe(true); + }); +}); diff --git a/worker/main.ts b/worker/main.ts index 51be8e5081..cdc993ea4e 100644 --- a/worker/main.ts +++ b/worker/main.ts @@ -43,29 +43,9 @@ import { checkMedspacyPrerequisites, checkPythonPdfPrerequisites } from "./prere import { annotateChunkAssertions, defaultAssertionTargets } from "./assertion-tagging"; import { buildTableFactRows } from "./table-facts"; import { enrichmentRepairDecision, ingestionFailureDecision } from "./behavior"; - -type JobDocument = { - id: string; - owner_id: string | null; - title: string; - file_name: string; - file_type: string; - storage_path: string; - content_hash?: string | null; - source_path?: string | null; - import_batch_id?: string | null; - status?: string | null; - metadata: Record | null; -}; - -type JobRow = { - id: string; - document_id: string; - batch_id: string | null; - attempt_count: number; - max_attempts: number; - documents: JobDocument; -}; +import { WorkerRuntimeControl, WorkerAbortError } from "./runtime-control"; +import { runWorkerLoop } from "./run-loop"; +import type { JobDocument, JobRow } from "./types"; const supabase = createAdminClient(); const workerId = `${os.hostname()}-${process.pid}-${randomUUID().slice(0, 8)}`; @@ -1906,8 +1886,10 @@ async function processJob(job: JobRow) { async function main() { const once = process.argv.includes("--once"); + const controller = new WorkerRuntimeControl(); + controller.attachSignals(); + const prereqs = await checkPythonPdfPrerequisites(); - let consecutiveClaimFailures = 0; console.log(`Clinical KB worker started. worker=${workerId}`); if (!prereqs.ok) { console.warn(`PDF/OCR prerequisite warning: ${prereqs.detail}`); @@ -1919,62 +1901,31 @@ async function main() { } } - while (true) { - let jobs: JobRow[] = []; - try { - const health = await probeSupabaseHealth(supabase); - if (!health.ok) { - console.warn("Supabase health check failed; worker is backing off", { message: health.message }); - if (once) { - process.exitCode = 1; - return; - } - await new Promise((resolve) => setTimeout(resolve, env.WORKER_HEALTH_BACKOFF_MS)); - continue; - } - jobs = await claimJobs(); - consecutiveClaimFailures = 0; - } catch (error) { - consecutiveClaimFailures += 1; - console.warn("Ingestion job claim failed", safeErrorLogDetails(error)); - // Report only on the threshold transition: a single transient claim - // failure is normal, and a sustained outage must not spam Sentry on - // every subsequent poll after the retry budget is spent. - if (consecutiveClaimFailures === env.WORKER_MAX_CLAIM_FAILURES) { - captureWorkerException(error, "claim"); - } - if (once) throw error; - if (consecutiveClaimFailures >= env.WORKER_MAX_CLAIM_FAILURES) { - await new Promise((resolve) => setTimeout(resolve, env.WORKER_HEALTH_BACKOFF_MS)); - continue; - } - await new Promise((resolve) => setTimeout(resolve, workerBackoffMs(consecutiveClaimFailures))); - continue; - } - - if (jobs.length > 0) { - await Promise.all( - jobs.map(async (job) => { - console.log(safeIngestionJobLog(job.id)); - try { - await processJob(job); - } catch (error) { - console.error("Ingestion job processing failed", safeErrorLogDetails(error)); - captureWorkerException(error, "process"); - } - }), - ); - if (once) break; - continue; - } - - if (once) break; - await new Promise((resolve) => setTimeout(resolve, env.WORKER_POLL_MS)); - } + await runWorkerLoop({ + once, + pollMs: env.WORKER_POLL_MS, + healthBackoffMs: env.WORKER_HEALTH_BACKOFF_MS, + maxClaimFailures: env.WORKER_MAX_CLAIM_FAILURES, + claim: claimJobs, + process: processJob, + probe: () => probeSupabaseHealth(supabase), + backoff: workerBackoffMs, + controller, + log: (message, level, extra) => (extra ? console[level](message, extra) : console[level](message)), + captureException: captureWorkerException, + }); + console.log("Clinical KB worker stopped gracefully"); + // Flush before exit so any buffered claim/process events are not dropped. + await flushWorkerErrorTracking(); + process.exit(0); } main().catch(async (error) => { - console.error("Clinical KB worker stopped unexpectedly", safeErrorLogDetails(error)); + const abort = error instanceof WorkerAbortError ? error : null; + console.error( + abort ? "Clinical KB worker aborted" : "Clinical KB worker stopped unexpectedly", + safeErrorLogDetails(error), + ); captureWorkerException(error, "fatal"); if (env.WORKER_FAILURE_WEBHOOK_URL) { try { @@ -1982,7 +1933,7 @@ main().catch(async (error) => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - text: `CRITICAL: Clinical KB worker stopped unexpectedly. Error: ${error instanceof Error ? error.message : String(error)}`, + text: `CRITICAL: Clinical KB worker ${abort ? "aborted" : "stopped unexpectedly"}. Error: ${error instanceof Error ? error.message : String(error)}`, }), }); } catch (webhookError) { @@ -1991,5 +1942,5 @@ main().catch(async (error) => { } // Flush last: the process is about to exit and buffered events would be lost. await flushWorkerErrorTracking(); - process.exitCode = 1; + process.exit(abort?.exitCode ?? 1); }); diff --git a/worker/python/requirements.in b/worker/python/requirements.in new file mode 100644 index 0000000000..7a4e5b9163 --- /dev/null +++ b/worker/python/requirements.in @@ -0,0 +1,7 @@ +# Direct Python dependencies for the worker OCR stack. +# Run `npm run generate:worker-python-lock` to regenerate `requirements.txt`. + +PyMuPDF>=1.24.0 +pillow>=10.0.0 +pytesseract>=0.3.10 +medspacy>=1.1.0 diff --git a/worker/python/requirements.txt b/worker/python/requirements.txt index d0734e1b16..5d76a93e7a 100644 --- a/worker/python/requirements.txt +++ b/worker/python/requirements.txt @@ -1,6 +1,1571 @@ -PyMuPDF>=1.24.0 -pillow>=10.0.0 -pytesseract>=0.3.10 -# Optional: clinical assertion tagging (negation/uncertainty), used only when -# WORKER_MEDSPACY_ASSERTION=true. medspaCy pins its own compatible spaCy version. -medspacy>=1.1.0 +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# npm run generate:worker-python-lock +# +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + # via pydantic +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # jsonschema + # referencing +blis==0.7.11 \ + --hash=sha256:0421d6e44cda202b113a34761f9a062b53f8c2ae8e4ec8325a76e709fca93b6e \ + --hash=sha256:075431b13b9dd7b411894d4afbd4212acf4d0f56c5a20628f4b34902e90225f1 \ + --hash=sha256:0dc9dcb3843045b6b8b00432409fd5ee96b8344a324e031bfec7303838c41a1a \ + --hash=sha256:162e60d941a8151418d558a94ee5547cb1bbeed9f26b3b6f89ec9243f111a201 \ + --hash=sha256:1b68df4d01d62f9adaef3dad6f96418787265a6878891fc4e0fabafd6d02afba \ + --hash=sha256:2e62cd14b20e960f21547fee01f3a0b2ac201034d819842865a667c969c355d1 \ + --hash=sha256:2fb36989ed61233cfd48915896802ee6d3d87882190000f8cfe0cf4a3819f9a8 \ + --hash=sha256:2ff7abd784033836b284ff9f4d0d7cb0737b7684daebb01a4c9fe145ffa5a31e \ + --hash=sha256:31273d9086cab9c56986d478e3ed6da6752fa4cdd0f7b5e8e5db30827912d90d \ + --hash=sha256:324fdf62af9075831aa62b51481960e8465674b7723f977684e32af708bb7448 \ + --hash=sha256:5a305dbfc96d202a20d0edd6edf74a406b7e1404f4fa4397d24c68454e60b1b4 \ + --hash=sha256:5bb38adabbb22f69f22c74bad025a010ae3b14de711bf5c715353980869d491d \ + --hash=sha256:5bcdaf370f03adaf4171d6405a89fa66cb3c09399d75fc02e1230a78cd2759e4 \ + --hash=sha256:68544a1cbc3564db7ba54d2bf8988356b8c7acd025966e8e9313561b19f0fe2e \ + --hash=sha256:686a7d0111d5ba727cd62f374748952fd6eb74701b18177f525b16209a253c01 \ + --hash=sha256:688a8b21d2521c2124ee8dfcbaf2c385981ccc27e313e052113d5db113e27d3b \ + --hash=sha256:6df00c24128e323174cde5d80ebe3657df39615322098ce06613845433057614 \ + --hash=sha256:7de19264b1d49a178bf8035406d0ae77831f3bfaa3ce02942964a81a202abb03 \ + --hash=sha256:7ea09f961871f880d5dc622dce6c370e4859559f0ead897ae9b20ddafd6b07a2 \ + --hash=sha256:809d1da1331108935bf06e22f3cf07ef73a41a572ecd81575bdedb67defe3465 \ + --hash=sha256:844b6377e3e7f3a2e92e7333cc644095386548ad5a027fdc150122703c009956 \ + --hash=sha256:89b01c05a5754edc0b9a3b69be52cbee03f645b2ec69651d12216ea83b8122f0 \ + --hash=sha256:8ea55c6a4a60fcbf6a0fdce40df6e254451ce636988323a34b9c94b583fc11e5 \ + --hash=sha256:a73945a9d635eea528bccfdfcaa59dd35bd5f82a4a40d5ca31f08f507f3a6f81 \ + --hash=sha256:afebdb02d2dcf9059f23ce1244585d3ce7e95c02a77fd45a500e4a55b7b23583 \ + --hash=sha256:bfabd5272bbbe504702b8dfe30093653d278057656126716ff500d9c184b35a6 \ + --hash=sha256:ca684f5c2f05269f17aefe7812360286e9a1cee3afb96d416485efd825dbcf19 \ + --hash=sha256:cd5fba34c5775e4c440d80e4dea8acb40e2d3855b546e07c4e21fad8f972404c \ + --hash=sha256:cec6d48f75f7ac328ae1b6fbb372dde8c8a57c89559172277f66e01ff08d4d42 \ + --hash=sha256:cfee5ec52ba1e9002311d9191f7129d7b0ecdff211e88536fb24c865d102b50d \ + --hash=sha256:d06883f83d4c8de8264154f7c4a420b4af323050ed07398c1ff201c34c25c0d2 \ + --hash=sha256:dadf8713ea51d91444d14ad4104a5493fa7ecc401bbb5f4a203ff6448fadb113 \ + --hash=sha256:ee493683e3043650d4413d531e79e580d28a3c7bdd184f1b9cfa565497bda1e7 \ + --hash=sha256:f9caffcd14795bfe52add95a0dd8426d44e737b55fcb69e2b797816f4da0b1d2 + # via thinc +catalogue==2.0.10 \ + --hash=sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15 \ + --hash=sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f + # via + # spacy + # srsly + # thinc +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via requests +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 + # via requests +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via nltk +cloudpathlib==0.24.0 \ + --hash=sha256:b1c51e2d2ec7dc4fed6538991f4aea849d6cf11a7e6b9069f86e461aa1f9b5b4 \ + --hash=sha256:c521a984e77b47e656fe78e20a7e3e260e0ab45fc69e33ac01094227c979e34a + # via weasel +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # click + # loguru + # pytest + # tqdm + # typer + # wasabi +confection==0.1.5 \ + --hash=sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e \ + --hash=sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14 + # via + # thinc + # weasel +cymem==2.0.13 \ + --hash=sha256:03cb7bdb55718d5eb6ef0340b1d2430ba1386db30d33e9134d01ba9d6d34d705 \ + --hash=sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f \ + --hash=sha256:0d78a27c88b26c89bd1ece247d1d5939dba05a1dae6305aad8fd8056b17ddb51 \ + --hash=sha256:0dca715e708e545fd1d97693542378a00394b20a37779c1ae2c8bdbb43acef79 \ + --hash=sha256:1366c7437a209230f4b797fae10227a8206d4021d37c9f9c0d31fd97ea4feb35 \ + --hash=sha256:1710390e7fb2510a8091a1991024d8ae838fd06b02cdfdcd35f006192e3c6b0e \ + --hash=sha256:1775d3fd34cf099929b79c3e48469283642463f977af6801231f3c0e5d9c9369 \ + --hash=sha256:18ad5b116a82fa3674bc8838bd3792891b428971e2123ae8c0fd3ca472157c5e \ + --hash=sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588 \ + --hash=sha256:2fae4a29a36267772010a05ca85220e53f4fb3e1083d4f686342d8a3cc7620db \ + --hash=sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc \ + --hash=sha256:30c4e75a3a1d809e89106b0b21803eb78e839881aa1f5b9bd27b454bc73afde3 \ + --hash=sha256:38aefeb269597c1a0c2ddf1567dd8605489b661fa0369c6406c1acd433b4c7ba \ + --hash=sha256:45dcaba0f48bef9cc3d8b0b92058640244a95a9f12542210b51318da97c2cf28 \ + --hash=sha256:52b76216c59e0078cebd988754426c41ee60e7c972f4c5185ac32fbd53b6d035 \ + --hash=sha256:666ce6146bc61b9318aa70d91ce33f126b6344a25cf0b925621baed0c161e9cc \ + --hash=sha256:673183466b0ff2e060d97ec5116711d44200b8f7be524323e080d215ee2d44a5 \ + --hash=sha256:68489bf0035c4c280614067ab6a82815b01dc9fcd486742a5306fe9f68deb7ef \ + --hash=sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67 \ + --hash=sha256:6d36710760f817194dacb09d9fc45cb6a5062ed75e85f0ef7ad7aeeb13d80cc3 \ + --hash=sha256:717270dcfd8c8096b479c42708b151002ff98e434a7b6f1f916387a6c791e2ad \ + --hash=sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33 \ + --hash=sha256:7700b116524b087e0169f10f267539223b48240ef2734c3a727a9e6b4db9a671 \ + --hash=sha256:7a8d404a48af953084fcec4239487ab05e14b829da251628827d51c87880aeb8 \ + --hash=sha256:7e1a863a7f144ffb345397813701509cfc74fc9ed360a4d92799805b4b865dd1 \ + --hash=sha256:8431347901026e9b554daeb982f5f1ecf3780ce46435c7a8e0cb82490e58f13c \ + --hash=sha256:84c1168c563d9d1e04546cb65e3e54fde2bf814f7c7faf11fc06436598e386d1 \ + --hash=sha256:84e2976e38cd663f758e40b5497fa5cd183d7c5fb0d04ce81a4b42a1ba124ff0 \ + --hash=sha256:891fd9030293a8b652dc7fb9fdc79a910a6c76fc679cd775e6741b819ffea476 \ + --hash=sha256:89c4889bd16513ce1644ccfe1e7c473ba7ca150f0621e66feac3a571bde09e7e \ + --hash=sha256:8efc4f308169237aade0e82877a65a563833dec32eb7ab2326120253e0e9e918 \ + --hash=sha256:90c2d0c04bcda12cd5cebe9be93ce3af6742ad8da96e1b1907e3f8e00291def1 \ + --hash=sha256:92a2ce50afa5625fb5ce7c9302cee61e23a57ccac52cd0410b4858e572f8614b \ + --hash=sha256:9d441d0e45798ec1fd330373bf7ffa6b795f229275f64016b6a193e6e2a51522 \ + --hash=sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6 \ + --hash=sha256:a868c1e3fe4f6f51232883b7bee7319a35e36644604577f4c6032cf957b9056b \ + --hash=sha256:a9150be2896fbe0096911bc9138a54e6fcc3814f197bc0c3d64d7c005a5e579d \ + --hash=sha256:ac699c8ec72a3a9de8109bd78821ab22f60b14cf2abccd970b5ff310e14158ed \ + --hash=sha256:bc116a70cc3a5dc3d1684db5268eff9399a0be8603980005e5b889564f1ea42f \ + --hash=sha256:bee2791b3f6fc034ce41268851462bf662ff87e8947e35fb6dd0115b4644a61f \ + --hash=sha256:c0046a619ecc845ccb4528b37b63426a0cbcb4f14d7940add3391f59f13701e6 \ + --hash=sha256:c16cb80efc017b054f78998c6b4b013cef509c7b3d802707ce1f85a1d68361bf \ + --hash=sha256:c3ff57aec4f264451739b18b7eab9d161d51af42b570ca0392fc051dcca28cb1 \ + --hash=sha256:c8dbfddfe5c604974e17c6f373cedd4d25cd67f84812ede7dea12128fa0c2015 \ + --hash=sha256:c8f30971cadd5dcf73bcfbbc5849b1f1e1f40db8cd846c4aa7d3b5e035c7b583 \ + --hash=sha256:c90a6ecba994a15b17a3f45d7ec74d34081df2f73bd1b090e2adc0317e4e01b6 \ + --hash=sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8 \ + --hash=sha256:ce821e6ba59148ed17c4567113b8683a6a0be9c9ac86f14e969919121efb61a5 \ + --hash=sha256:d1c950eebb9f0f15e3ef3591313482a5a611d16fc12d545e2018cd607f40f472 \ + --hash=sha256:d2a4bf67db76c7b6afc33de44fb1c318207c3224a30da02c70901936b5aafdf1 \ + --hash=sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298 \ + --hash=sha256:d8d06ea59006b1251ad5794bcc00121e148434826090ead0073c7b7fedebe431 \ + --hash=sha256:e02d3e2c3bfeb21185d5a4a70790d9df40629a87d8d7617dc22b4e864f665fa3 \ + --hash=sha256:e03bb575a96c59bc210d7d59862747f0012696b0dac3427ce8af33c7afb3d4a2 \ + --hash=sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb \ + --hash=sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f \ + --hash=sha256:e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c \ + --hash=sha256:ec99efa03cf8ec11c8906aa4d4cc0c47df393bc9095c9dd64b89b9b43e220b04 \ + --hash=sha256:ed9de1b9b042f76fe5c312e4359eab58bf52ac7dfdf6887368a760410d809440 \ + --hash=sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d \ + --hash=sha256:f3aee3adf16272bca81c5826eed55ba3c938add6d8c9e273f01c6b829ecfde22 \ + --hash=sha256:fb8291691ba7ff4e6e000224cc97a744a8d9588418535c9454fd8436911df612 \ + --hash=sha256:fe5424b38f61709b046df2ba7a6d7b54272ee1e2a2c56772d9cd309e4c1ea5ef \ + --hash=sha256:fece5229fd5ecdcd7a0738affb8c59890e13073ae5626544e13825f26c019d3c \ + --hash=sha256:ff036bbc1464993552fd1251b0a83fe102af334b301e3896d7aa05a4999ad042 + # via + # preshed + # spacy + # thinc +cython==3.1.8 \ + --hash=sha256:0274d3c2f95f79784bccd5454ab9dd380aa5ce237e1dc685cc1ce149ac2ffbc2 \ + --hash=sha256:03959ed8996e16550267469187442f3926ea3ba4bb220a06714e6203e4cb6017 \ + --hash=sha256:062f1179af6c5eba92be6eef52baf46eb9cf0677a53aa6bf96ba7861482947a8 \ + --hash=sha256:08ba4010e2a96112c1cc3063c0f96246c33fdaeb937572a675229204745207ad \ + --hash=sha256:0aa5443cc8bfc937369871fbba59990b36cd9af73ed70db9052ac980d44492e4 \ + --hash=sha256:0bc71b05497608c1f8dc99340055c0456f712ce8a1cf35391c9134bbd037f7c6 \ + --hash=sha256:1be05353f84e6a32cbae5869da1af72d356cc73a6dfa521423543688f6b59d67 \ + --hash=sha256:1cbb54f823c6f06d05ec891de0b09ec99cf4fe64365c2b54ce2c918f7d209a4f \ + --hash=sha256:1d04bd43b80ed9ab6228193f24f287fe98b839270625051f7705cedd4e76ac8e \ + --hash=sha256:282b3c8e6abc3fea421919e862e898ffdd86fc0796009bdb5ffdf8211413219f \ + --hash=sha256:306cec0045c18b23152882a583c6181fa8d0c069a4aabf6e6b12bafb8600eed4 \ + --hash=sha256:34eb6dfaa0786a7477aaac86696f26efc052cda586d4f97a9d1d2c6b2ff6d26d \ + --hash=sha256:354f43c6d0e312f650ed39c683e550f669ed18f49ca0bd5772f207ef31f8980c \ + --hash=sha256:3a9eb5545c9ef0f5f6cfb864f70262e284df270de8cd5d73ee2097af0de432a6 \ + --hash=sha256:3d7df7d45733460167d96d9c093fe0abf5d4898aca8f05b7e4cd397e9c8f1d2e \ + --hash=sha256:45a5e806fbfb37c603c4cf0e60b7cd1165ad66bc3aa206f379606ca34dd93c42 \ + --hash=sha256:4673f213e692b9502de8d25cd8f2d8aa3c0ea096546b5f6f019a7ec55df0a56d \ + --hash=sha256:4fa23531b04d63ba2fac0dbf577323ac5c40358ea3ec6bc2b98445bf418faca7 \ + --hash=sha256:50ec9dac15bd8bdb276fd38a146c5294184330551bc1fb053e7e04ff66b7c756 \ + --hash=sha256:518475fbe633ba30c1577669f417440f41e1a23ad6b6e2dbb20703cd457f04ef \ + --hash=sha256:53e5bbf18c274d99dec3e75e1ef19c0add393c8fa14e3c5f1f7ed7fb88e9621d \ + --hash=sha256:554ce2d8243e9330c21ed6e8a5472f1fa812afb99794581c4276b88bd523af5d \ + --hash=sha256:58e21933b3b568817a2896767577ff8a31a246f5c6a5589c73469d83f2bcbfd5 \ + --hash=sha256:5e10d15d2e7f245e61dec89c27da377c2136c08efb6cc2c50baea25b39aab3f9 \ + --hash=sha256:6221417b160e9172aae69b0fd397e02528a6076e1fec642a181aa11c64e32cc9 \ + --hash=sha256:658b8d9f7261a79aa66cf2a5eb0fd7a9b8e196f21671bfc945765a615205d775 \ + --hash=sha256:65d0cf78a14616cb05ff21f63b47bf369e23f353acd51420526c5eb3029f682a \ + --hash=sha256:6ee0772182f6c943d8224a043f00da2c1a8f62cae7de13d02044ef7865035745 \ + --hash=sha256:70ac13ab4bf7e9c1285172d36f9bb06644bb84dc6d4fc0e156ffb8d04b9df86a \ + --hash=sha256:791414cca4dc04eb95b928da88154d8bf5164f85a645816291e2f549c5e8b935 \ + --hash=sha256:7cca0af72f79866994aa7e79ee72d11b8618313c2a4f9405a6943b3e1186952e \ + --hash=sha256:7eb8448eb1c3632c4684ee98db935cf7d3b54fb528a7a455841789316667cd88 \ + --hash=sha256:850503d88092af519662cf66aef26f95db677bdbba55a482ae8658c5b3ac6ad4 \ + --hash=sha256:85c39a8005f314ee90ab271d2b68b6725f94fc08c4e6728417b64704a7ebce04 \ + --hash=sha256:8d10dc3b84b1fe6a7edd43c5bb09cbbf98a7e14f235972e895d029dfcb0a9ce0 \ + --hash=sha256:8f827bf462359eec838e4462c1d819e0008e82dc53df107be13e7617bc96821d \ + --hash=sha256:92a8df673744117afe2a4cb979887ca9440f9e59cbb2e9d0753db2194e2a5080 \ + --hash=sha256:98f45565771122c3f1604b057a5c506a7e682f99ec9d44197cb2990117ff4869 \ + --hash=sha256:9f9af578c0a3ae0a10a8cf41805d0dbec173f909a297368f30090a55a5a9cbe0 \ + --hash=sha256:a9288fa02135743719942deaeb44976399a5785a59f076221ef7e9b22cd0d571 \ + --hash=sha256:b008f7802789bafcdd61de66e11697ee6807839154fc1bf72d81705500997551 \ + --hash=sha256:b690edfa6b333f6139e8ecaa5af8fe87ea591f04d2f2b6a492ed4f2896d818c4 \ + --hash=sha256:bbd835515316a0d014ce27256fb7ceda052d14ac32c242d3f1d6d41734ba3c28 \ + --hash=sha256:bc4fdb56b64f035025bf6958cbd22a61fce1668a2b19ec47aa418df5e9b6b61d \ + --hash=sha256:c27c5c5d675ecabfce60f27a24024df20472b90735d57ce3b869685606b31617 \ + --hash=sha256:c6b74c42bf6b2381d9fdb65f7eda44bcc5c29e4e96c677167495d2cd882d7a3c \ + --hash=sha256:c8803c6b42d482f894cd2ff50f334cc27f70029a9177f7183cac1e5da3978edd \ + --hash=sha256:cae61e8e730bfc7de917204c8b7c1ca8e3e0d134dcaa55a2e2ef98de741590fb \ + --hash=sha256:d114a15b1660ffc8456baf6bfff836fbe2f9c523954de6913040cf6cea1b2349 \ + --hash=sha256:d1bcbed7578f1986a61471476ea5deb0806591aa477230954dd9898a25d0f4e2 \ + --hash=sha256:d249b8ba93cdc0172de325a48f122feec73c03f31d88a738995499a2153f98c4 \ + --hash=sha256:d379eb20a48186573c1371b9f7ae45486f3cc378c3b8f5a38e005cb907596ef2 \ + --hash=sha256:d9029c0eb895a56746ca0d4b3a6a0a57e4c90f2599204e99f7033818ea1c3022 \ + --hash=sha256:d973d25a767abcf977737221dc11befda98daf2573e8bc3c2e6995479505f92a \ + --hash=sha256:d9cd8d1194ab8f52898be10d252ff17360e0af0e2a6662a8d9688fdd8e4cdd7e \ + --hash=sha256:ddab120e53aab67c098416ac5a1467a01f03360aa75eb21a0064b85e9d621be7 \ + --hash=sha256:ddc98eea4a396e903c47ecb8a9defa39b951dd8ccae40e425fe32ec953e228d8 \ + --hash=sha256:de5613a09f3cf056561528a0146aa3529920ce711728fb28d5399180c52ec520 \ + --hash=sha256:e04bb68f932853a38bab7f7c024088a9f6510ccb45ad9a12cca6ebb09ec7a25f \ + --hash=sha256:e153a5aa8f96b79f6e4b9a3d221d4f38b0f7f00a1462dec01478ab99075abd4a \ + --hash=sha256:e1715bf5838c43ef3abfc41d6552a1a5d4bc4d7b041a5d6833080d6b514cc590 \ + --hash=sha256:e26239169b0332eda9a00de22db1fb9d5eb51b67f0af6f19a45205bd119a2e12 \ + --hash=sha256:e3d59036be47961fba075067847abd16c82f5a45ea96ba709b1c485910c20e2e \ + --hash=sha256:e6f3a46fdc5b6da9481b81da6e0f8f3c04cb161baf6956e185a4f242fa1242e2 \ + --hash=sha256:ec653d499d02ada3d150894e840018a9fc82cc5f8b9ac3af00066a8c1f76e0bd \ + --hash=sha256:ef3000be84941e5b597579d3f753460d1e058c32a8697041f78a827797037280 \ + --hash=sha256:f36368c1d7ea4003c52e9e93f3556074a631a6482f8a32e781cb073e211e4df9 \ + --hash=sha256:f519dd0b076bc78edf3b7ab0722638b42d5ab43323654fbfe2fbf63216a3e854 \ + --hash=sha256:f6afffe05b2db113f6502c5b652e0ad5158584e185a51538beeffa8771bdb94d \ + --hash=sha256:f7c3678b89689a367c19fc7c09220a077c6c31c710728ab1aaf4b114c830ee15 \ + --hash=sha256:fc28b2933f2c3c897e1a27d0dee909d9731fa1d19fda3f1d8908dddf31825d2f \ + --hash=sha256:fc3cbaee08d424d2852c7b134ca432501792f67021b73a029cf2f13fb6172423 + # via + # pyfastner + # pyrush + # quicksectx +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via nltk +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via requests +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via spacy +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 + # via nltk +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via medspacy +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d + # via jsonschema +langcodes==3.5.1 \ + --hash=sha256:40bff315e01b01d11c2ae3928dd4f5cbd74dd38f9bd912c12b9a3606c143f731 \ + --hash=sha256:b6a9c25c603804e2d169165091d0cdb23934610524a21d226e4f463e8e958a72 + # via spacy +loguru==0.7.3 \ + --hash=sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6 \ + --hash=sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c + # via pyrush +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +medspacy==1.3.1 \ + --hash=sha256:976599d9446e98018b311951cfa926be5a59d0c140967b828fe98bc4bada40fe + # via -r worker/python/requirements.in +medspacy-quickumls==3.2 \ + --hash=sha256:eeea4c94f4e46529470b9de894bac3384fe883f843d321cd578a62f880439165 + # via medspacy +medspacy-unqlite==0.9.8 \ + --hash=sha256:08b82694bf9546c327bc8050e563a179ef840c5d421f3aa3fe2498e2ea1e560d \ + --hash=sha256:0fba020a03390c864859e470d3d61208177f4c251f98376e15e7f65324a42b0b \ + --hash=sha256:13f8b55b4f5150dcd765779abd54b3c8a6f38ef57494cecf1eaf19ac546c902b \ + --hash=sha256:191c4fa809c01748770f458c6d2dc710bca39510ed1ebbda0699a38c7593798f \ + --hash=sha256:221ffffe5a4242996a8593cd8c4eff7c1c040eac8c9eeb356af61be8ce18a1bc \ + --hash=sha256:2a00a3db96391d0b18273be99c19d20f85e845dceeb5895b77adb074a8e6e912 \ + --hash=sha256:2b5d1dc4985c5b920c6a07628f7aaebc5967692f6eb03037c28f2f282990fef0 \ + --hash=sha256:38d618f7f075b8341751b2985e8283dee9c3cc97000e1f61479343324fcadc79 \ + --hash=sha256:39a5bda42ea1930e5153458739db545b61a0517487fc949f4855b0bff2f8168a \ + --hash=sha256:3dbb26ca6219dcb57dfe518395009c1ce75d43202e3cbd725bfa9c5c071b8857 \ + --hash=sha256:3de98be2084025e94fa0a42ee789a5acf1739d695eed20d3a23c425513a29544 \ + --hash=sha256:41c69c542d15ec650e219e1cb6033deb5f466160b95b6ff6b66feefab77069f0 \ + --hash=sha256:48296849841b7b4127fed3b13af1fd1a01c525f896027b1135cdd6c9965f7d5d \ + --hash=sha256:4a51ab7c6bbe08b8b18ff3f6695ad391ecf29c2f1cbf7b615e3a85ade2158c5f \ + --hash=sha256:52aa29e054335a1eb0facd13bfc32cca45131b3329ac5a6ea23660a13eca03b5 \ + --hash=sha256:552799aebf8f29bbb7735214fc9990c9ab2d2bc08f189c6e2500abb3491c438c \ + --hash=sha256:5d5564246ebe17ad16a4432b2e8bb8fa562fee481d8bed291e205d18e870ba7d \ + --hash=sha256:5fb8d73de8a502833190f9f7f23482117fed28d79103f84dd12a92800078173f \ + --hash=sha256:8bcd311b7038346d8803a1d9c50823ef3c99713ffb0a18f22d17ba6c9563a2eb \ + --hash=sha256:91bc3281c23ed31a1a691b4f26002ea7d42c1e9d8fa9dbeee19a57324be83205 \ + --hash=sha256:9223885a6dd32e5861a5c2be87639d58b2b94724c8d11b387e90ac51db093355 \ + --hash=sha256:96424f7accbe217b00ab484dd49f234ad45d835f2dea529004c8c9523a9ae511 \ + --hash=sha256:983255159f73b33e0560e29eb5d17d3e278768dc2b8d1c6d7a35004ce1047236 \ + --hash=sha256:9b8dbae243f0e0eb80957420ea111cea29fcc5949da54c5f54294468e8d82ea2 \ + --hash=sha256:a86bbdb5979fec09a59a03880bd7c181c33fe713a1d3805bbecdb03a707643d3 \ + --hash=sha256:a8e2aa2b65dbad275f458cac530065dba29898e2fc6319da3999f31c0c396c52 \ + --hash=sha256:d305f8d9ba4d58fdfe55849622386196eb9c14e1ef6896dd36d956ef9ebe3311 \ + --hash=sha256:d6f80f59ae4d7959ad38b3ecc0f409ba59bb29fd18e11f5322e9c01e73b331c1 \ + --hash=sha256:d79203c19c8ac92bd7c15d2aef56d112f590b88be2357ceb97f27a648828e712 \ + --hash=sha256:d946ec243a44df15908df0e7b01d86aaa08192d04e9037b0d847c98cc0037744 \ + --hash=sha256:db392053fcf3ce76beb89982eac078837dd486247770e350b2dd759b986ff45e \ + --hash=sha256:dc57ebf2107f233cea5077ab4acbbdd1be52ce90e4b330289a7935775932424a \ + --hash=sha256:e8bd0e66cff1b3b82c5b62ff4047b1a0c3627af0bc4a10e608512dc0f16209ac \ + --hash=sha256:f0d1c8700ab0bb82321d491d3f581078c28ccfc8497db3c2b6c387d7a9204898 \ + --hash=sha256:f764d5fc7e6f55890f871f34460abc85d2252be4b8c1e145d6d8b915e2386fbc \ + --hash=sha256:f7e49c7b0888bbfce6a4f1536397b23b0acec5c9987b232d5723c76446ab7454 \ + --hash=sha256:f814537abd391fe5d057738c96192dd0dbe4cc61135db939112251ed92bdb8ed + # via medspacy-quickumls +murmurhash==1.0.15 \ + --hash=sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d \ + --hash=sha256:1349a7c23f6092e7998ddc5bd28546cc31a595afc61e9fdb3afc423feec3d7ad \ + --hash=sha256:189a8de4d657b5da9efd66601b0636330b08262b3a55431f2379097c986995d0 \ + --hash=sha256:213d710fb6f4ef3bc11abbfad0fa94a75ffb675b7dc158c123471e5de869f9af \ + --hash=sha256:2224f30f7729717644745a6f513ea7662517dfe7b1867cf1588177f64c61df3c \ + --hash=sha256:22aa3ceaedd2e57078b491ed08852d512b84ff4ff9bb2ff3f9bf0eec7f214c9e \ + --hash=sha256:231dc7982e1aeae8bbab21f8f21953e3b9fb0a34e3ffe2d2342ff32e4f07af9d \ + --hash=sha256:263807eca40d08c7b702413e45cca75ecb5883aa337237dc5addb660f1483378 \ + --hash=sha256:2680851af6901dbe66cc4aa7ef8e263de47e6e1b425ae324caa571bdf18f8d58 \ + --hash=sha256:26fd7c7855ac4850ad8737991d7b0e3e501df93ebaf0cf45aa5954303085fdba \ + --hash=sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400 \ + --hash=sha256:342277d8d7f712d136507fb3ccdba26c076a34ca0f8d1b96f65f0daa556da2e9 \ + --hash=sha256:34e5a91139c40b10f98d0b297907f5d5267b4b1b2e5dd2eb74a021824f751b98 \ + --hash=sha256:3c69b4d3bcd6233782a78907fe10b9b7a796bdc5d28060cf097d067bec280a5d \ + --hash=sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40 \ + --hash=sha256:43cc6ac3b91ca0f7a5ae9c063ba4d6c26972c97fd7c25280ecc666413e4c5535 \ + --hash=sha256:44d211bcc3ec203c47dac06f48ee871093fcbdffa6652a6cc5ea7180306680a8 \ + --hash=sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda \ + --hash=sha256:4fd8189ee293a09f30f4931408f40c28ccd42d9de4f66595f8814879339378bc \ + --hash=sha256:539d8405885d1d19c005f3a2313b47e8e54b0ee89915eb8dfbb430b194328e6c \ + --hash=sha256:55e1a7f65095f0af141c8a460765e026e35d9ec526f0daa2f88fed29a292b2ff \ + --hash=sha256:5678a3ea4fbf0cbaaca2bed9b445f556f294d5f799c67185d05ffcb221a77faf \ + --hash=sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01 \ + --hash=sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616 \ + --hash=sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26 \ + --hash=sha256:66395b1388f7daa5103db92debe06842ae3be4c0749ef6db68b444518666cdcc \ + --hash=sha256:671979f15b24817968ff6fab5e3a468e2b05555bac5e92f11155610a37165ffc \ + --hash=sha256:694fd42a74b7ce257169d14c24aa616aa6cd4ccf8abe50eca0557e08da99d055 \ + --hash=sha256:6cb4e962ec4f928b30c271b2d84e6707eff6d942552765b663743cfa618b294b \ + --hash=sha256:7c4280136b738e85ff76b4bdc4341d0b867ee753e73fd8b6994288080c040d0b \ + --hash=sha256:8155d106c63c5a509ec58c19b0c30804f3a0931bc65cca9826efc53373332536 \ + --hash=sha256:847d712136cb462f0e4bd6229ee2d9eb996d8854eb8312dff3d20c8f5181fda5 \ + --hash=sha256:88dc1dd53b7b37c0df1b8b6bce190c12763014492f0269ff7620dc6027f470f4 \ + --hash=sha256:898a629bf111f1aeba4437e533b5b836c0a9d2dd12d6880a9c75f6ca13e30e22 \ + --hash=sha256:899068ba3d7c371e7edd093852c634cce802fefd9aaddfcc0d2fda1d7433c7f9 \ + --hash=sha256:8a181494b5f03ba831f9a13f2de3aab9ef591e508e57239043d65c5c592f5837 \ + --hash=sha256:95d7c52598dce7a8543e5a5f61a893cbb762a62a907c6c9cd025d5f618bb8522 \ + --hash=sha256:9aba94c5d841e1904cd110e94ceb7f49cfb60a874bbfb27e0373622998fb7c7c \ + --hash=sha256:a2ea4546ba426390beff3cd10db8f0152fdc9072c4f2583ec7d8aa9f3e4ac070 \ + --hash=sha256:a32054edb567417ac81f7172b7dd7731846f25c63084edeb20545fa7abc849d7 \ + --hash=sha256:aadac5fd5f3f465094a5e4ecd00d739a2b870651cad06c8d1005153c2e815fb3 \ + --hash=sha256:b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f \ + --hash=sha256:b65a5c4e7f5d71f7ccac2d2b60bdf7092d7976270878cfec59d5a66a533db823 \ + --hash=sha256:bba0e0262c0d08682b028cb963ac477bd9839029486fa1333fc5c01fb6072749 \ + --hash=sha256:bc54facccb32fe1e97d6231edd4f3e2937467c35658b26aa35bbd6a87ebb7cb0 \ + --hash=sha256:c22e56c6a0b70598a66e456de5272f76088bc623688da84ef403148a6d41851d \ + --hash=sha256:c4cd739a00f5a4602201b74568ddabae46ec304719d9be752fd8f534a9464b5e \ + --hash=sha256:cb8ebafae60d5f892acff533cc599a359954d8c016a829514cb3f6e9ee10f322 \ + --hash=sha256:cc93769619b6b42740cab8eebb587709daaa3d8f813372cc60358a571de20d2d \ + --hash=sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb \ + --hash=sha256:d4d681f474830489e2ec1d912095cfff027fbaf2baa5414c7e9d25b89f0fab68 \ + --hash=sha256:d7e47c5746785db6a43b65fac47b9e63dd71dfbd89a8c92693425b9715e68c6e \ + --hash=sha256:dc35606868a5961cf42e79314ca0bddf5a400ce377b14d83192057928d6252ec \ + --hash=sha256:e43a69496342ce530bdd670264cb7c8f45490b296e4764c837ce577e3c7ebd53 \ + --hash=sha256:e525bbd8e26e6b9ab1b56758a59b16c2fffd73bad2f7b8bf361c16f70ff1d980 \ + --hash=sha256:e8e674f02a99828c8a671ba99cd03299381b2f0744e6f25c29cadfc6151dc724 \ + --hash=sha256:ef19f38c6b858eef83caf710773db98c8f7eb2193b4c324650c74f3d8ba299e0 \ + --hash=sha256:f32307fb9347680bb4fe1cbef6362fb39bd994f1b59abd8c09ca174e44199081 \ + --hash=sha256:f3e99a6ee36ef5372df5f138e3d9c801420776d3641a34a49e5c2555f44edba7 \ + --hash=sha256:f4989c16053a9a83b02c520dd00a31f0877d5fd2ab8a9b6b75ed9eba0e25c489 \ + --hash=sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca \ + --hash=sha256:f9bf47101354fb1dc4b2e313192566f04ba295c28a37e2f71c692759acc1ba3c \ + --hash=sha256:fa1b70b3cc2801ab44179c65827bbd12009c68b34e9d9ce7125b6a0bd35af63c \ + --hash=sha256:fe50dc70e52786759358fd1471e309b94dddfffb9320d9dfea233c7684c894ba \ + --hash=sha256:fe883982114de576c793fd1cf55945c8ee6453ad4c4785ac1a48f84e74fdc650 + # via + # preshed + # spacy + # thinc +nltk==3.10.1 \ + --hash=sha256:55b8780b6b97732c1c3806d4ae02d46113204b11bfdc19dddb95729f627f8853 \ + --hash=sha256:86a1b41d9ca0d35a2cb72fa60af4c9aaba9fe405b717161fd94cecd69f467007 + # via medspacy-quickumls +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via + # blis + # medspacy-quickumls + # spacy + # thinc +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via + # pytesseract + # pytest + # spacy + # thinc + # weasel +pillow==12.3.0 \ + --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ + --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ + --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ + --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ + --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ + --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ + --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ + --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ + --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ + --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ + --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ + --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ + --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ + --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ + --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ + --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ + --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ + --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ + --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ + --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ + --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ + --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ + --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ + --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ + --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ + --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ + --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ + --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ + --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ + --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ + --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ + --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ + --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ + --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ + --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ + --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ + --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ + --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ + --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ + --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ + --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ + --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ + --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ + --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ + --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ + --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ + --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ + --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ + --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ + --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ + --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ + --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ + --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ + --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ + --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ + --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ + --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ + --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ + --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ + --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ + --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ + --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ + --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ + --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ + --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ + --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ + --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ + --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ + --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ + --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ + --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ + --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ + --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ + --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ + --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ + --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ + --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ + --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ + --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ + --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ + --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ + --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ + --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ + --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ + --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ + --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ + --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 + # via + # -r worker/python/requirements.in + # pytesseract +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +preshed==3.0.13 \ + --hash=sha256:04d8f13f2986e5d11af5ac51f55ce3106c70c41b483d20ea392e6180bdd0f870 \ + --hash=sha256:09397592d333a77f88454e72b7f1f941b2afaf040b392b9e74898dbc4648cdf5 \ + --hash=sha256:09f96b477c987755b3c945df214ea1c1c80bfb350e9f34e78da89585535b77e8 \ + --hash=sha256:0e5b2865aecbd2e1e10e5d19bb8bfad765863c1307c6c3e51f2a08bd64122409 \ + --hash=sha256:183b339956a9e1d7a4a00038a3b9587a734db9e8bd915939a49791bd1b372156 \ + --hash=sha256:19318dc1cd8cac6663c6c830bf7e0002d2de853769fb03e056774e97c21bedfd \ + --hash=sha256:208dcebbe294bf1881ce33fb015d56ab2a7587aece85a09147727174207892e4 \ + --hash=sha256:2b704e46cb7b88f656ef16a3e5347b36525a1c53721d327a4ba1457404101f85 \ + --hash=sha256:2e77bed56aded7cbe5d28d6bd2178bc5b13eda0e0e464dab205fb578fa915000 \ + --hash=sha256:35d6c5acb3ee3b12b87a551913063f0cec784055c2af16e028c19fe875f079d0 \ + --hash=sha256:3e3528f6628329349e281b607aad746ae3c06c15ba59fd5b6599c7c2fd77911b \ + --hash=sha256:40e9445911051bc67cf84ba12745e3be4d010aaf4f8ade3ba3def82fdb45d18e \ + --hash=sha256:42c58b07e8b431e33d0ad9922e896632453821cad8b09171b619b8c61101916f \ + --hash=sha256:461327f8dd36520dcf1fd55a671e0c3c2c97a2d95e22fc85faa31173f4785dda \ + --hash=sha256:4a7bc48220de579be6bdb0a8715482cf36e2a625a6fd5ad26c9f43485a4a23b5 \ + --hash=sha256:4e9ae86c982e49f58620d45eeb51e073e50feb88f52ac10a95fe117c1d222a84 \ + --hash=sha256:4f8856ca3d88e9b250630d70abb4f260d8933151ddfb413024784b25b009868e \ + --hash=sha256:502f93f49a22788203f02d3067d4ea077a0cca3864de6a792eae12e7ce589e14 \ + --hash=sha256:5268c0e6fa96f50cdf87f516c2d4b32563c12706ee768e75c00e8d0098acd545 \ + --hash=sha256:5d14eea14bd01291388928991d7df7d60b9fd19ae970e55006eb4d29b0c1e8eb \ + --hash=sha256:5e2753779832e411e93eb727f3d409c0a6b7408e5ce4dd868076d8ece48c7693 \ + --hash=sha256:62cf7f3113132891d6bba70ff547ad81c6fe50a31930bbbb8499f1d47cd122b7 \ + --hash=sha256:670db59a52e1823b5f088c764df474e65b686592d4093adbeef14581c95ee2cb \ + --hash=sha256:70d502e081348df207d90f347f21770ed596822bb04eb3c3b32b7281579e90c6 \ + --hash=sha256:7557963d0125a3a7bcdb2eb6948f3e45da31b5a7f066b55320de3dea22d7557f \ + --hash=sha256:7770987c2e57497cd26124a9be5f652b5b3ccd0def89859ab0da8bca6144a3de \ + --hash=sha256:7c333f18e9a81c8a6de0603fd8781e17115324b117c445ca91abdf7bfb1abe49 \ + --hash=sha256:7da9d931e7660dcdd757e5870269f0c159126d682ed73ed313971d199eb0f334 \ + --hash=sha256:867aa73abbf4ee3b4d7662148091c33a8c039271269e3a7f1e0ca995f91995c8 \ + --hash=sha256:8b82d7a7bb63d248a6cbbfcabb4a570c993d54d964e39dc5d85c14018ba2079e \ + --hash=sha256:8b8de3f58043070a354477995acdd98626ce43e4193c708ebd0f694e467f5155 \ + --hash=sha256:8d6acc1f5031a535a55a6f7148e2f274554a8343a16309c700cebea0fe7aee8c \ + --hash=sha256:985cb9b097beda76cd13c01a0499707103e8915f888fa30f8aa8324ef2cc6b08 \ + --hash=sha256:9ca43ecbc3783eda4d6ab3416ae2ecd9ef23dca5f53995843f69f7457bcd0677 \ + --hash=sha256:a06e27f4e5b9d7943840087828c6a0dae4a3475576d12c2e95b71abbb325a80b \ + --hash=sha256:a3ac301b065e67e9541f8e3ab3f67533e53deb57c2d258395c5bb98f9723f99b \ + --hash=sha256:a8682988e47739adba369bf43789fc870b554a21e2d1f30a3d17ed336d05f451 \ + --hash=sha256:acd4d89abeca3678c5d8c89b3cd351314465bc67c7fa053d2644f8513e543386 \ + --hash=sha256:b03e21b0bf95eb56e23973f32cabb930e94f352228652f81c0955dbd6967d904 \ + --hash=sha256:b980f3ea9bb74b7f94464bc3d6eb3c9162b6b79b531febd14c6465c24344d2cc \ + --hash=sha256:bef84b225d226af43adfee78ce5ddede72a6155ce5292c1a41dcd1f0b9c87c30 \ + --hash=sha256:c046736239cc8d72670749b79b526e4111839a2fc461a58545d212797649129c \ + --hash=sha256:c0d0c14187dc0078d8a63bf190ec045a4d13e7748b6caeb557a7d575e411410b \ + --hash=sha256:c4bc60dc994864095d784b7e4d77dba3e64188d169ac88722b699d175561fddb \ + --hash=sha256:c8596e41a258ff213553a441e0bb3eb388fd8158e84a7bf3aae6d8ede2c166d3 \ + --hash=sha256:cf8e1a7a1823b2a7765121446c630140ac6e8650c07a6efbf375e168d1fef4f7 \ + --hash=sha256:d0e114300e5577e806c17fb1cc9f07bc6584188d84545401f66e690c8315feef \ + --hash=sha256:d2f1efae396cadab5f3890a2fd43d2ee65373ef9096ccbb805e51e8d8bcc563b \ + --hash=sha256:d4ae5cfe075bb7a07982e382bca44f41ddf041f4d24cbd358e8cccfc049259b8 \ + --hash=sha256:d75f718bbfd97e992f7827e0fa7faf6a91bdd9c922d5baa4b50d62731396cb89 \ + --hash=sha256:dbd7c735a613857ae39ac23bf4690b0d92adc30add977828529b50ba09e33fbc \ + --hash=sha256:de87fbabb0f37c3c92d4dd9b94fc82ab73cdab4247cdfbd57ab3926caa983919 \ + --hash=sha256:df642547a1a94079978a0ea8f4593ab4b8d3bd43f767bef0ef64d9a214f8c4c9 \ + --hash=sha256:e1ab099b2f5843b19e875502b64001b0705e375fb5bd1ca6240aa14e4ffc31e4 \ + --hash=sha256:e5c8462472f790c16708306aef3a102a762bd19dfe3d2f8ee08bd5e12f51b835 \ + --hash=sha256:f05b08ce92399c0655b5e0eb5a1cc1f9e295703ed3aabdfaf6538dfa8ae23d57 \ + --hash=sha256:f8e6fe0620ed0f96a246d46447055c447e071cd8222731a045c235e8a758c918 + # via + # spacy + # thinc +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 + # via + # confection + # spacy + # thinc + # weasel +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae + # via pydantic +pyfastner==1.0.10 \ + --hash=sha256:259665dd8a84c907559645d2ce33ec807be0fb03664cfdb14a50aa6436cedb72 \ + --hash=sha256:d477fc4f7acbb68f0c828b0bed478206092bb8190997dfda006993db5813b262 + # via pyrush +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via + # pytest + # rich +pymupdf==1.28.0 \ + --hash=sha256:44f0973f5e5edbaec95bc34b64e71d1959d4ee90b1328de1b4f4f5b4fa78673f \ + --hash=sha256:47a5c29ed4eb0744de9c4e37bb49b1259b18d4d75fcc8a7c130f7c9fa15956f6 \ + --hash=sha256:4d61ec323a706e153a12e262e51febfb43eeaa20977785ace135d18d48bcdc83 \ + --hash=sha256:4d692dcf44d3566ae96bc6f6346c6ad432274a29ba617bf7a9fe18009e24adb4 \ + --hash=sha256:74c6d00ba2a9aad3a635db73b07c15db462b480741d831a34a75a56535ebc22b \ + --hash=sha256:892b89ba88e8f98b53133b62877a9dc9b5e7dc6a4aeb837b612db56a8d2e03ac \ + --hash=sha256:b3e1399c7a64c6914239116a369efcdaac4cfb9e838bde2656d7accc4a85c72d \ + --hash=sha256:caea2b3b67347fd79e5d15ed7929b0e886aac594ea228073b6d39de0078189da \ + --hash=sha256:e01e90fd86abfeb37ceb921eddb951f988a11d45ff6ce6b7664f2039849068ec \ + --hash=sha256:e53f3567403a92da15caa9e7ae0164327fff48817e9f40175367fb9de524258d + # via -r worker/python/requirements.in +pyrush==1.0.13 \ + --hash=sha256:03ceb9ef0f48e6e4479734b20c42c844348bcd62c9df8567e6b1ea499c527f9b \ + --hash=sha256:27fe94880b7fc1a5a86696d36bb8e72dc04c1445807f8f415d9f31291a0708e7 \ + --hash=sha256:5886dde7a7be2bb1e69c9771462c83d10f29aa8ff613af9d034e4fd07dcb2a0d \ + --hash=sha256:6419e261972917834e9a111e6c5ec1b3569023819c11b19aad8c266bda881c4a \ + --hash=sha256:64812dee07ac34e9f3b8ddf1a997bcbb6f9b4196703d11a6528e3111e3f97493 \ + --hash=sha256:6d5ee8cd1c73270d62dfd7df14539cc77070427248c9b3daa2aae3f2c5dc1051 \ + --hash=sha256:6fa2d79f998d78ee01d057e336d526ad19920914efde655638a241a1f78ee5d6 \ + --hash=sha256:79a070b691fe6a896df16c5218a31c016c49838fdf8d925b9ceec1137afbf7ce \ + --hash=sha256:7da554d6c50b39b3679f67f59ebedb70c043849ec934c88bb1b0a29c9c99e54e \ + --hash=sha256:9b7ab908ee08a48e60b9466953c77a158366e2cad5f89407f70a7a7efa8e58f7 \ + --hash=sha256:9bbd9d6b1626c478bbb07566297367fbd8a92f0f61ffe8e554f5afc9370353bb \ + --hash=sha256:a6a3d586d07bd6636fbebfcbb8ee40c0ec2808ee6b7ed9e1a17b47629d78d750 \ + --hash=sha256:c941738f66052eb0eb9b47f32534cfe7b1f59ed6866bf3f89dbeb96d18a871a5 \ + --hash=sha256:e338921588f763ec447972856086083ba0ce490ad2ac2de92a61610289856da2 \ + --hash=sha256:e9c096c2798ddba9454d2aeaebca9cad7c25ebde611aa16442692e62b9e2cc0b \ + --hash=sha256:ed1bf0f502e6f4f5ac4733d7177d02ea068aede6801d311fda033d989160c8b7 \ + --hash=sha256:ef2d05d25c507c2d0f8604e63ab94d9fe79e46c636289a53a4f08c6ebc3326e4 \ + --hash=sha256:f2f691eea8e24dc42188ece2c5028ba02a796fe8d8f466a8cd729139bc799f10 \ + --hash=sha256:f423d47abb182b330acdbed5f9fead440c0deb1ba788c7fa9fd7530a1774cb45 \ + --hash=sha256:f824ea5f1e6528fd4426421bbea773a35b9b9c5bad825cad03fc5bde031f68fa \ + --hash=sha256:ff006e31987eac79c5617afd692e84bbd9d2c39bae055f7b3b1aca21d0022f87 + # via medspacy +pysbd==0.3.4 \ + --hash=sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953 + # via medspacy +pysimstring==1.3.0 \ + --hash=sha256:1b4b48a869a0d78f42e5a4ca401a46080c9e34f1a18aee3ac4970885a4352850 \ + --hash=sha256:1c6e5413bd7bc1f963a295ce577ea08a7876db5b1f5bd5e4ddeb5029bc73bf03 \ + --hash=sha256:332920eda9c09a6622234829516988439e12b4b0b7903832d53ace6e611dc286 \ + --hash=sha256:334003549c0c6437da48f2175e9f1bede72c292558918e201dbffab4c5dfd56b \ + --hash=sha256:357548aead40c5b5966eed0758e9269959da91f5a03468cc4ad3b4418c0b6a3f \ + --hash=sha256:37c68e8e095fc64811d343b7141665de570fb9d3b87d875370db473f528eea32 \ + --hash=sha256:3a5f236dba9960f9ae485c96e41be4e33976f164ce05ad2b918099daf752b465 \ + --hash=sha256:3b4b27bbaac708198e0b2ae70c3ddeb2e5b0ea75aaa4a836bc97730242f24a56 \ + --hash=sha256:3ddf76002e15b6de81fd0245f46c4ac73fcdc002b992ff98ee7a57c1e2df2ddb \ + --hash=sha256:4104d7a3081e4334747b912da161645dd004b4173c64d3cfe64f07f3cf96f1ac \ + --hash=sha256:496e98d3cc2f8708ec1bf5037b24080b606d943ae9da783b4158928d0ed65eee \ + --hash=sha256:499d3502f4c3640bcacee947b9947ed5ddefea709405749abe5716737b9af04d \ + --hash=sha256:4c985d6c6eeb474edf098211d331c4de237024337aaa57db16cc7bba1956620d \ + --hash=sha256:574075131b23e23adc73e4e23ecd9006db11ffe194aa576b22b67e09e632a966 \ + --hash=sha256:6a3c0d9ef940dfdf74e45e2f5c8b3f7b76d01c43ca584bdb4012ed5e1e6b6108 \ + --hash=sha256:781df8a12ad74de9a2cdb35b72e69fa8ed8bf3a39022f5de5dcea38e02515ee3 \ + --hash=sha256:7ae1ff135c0ac51c2300a4983b012b63729b1451f2c49cb4b9e9ebf8dea35d63 \ + --hash=sha256:83ac2673d8e3598d7e94765bf9ae5c9c8ce3cd00cf1c65cbb98ecafd73350f03 \ + --hash=sha256:a544c11ad5d42f675cbb8a1c5b25cbdb18376c32aa3c10454e79f6afab1516ab \ + --hash=sha256:b2157464d5f65020b9548e4cd9ec77d4922f14b4c64836512c84e88cfbca85f8 \ + --hash=sha256:b27350103b182b66125c61dbe57659c10f00b348e3182e66c5e36794f10db1ba \ + --hash=sha256:b840f13214ee80982d642f325a0b6ffc6dc328e7810a516ba52febdd55338a5d \ + --hash=sha256:bb3b6428090f698664c3ad82a2707aa9fec4f71a906f38ba68e1f977d77fec26 \ + --hash=sha256:d98a00e1c1d1cc51f8574040243b49654de630f5f89612959928b04955269230 \ + --hash=sha256:da86217a480b3e81628ec62e6ab600891017ca2a233febbffded9bf18e0e4492 \ + --hash=sha256:f32cccda0bcf06d1d0b282388cbeb089175405607e924aaef063ebe5b4a26f6e \ + --hash=sha256:fbfc61fe7321ebe31342d0b0afd0d8167acf0edd01894850b2fcc32beee5bbde \ + --hash=sha256:ff5215f0b27f6c07041c1e8a1a0e795df3dc9e149a7ef0a1418ace65361d2322 + # via medspacy-quickumls +pytesseract==0.3.13 \ + --hash=sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9 \ + --hash=sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34 + # via -r worker/python/requirements.in +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via medspacy-quickumls +quicksectx==0.4.1 \ + --hash=sha256:086e1e4e7541c2a57be23067ddbc3cb13bbd68bc9977dba7bdd4e229fdb8c04f \ + --hash=sha256:21e7123982f6e33f8b25b9f0f102c80f900fba0cebfec4c14a17a81a987eb24c \ + --hash=sha256:2367db2333170159affa50cfb530142c3a010d64cad83e598bdd1fb5b5223229 \ + --hash=sha256:36a2254750555a33281b1ca259e45338cce413a56c6b83dc145b61d50c7cde02 \ + --hash=sha256:4a9eb3fa7b18e9367b617e628806e826363d23915fa2daa9316d7d0080fed369 \ + --hash=sha256:60531878a5d010b2ff67d2ea648fd7e3e70ba81871388e02a588be6e290ba6b7 \ + --hash=sha256:7c9edd08c948bc749634fe9333eae9a820cc38ba4911babc23efbd18f968b62e \ + --hash=sha256:80d7bb802fcb4605bfd82a661856f250c9bbe1941c19f7e4b30ec487126c2659 \ + --hash=sha256:88af6881b9b21aabcf58474f41ff98cf3d60fc4c8c785765330741cfb0132a08 \ + --hash=sha256:91a51ab80a27c65ed80a7e23819566b629249c53eb50c149901fcdf3362fe1ca \ + --hash=sha256:98d0e3f1708828d0cefcf3c3a46143b562a35f0bdf8f85610d9599e8dff3a9c0 \ + --hash=sha256:9dc7380b2954a36fde4d5db621c3576deb7b6697b2bd73160c9f4975fc344dae \ + --hash=sha256:b8938929cf893d9d0d6e74feae74539beef55fbf287fea0bc996752cd5fca54f \ + --hash=sha256:d21c7e214337c40551dea0e9f6b942ac5237938234e0b85ce40121dcbd0c8390 \ + --hash=sha256:e9f2bb93adf5c6d39f3d55ca87a4a017bb2823e5a235b27ace0a39b3c381294d \ + --hash=sha256:ed13aadcf0023eef1b91141133d8b57a0aa426ca1f0a4f1ad88d4aa4d41303c5 + # via + # pyfastner + # pyrush +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 + # via + # jsonschema + # jsonschema-specifications +regex==2026.7.19 \ + --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \ + --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \ + --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \ + --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \ + --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \ + --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \ + --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \ + --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \ + --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \ + --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \ + --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \ + --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \ + --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \ + --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \ + --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \ + --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \ + --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \ + --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \ + --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \ + --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \ + --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \ + --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \ + --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \ + --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \ + --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \ + --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \ + --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \ + --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \ + --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \ + --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \ + --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \ + --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \ + --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \ + --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \ + --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \ + --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \ + --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \ + --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \ + --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \ + --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \ + --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \ + --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \ + --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \ + --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \ + --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \ + --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \ + --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \ + --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \ + --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \ + --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \ + --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \ + --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \ + --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \ + --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \ + --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \ + --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \ + --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \ + --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \ + --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \ + --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \ + --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \ + --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \ + --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \ + --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \ + --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \ + --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \ + --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \ + --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \ + --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \ + --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \ + --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \ + --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \ + --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \ + --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \ + --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \ + --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \ + --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \ + --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \ + --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \ + --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \ + --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \ + --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \ + --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \ + --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \ + --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \ + --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \ + --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \ + --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \ + --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \ + --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \ + --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \ + --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \ + --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \ + --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \ + --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \ + --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \ + --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \ + --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \ + --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \ + --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \ + --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \ + --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \ + --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \ + --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \ + --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \ + --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \ + --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \ + --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \ + --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \ + --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \ + --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \ + --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \ + --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \ + --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2 + # via nltk +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # spacy + # weasel +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via typer +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef + # via + # jsonschema + # referencing +shellingham==1.5.4 \ + --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ + --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de + # via typer +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via medspacy-quickumls +smart-open==7.7.1 \ + --hash=sha256:9414ba5733e28309f29b28a303b0f1054ad23fe0275f1a1b600c80a724f4bd1a \ + --hash=sha256:cb62dc45f519bf39b612564d326d2a17556f3a6056e3c3a86a07215d670d45bc + # via weasel +spacy==3.7.5 \ + --hash=sha256:07677e270a6d729453cc04b5e2247a96a86320b8845e6428d9f90f217eff0f56 \ + --hash=sha256:1171bf4d8541c18a83441be01feb6c735ffc02e9308810cd691c8900a6678cd5 \ + --hash=sha256:12fdc01a4391299a47f16915505cc515fd059e71c7239904e216523354eeb9d9 \ + --hash=sha256:190ba0032a5efdb138487c587c0ebb7a98f86adb917f464b252ee8766b8eec4a \ + --hash=sha256:262f8ebb71f7ed5ffe8e4f384b2594b7a296be50241ce9fbd9277b5da2f46f38 \ + --hash=sha256:2a21b2a1e1e5d10d15c6f75990b7341d0fc9b454083dfd4222fdd75b9164831c \ + --hash=sha256:38de1c9bbb73b8cdfea2dd6e57450f093c1a1af47515870c1c8640b85b35ab16 \ + --hash=sha256:3dad4853950a2fe6c7a0bdfd791a762d1f8cedd2915c4ae41b2e0ca3a850eefc \ + --hash=sha256:3e207dda0639818e2ef8f12e3df82a526de118cc09082b0eee3053ebcd9f8332 \ + --hash=sha256:4145cea7f9814fa7d86b2028c2dd83e02f13f80d5ac604a400b2f7d7b26a0e8c \ + --hash=sha256:43acd19efc845e9126b61a05ed7508a0aff509e96e15563f30f810c19e636b7c \ + --hash=sha256:4b0bf1788ca397eef8e67e9c07cfd9287adac438512dd191e6e6ca0f36357201 \ + --hash=sha256:4cfb85309e11a39681c9d4941aebb95c1f5e2e3b77a61a5451e2c3849da4b92e \ + --hash=sha256:4e00d076871af784c2e43185a71ee676b58893853a05c5b81717b8af2b666c07 \ + --hash=sha256:5694dd3b2f6414c18e2a3f31111cd41ffd597e1d614b51c5779f85ff07f08f6c \ + --hash=sha256:591d90d8504e9bd5be5b482be7c6d6a974afbaeb62c3181e966f4e407e0ab300 \ + --hash=sha256:6a7dbfbca42c1c128fefa6832631fe49e11c850e963af99229f14e2d0ae94f34 \ + --hash=sha256:713b56fe008c79df01617f3602a0b7e523292211337eb999bdffb910ea1f4825 \ + --hash=sha256:7f8fbe9f6b9de1bf05d163a9dd88108b8f20b138986e6ed36f960832e3fcab33 \ + --hash=sha256:8002897701429ee2ab5ff6921ae43560f4cd17184cb1e10dad761901c12dcb85 \ + --hash=sha256:8b493a8b79a7f3754102fa5ef7e2615568a390fec7ea20db49af55e5f0841fcf \ + --hash=sha256:a648c6cbf2acc7a55a69ee9e7fa4f22bdf69aa828a587a1bc5cfff08cf3c2dd3 \ + --hash=sha256:bf54c3c2425428b328b53a65913d47eb4cb27a1429aa4e8ed979ffc97d4663e0 \ + --hash=sha256:cd93c34bf2a02bbed7df73d42aed8df5e3eb9688c4ea84ec576f740ba939cce5 \ + --hash=sha256:d211920ff73d68b8febb1d293f10accbd54f2b2228ecd3530548227b750252b1 \ + --hash=sha256:d244d524ab5a33530ac5c50fc92c9a41da6c3980f452048b9fc29e1ff1bdd03e \ + --hash=sha256:d9108f67675fb2078ed77cda61fd4cfc197f9256c28d35cfd946dcb080190ddc \ + --hash=sha256:f044522b1271ea54718dc43b6f593b5dad349cd31b3827764c501529b599e09a \ + --hash=sha256:faa1e2b6234ae33c0b1f8dfa5a8dcb66fb891f19231725dfcff4b2666125c250 \ + --hash=sha256:fdbb667792d6ca93899645774d1db3fccc327088a92072029be1e4bc25d7cf15 + # via + # medspacy + # medspacy-quickumls + # pyrush +spacy-legacy==3.0.12 \ + --hash=sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f \ + --hash=sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774 + # via spacy +spacy-loggers==1.0.5 \ + --hash=sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645 \ + --hash=sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24 + # via spacy +srsly==2.5.3 \ + --hash=sha256:0017c7d2a0cd9a4f1bdc00d946b45edcf90bb0e271e8f084c1ce542bf6708c32 \ + --hash=sha256:06a43d63bde2e8cccadb953d7fff70b18196ca286b65dd2ad16006d65f3f8166 \ + --hash=sha256:07d682679e639eb46ff7e6da4a92714f4d5ffe351d088ee66f221e9b1f8865bb \ + --hash=sha256:08f98dbecbff3a31466c4ae7c833131f59d3655a0ad8ac749e6e2c149e2b0680 \ + --hash=sha256:0f106b0a700ab56e4a7c431b0f1444009ab6cb332edc7bbf6811c2a43f4722cb \ + --hash=sha256:111805927f05f5db440aeeacb85ce43da0b19ce7b2a09567a9ef8d30f3cc4d83 \ + --hash=sha256:14c930767cc169611a2dc14e23bc7638cfb616d6f79029700ade033607343540 \ + --hash=sha256:1a3d6e03c65e3af15bfb1ad18f1888ba0a8482903218c1a5b5ed6fd66f5b0fb1 \ + --hash=sha256:1c9129c4abe31903ff7996904a51afdd5428060de6c3d12af49a4da5e8df2821 \ + --hash=sha256:1d93c22f42dfc4383a89ff3fcbe89ed9b286cf1d7e762cba533f2fac5ed36b28 \ + --hash=sha256:1fd6c35c65c4d2435ae5bfb57b59682cf9b61606318a2a761856be9d7cc2d9e3 \ + --hash=sha256:21cf09e417d3e4f3fbf7dd337fd6d948c97abd01896b9b4cb80e81cd9778a73a \ + --hash=sha256:29d5d01ba4c2e9c01f936e5e6d5babc4a47b38c9cbd6e1ec23f6d5a49df32605 \ + --hash=sha256:2f2d464f0d0237e32fb53f0ec6f05418652c550e772b50e9918e83a1577cba4d \ + --hash=sha256:2f73c0db911552e94fe2016e1759d261d2f47926f68826664cada3723c87006a \ + --hash=sha256:2f76a2507cc2debf0aeb31120c8d04d752eb0ca8bd84599a62461796a3c0f71f \ + --hash=sha256:348c231b4477d8fe86603131d0f166d2feac9c372704dfc4398be71cc5b6fb07 \ + --hash=sha256:3576c125c486ce2958c2047e8858fe3cfc9ea877adfa05203b0986f9badee355 \ + --hash=sha256:39c13d552a9f9674a12cdcdc66b0c2f02f3430d0cd04c5f9cf598824c2bd3d65 \ + --hash=sha256:4b1b721cd3ad1a9b2343519aadc786a4d09d5c0666962d49852eb12d6ec3fe26 \ + --hash=sha256:4ca4a068f6e14d84113a02fcb875c6b50a6285a12938c0e7a157eb3a63c50a86 \ + --hash=sha256:4d6ebaeac9baa5c85b6b56c180458f1b4fef9213bc36b62fcbecf5b3d8a3001c \ + --hash=sha256:565f69083d33cb329cfc74317da937fb3270c0f40fabc1b4488702d8074b4a3e \ + --hash=sha256:598f1e494c18cacb978299d77125415a586417081959f8ec3f068b32d97f8933 \ + --hash=sha256:5c1ac27ae5f4bb9163c7d2c45fc8ec173aac3d92e32086d9472b326c5c6e570e \ + --hash=sha256:5c8df4039426d99f0148b5743542842ab96b82daded0b342555e15a639927757 \ + --hash=sha256:5f6a837954429ecbe6dcdd27390d2fb4c7d01a3f99c9ffcf9ce66b2a6dd1b738 \ + --hash=sha256:5fb59c42922e095d1ea36085c55bc16e2adb06a7bfe57b24d381e0194ae699f2 \ + --hash=sha256:63c0f4c088ddf0c736a24f7d7be1f6e2896a71822630c2805569b14de93eb393 \ + --hash=sha256:66ebae2c70305987341519ec1a720072a3cb3e4b1d52ac0e9e841f4d02658d3d \ + --hash=sha256:6a02d7dcc16126c8fae1c1c09b2072798a1dc482ab5f9c52b12c7114dac47325 \ + --hash=sha256:71d4cbe2b2a1335c76ed0acae2dc862163787d8b01a705e1949796907ed94ccd \ + --hash=sha256:71e51c046ccbeefb86524c6b1e17574f579c6ac4dc8ea4a09437d3e8f88342d3 \ + --hash=sha256:7326bc048073b04e4e7d59dd4a2d737c8e7cc270ef74ea1acb764382e995590c \ + --hash=sha256:785a09216ac31570fb301ddb9f61ee73d1f18f8b9561f712dce0b8ac8628bc88 \ + --hash=sha256:7ea5412ea229e571ac9738cbe14f845cc06c8e4e956afb5f42061ccd087ef31f \ + --hash=sha256:808cfafc047f0dec507a34c8fa8e4cda5722737fd33577df73452f52f7aca644 \ + --hash=sha256:8ac016ffaeac35bc010992b71bf8afdd39d458f201c8138d84cf78778a936e6c \ + --hash=sha256:8d3988970b4cf7d03bdd5b5169302ff84562dd2e1e0f84aeb34df3e5b5dc19bf \ + --hash=sha256:8e0542d85d6b55cf2934050d6ffcb1cd76c768dcf9572e7467002cf087bb366d \ + --hash=sha256:91688edb1f49110870d2c215db2cf445f1763c14173698ead0818908c51fb2a1 \ + --hash=sha256:916edb6dc1051732610e37863232e5a1d64bed7b130fed067f7dbc80d12d0065 \ + --hash=sha256:953d77ba0d8c96b29657622492c2c0bc7c161a304c069043a14c368aec20aeef \ + --hash=sha256:99026bcd9cbd3211cc36517400b04ca0fc5d3e412b14daf84ee6e65f67d9a2d8 \ + --hash=sha256:9ffc97e22730ea97b00f7c303ccc60b1305e786afadb2a4a46578dafa4d29da0 \ + --hash=sha256:a595958d0b1ff6d59c2570a3f0d1c8e36ab9f89d6e1b9c96fa7eb5e1a8698510 \ + --hash=sha256:b0938c2978c91ae1ef9c1f2ba35abb86330e198fb23469e356eba311e02233ee \ + --hash=sha256:b9df76d5a6bbf50967589bd42df3c522dd88babea2be745a507f56b41ab40626 \ + --hash=sha256:bc0ad5be2aeb9ff29c8512848d39d7c63fdd4bfbb5516bc523f5de5a77e55e6d \ + --hash=sha256:c378afcb7dd7c42f426a66112496c949fc39e5883de6817d86e60afa51720ccc \ + --hash=sha256:c812302a9acfe171e82f680b7ad642014cd017380b2c678441b3da4fb513c498 \ + --hash=sha256:d18933248a5bb0ad56a1bae6003a9a7f37daac2ecb0c5bcbfaaf081b317e1c84 \ + --hash=sha256:d2b8cfd8aee4d06ab335d359e4095d206102300a5e105a4b4bc69acca42427a6 \ + --hash=sha256:d822083fe26ec6728bd8c273ac121fc4ab3864a0fdf0cf0ff3efb188fcd209ed \ + --hash=sha256:e283fa2a8f7350fb9fb70ecdee28d59d39c92f4c7f1cc90a44d6b86db3b3a8b3 \ + --hash=sha256:e67b6bbacbfadea5e100266d2797f2d4cec9883ea4dc84a5537673850036a8d8 \ + --hash=sha256:f09b551f6c3e334652831ac68c770ee4284741ce0a3895bf1ccf2a1178d66cdd + # via + # confection + # spacy + # thinc + # weasel +thinc==8.2.5 \ + --hash=sha256:0933adbd3e65e30d3bef903e77a368bc8a41bed34b0d18df6d4fc0536908e21f \ + --hash=sha256:34db8a023b9f70645fdf06c510584ba6d8b97ec53c1e094f42d95652bf8c875f \ + --hash=sha256:399260197ef3f8d9600315fc5b5a1d5940400fceb0361de642e9fe3506d82385 \ + --hash=sha256:54bac2ba23b208fdaf267cd6113d26a5ecbb3b0e0c6015dff784ae6a9c5e78ca \ + --hash=sha256:5b884e56eaeb9e5c7bfeb1c8810a3cbad19a599b33b9f3152b90b67f468471ac \ + --hash=sha256:5c78fb218273894168d1ca2dd3a20f28dba5a7fa698c4f2a2fc425eda2086cfc \ + --hash=sha256:6b166d1a22003ee03bc236370fff2884744c1fb758a6209a2512d305773d07d7 \ + --hash=sha256:87e729f33c76ec6df9b375989743252ab880d79f3a2b4175169b21dece90f102 \ + --hash=sha256:8901b30db1071ea8d5e4437429c8632535bf5ed87938ce3bb5057bed9f15aed8 \ + --hash=sha256:8e299d4dc41107385d6d14d8604a060825798a031cabe2b894b22f9d75d9eaad \ + --hash=sha256:8ef5d46d62e31f2450224ab22391a606cf427b13e20cfc570f70422e2f333872 \ + --hash=sha256:9fc26697e2358c71a5fe243d52e98ae67ee1a3b314eead5031845b6d1c0d121c \ + --hash=sha256:a75c0de3340afed594beda293661de145f3842873df56d9989bc338148f13fab \ + --hash=sha256:bb97e2f699a3df16112ef5460cbfb0c9189a5fbc0e76bcf170ed7d995bdce367 \ + --hash=sha256:c2963791c934cc7fbd8f9b942d571cac79892ad11630bfca690a868c32752b75 \ + --hash=sha256:c5f750ea2dd32ca6d46947025dacfc0f6037340c4e5f7adb9af84c75f65aa7d8 \ + --hash=sha256:cdc27da534807a2addd1c3d2a3d19f99e3eb67fdbce81c21f4e4c8bfa94ac15b \ + --hash=sha256:d80f1e497971c9fa0938f5cc8fe607bbe87356b405fb7bbc3ff9f32fb4eed3bb \ + --hash=sha256:dc267f6aad80a681a85f50383afe91da9e2bec56fefdda86bfa2e4f529bef191 \ + --hash=sha256:df2138cf379061017ecb8bf609a8857e7904709ef0a9a2252783c16f67a2b749 \ + --hash=sha256:e8a8f2f249f2be9a5ce2a81a6efe7503b68be7b57e47ad54ab28204e1f0c723b + # via spacy +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via + # nltk + # spacy +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 + # via + # spacy + # typer-slim +typer-slim==0.24.0 \ + --hash=sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e \ + --hash=sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34 + # via weasel +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # pydantic + # pydantic-core + # referencing + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via pydantic +unidecode==1.4.0 \ + --hash=sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021 \ + --hash=sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23 + # via medspacy-quickumls +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests +wasabi==1.1.3 \ + --hash=sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878 \ + --hash=sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c + # via + # spacy + # thinc + # weasel +weasel==0.4.3 \ + --hash=sha256:08f65b5d0dbded4879e08a64882de9b9514753d9eaa4c4e2a576e33666ac12cf \ + --hash=sha256:f293d6174398e8f478c78481e00c503ee4b82ea7a3e6d0d6a01e46a6b1396845 + # via spacy +win32-setctime==1.2.0 \ + --hash=sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390 \ + --hash=sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0 + # via loguru +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 + # via smart-open + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes and the requirement is not +# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# setuptools diff --git a/worker/run-loop.ts b/worker/run-loop.ts new file mode 100644 index 0000000000..71bc407d37 --- /dev/null +++ b/worker/run-loop.ts @@ -0,0 +1,117 @@ +import { safeErrorLogDetails, safeIngestionJobLog } from "../src/lib/privacy"; +import type { JobRow } from "./types"; +import type { WorkerErrorStage } from "./observability"; +import { WorkerRuntimeControl, WorkerAbortError } from "./runtime-control"; + +export type WorkerRunLoopDeps = { + once: boolean; + pollMs: number; + healthBackoffMs: number; + maxClaimFailures: number; + claim: () => Promise; + process: (job: JobRow) => Promise; + probe: () => Promise<{ ok: boolean; message?: string }>; + backoff: (consecutiveFailures: number) => number; + controller: WorkerRuntimeControl; + log: (message: string, level: "info" | "warn" | "error", extra?: Record) => void; + /** + * Optional Sentry/privacy-safe capture. Restores the pre-extraction + * threshold-transition claim report and per-job process report. + */ + captureException?: (error: unknown, stage: WorkerErrorStage) => void; +}; + +export async function runWorkerLoop(deps: WorkerRunLoopDeps): Promise { + const { + once, + pollMs, + healthBackoffMs, + maxClaimFailures, + claim, + process, + probe, + backoff, + controller, + log, + captureException, + } = deps; + let consecutiveClaimFailures = 0; + let active = 0; + + while (!controller.isStopped || active > 0) { + // After stop with no in-flight work, exit without probing or claiming. + if (controller.isStopped && active === 0) break; + + let jobs: JobRow[] = []; + try { + const health = await probe(); + if (!health.ok) { + log("Supabase health check failed; worker is backing off", "warn", { message: health.message }); + if (once) { + throw new WorkerAbortError("Supabase health check failed in --once mode", 1, { + cause: health.message ? new Error(health.message) : undefined, + }); + } + if (controller.isStopped) break; + await controller.sleep(healthBackoffMs); + continue; + } + // Do not lease new work after SIGTERM/SIGINT — drain only. + if (controller.isStopped) break; + jobs = await claim(); + consecutiveClaimFailures = 0; + } catch (error) { + if (error instanceof WorkerAbortError) throw error; + + consecutiveClaimFailures += 1; + log("Ingestion job claim failed", "warn", safeErrorLogDetails(error)); + // Report only on the threshold transition: a single transient claim + // failure is normal, and a sustained outage must not spam Sentry on + // every subsequent poll after the retry budget is spent. + if (consecutiveClaimFailures === maxClaimFailures) { + captureException?.(error, "claim"); + } + if (once) { + throw new WorkerAbortError("Ingestion job claim failed in --once mode", 1, { cause: error }); + } + if (consecutiveClaimFailures >= maxClaimFailures) { + if (controller.isStopped) break; + await controller.sleep(healthBackoffMs); + continue; + } + if (controller.isStopped) break; + await controller.sleep(backoff(consecutiveClaimFailures)); + continue; + } + + if (jobs.length > 0) { + active += jobs.length; + try { + await Promise.all( + jobs.map(async (job) => { + log(safeIngestionJobLog(job.id), "info"); + try { + await process(job); + } catch (error) { + log("Ingestion job processing failed", "error", safeErrorLogDetails(error)); + captureException?.(error, "process"); + // Do not rethrow: processJob already applies the configured + // retry/fail decision; rethrowing would short-circuit the batch + // and does not honour the per-job semantics. + } + }), + ); + } finally { + active -= jobs.length; + } + + if (once) break; + // If stop was requested while the batch ran, we drain first, then exit. + continue; + } + + if (once) break; + if (controller.isStopped) break; + await controller.sleep(pollMs); + } +} diff --git a/worker/runtime-control.ts b/worker/runtime-control.ts new file mode 100644 index 0000000000..740aba7c2a --- /dev/null +++ b/worker/runtime-control.ts @@ -0,0 +1,96 @@ +import { EventEmitter } from "node:events"; + +type SignalName = "SIGTERM" | "SIGINT"; + +export type WorkerRuntimeControlOptions = { + /** Optional signal source for testing. Defaults to `process`. */ + signalSource?: Pick; +}; + +export class WorkerAbortError extends Error { + readonly exitCode: number; + + constructor(message: string, exitCode: number = 1, options?: ErrorOptions) { + super(message, options); + this.name = "WorkerAbortError"; + this.exitCode = exitCode; + } +} + +export class WorkerRuntimeControl { + #stopped = false; + #cleanup: (() => void)[] = []; + #signalSource?: Pick; + #attached = false; + + constructor(options: WorkerRuntimeControlOptions = {}) { + this.#signalSource = options.signalSource; + } + + get isStopped(): boolean { + return this.#stopped; + } + + stop(): void { + if (this.#stopped) return; + this.#stopped = true; + for (const cb of this.#cleanup) { + try { + cb(); + } catch { + /* non-fatal */ + } + } + this.#cleanup.length = 0; + } + + onStop(cb: () => void): () => void { + this.#cleanup.push(cb); + return () => { + const index = this.#cleanup.indexOf(cb); + if (index >= 0) this.#cleanup.splice(index, 1); + }; + } + + sleep(ms: number): Promise { + if (this.#stopped) return Promise.resolve(); + return new Promise((resolve) => { + let settled = false; + let stopUnsub = () => {}; + const settle = () => { + if (settled) return; + settled = true; + clearTimeout(timer); + stopUnsub(); + resolve(); + }; + + const timer = setTimeout(settle, ms); + stopUnsub = this.onStop(settle); + }); + } + + attachSignals(signals: SignalName[] = ["SIGTERM", "SIGINT"]): void { + if (this.#attached) return; + this.#attached = true; + const source = this.#signalSource ?? process; + + const handler = () => this.stop(); + for (const signal of signals) { + source.on(signal, handler); + } + + // Store the detach logic so tests can explicitly call detachSignals. + this.#cleanup.push(() => { + for (const signal of signals) { + source.off(signal, handler); + } + }); + } + + detachSignals(): void { + if (!this.#attached) return; + this.#attached = false; + this.stop(); + } +} diff --git a/worker/types.ts b/worker/types.ts new file mode 100644 index 0000000000..3d4450345c --- /dev/null +++ b/worker/types.ts @@ -0,0 +1,22 @@ +export type JobDocument = { + id: string; + owner_id: string | null; + title: string; + file_name: string; + file_type: string; + storage_path: string; + content_hash?: string | null; + source_path?: string | null; + import_batch_id?: string | null; + status?: string | null; + metadata: Record | null; +}; + +export type JobRow = { + id: string; + document_id: string; + batch_id: string | null; + attempt_count: number; + max_attempts: number; + documents: JobDocument; +}; diff --git a/worker/validate-runtime.ts b/worker/validate-runtime.ts new file mode 100644 index 0000000000..928680d9d1 --- /dev/null +++ b/worker/validate-runtime.ts @@ -0,0 +1,156 @@ +import { existsSync, readFileSync } from "node:fs"; +import { builtinModules } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { env } from "../src/lib/env"; +import { checkPythonPdfPrerequisites, checkMedspacyPrerequisites } from "./prerequisites"; + +const NODE_BUILTINS = new Set(builtinModules); + +type ValidationResult = { + ok: boolean; + nodeVersion: string; + execPath: string; + externals: { spec: string; resolved: string; ok: boolean }[]; + python: Awaited>; + medspacy?: Awaited>; + pipCheck: { ok: boolean; detail: string }; + errors: string[]; +}; + +function resolveRelativeToBundle(relPath: string): URL { + return new URL(relPath, import.meta.url); +} + +function nodeMajor(): number { + return Number(process.versions.node.split(".")[0]); +} + +function npmMajor(): number | null { + const userAgent = process.env.npm_config_user_agent ?? ""; + const match = userAgent.match(/\bnpm\/(\d+\.\d+\.\d+)/); + return match ? Number(match[1].split(".")[0]) : null; +} + +async function runPipCheck(): Promise<{ ok: boolean; detail: string }> { + const { execFile } = await import("node:child_process"); + const pythonBin = env.PYTHON_BIN; + return new Promise((resolve) => { + execFile(pythonBin, ["-m", "pip", "check"], { timeout: 60_000 }, (error, stdout, stderr) => { + if (error) { + resolve({ ok: false, detail: (stdout + stderr).trim() || String(error) }); + } else { + resolve({ ok: true, detail: (stdout + stderr).trim() || "No broken requirements found." }); + } + }); + }); +} + +export type ValidateRuntimeOptions = { + /** Override the externals list for testing. */ + externals?: string[]; + /** Override the externals.json path for testing. */ + externalsPath?: URL | string; + /** Skip Python/Tesseract/pip checks (for unit tests). */ + skipPython?: boolean; +}; + +export async function validateRuntime(options: ValidateRuntimeOptions = {}): Promise { + const errors: string[] = []; + const result: ValidationResult = { + ok: true, + nodeVersion: process.versions.node, + execPath: process.execPath, + externals: [], + python: { ok: false, detail: "not checked" }, + pipCheck: { ok: false, detail: "not checked" }, + errors, + }; + + if (nodeMajor() !== 24) { + errors.push(`Expected Node 24.x, got ${process.versions.node}`); + } + + const npmMaj = npmMajor(); + if (npmMaj !== null && npmMaj !== 11) { + errors.push(`Expected npm 11.x, got npm ${npmMaj}`); + } + + let externals: string[] = options.externals ?? []; + if (externals.length === 0) { + const externalsPath = options.externalsPath + ? typeof options.externalsPath === "string" + ? pathToFileURL(options.externalsPath) + : options.externalsPath + : resolveRelativeToBundle("./externals.json"); + if (existsSync(externalsPath)) { + try { + externals = JSON.parse(readFileSync(externalsPath, "utf8")) as string[]; + } catch (error) { + errors.push(`Failed to parse externals.json: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + errors.push(`externals.json not found at ${externalsPath}`); + } + } + + for (const spec of externals) { + // esbuild may emit bare builtin names (`fs`) as well as `node:fs`. + if (spec.startsWith("node:") || NODE_BUILTINS.has(spec)) { + result.externals.push({ spec, resolved: spec.startsWith("node:") ? spec : `node:${spec}`, ok: true }); + continue; + } + try { + const resolved = import.meta.resolve(spec, import.meta.url); + const resolvedFile = fileURLToPath(resolved); + const ok = existsSync(resolvedFile); + result.externals.push({ spec, resolved: resolvedFile, ok }); + if (!ok) { + errors.push(`Resolved ${spec} to ${resolvedFile} but the file does not exist`); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + result.externals.push({ spec, resolved: "", ok: false }); + errors.push(`Failed to resolve ${spec}: ${message}`); + } + } + + if (!options.skipPython) { + result.python = await checkPythonPdfPrerequisites(); + if (!result.python.ok) { + errors.push(`Python PDF/OCR check failed: ${result.python.detail}`); + } + + if (env.WORKER_MEDSPACY_ASSERTION) { + result.medspacy = await checkMedspacyPrerequisites(); + if (!result.medspacy.ok) { + errors.push(`medSpaCy check failed: ${result.medspacy.detail}`); + } + } + + result.pipCheck = await runPipCheck(); + if (!result.pipCheck.ok) { + errors.push(`pip check failed: ${result.pipCheck.detail}`); + } + } + + result.ok = errors.length === 0; + return result; +} + +async function main() { + const result = await validateRuntime(); + console.log(JSON.stringify(result, null, 2)); + process.exit(result.ok ? 0 : 1); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error( + JSON.stringify({ + ok: false, + error: error instanceof Error ? error.message : String(error), + }), + ); + process.exit(1); + }); +}