Ship CBP enrichment as grammar in the standard enrichment engine#2154
Conversation
…oms corpora; fix bulk-ingest ext gate and relationship-embedding bottleneck - opencontractserver/enrichment/services/customs_ruling_citation_service.py: detects HTS tariff codes (plain annotations) and CBP ruling-number citations (CorpusReference rows resolved against sibling document titles) from each document's own post-parse text, reusing EnrichmentWriter for persistence. Runnable via manage.py enrich_customs_rulings. Validated at 100% precision/recall against a 96-doc pilot vs. the source dataset's own golden-tested extraction. - ingest_corpus management command: ext_ok now unions get_convertible_extensions() so files eligible for the configured file converter (e.g. Gotenberg -> .doc) are actually accepted, matching the command's own docstring. - embeddings_task.py: calculate_embeddings_for_relationship_batch's explicit-embedder path now batches through embed_texts_batch() (shared _batch_embed_items helper, also used by the annotation path) instead of one HTTP call per relationship -- a real bottleneck for parsers that emit many relationships per document (e.g. Warp-Ingest's heading-hierarchy OC_SUBTREE_GROUP rows). - celery worker start script: ignore .pilot_data so watchfiles doesn't choke on large locally-materialized ingest batches.
A sustained bulk-ingest run drove warp-ingest to ~5.7GB with no per-container memory limit anywhere in local.yml, exhausting host RAM/swap and causing the kernel OOM-killer to take down unrelated desktop processes. Caps + restart policy contain future runaway growth to a single container instead of the whole host.
convert_document_to_pdf, extract_thumbnail, ingest_doc, and set_doc_lock_state previously shared one queue. On a .doc-heavy bulk corpus this let the flood of cheap conversion tasks (enqueued for every document up front) statistically starve the actual parsing/unlock work, observed at a ~190:1 completion ratio even after doubling worker concurrency. extract_thumbnail/ingest_doc/ set_doc_lock_state now route to their own doc_parse queue with dedicated consumer capacity; standard worker startup scripts updated to subscribe to it so normal (non-bulk) uploads keep working.
Capping only the four heaviest services (docling-parser, warp-ingest, vector-embedder, celeryworker) left postgres, redis, django, gotenberg, multimodal-embedder, and docxodus-parser unbounded. Under concurrent load from a parallelized enrichment run plus a large Celery backlog drain, their aggregate uncapped memory usage exhausted host RAM/swap and forced a hard reboot, even though each individually-capped service stayed under its own ceiling. Every Docker-managed service now has a mem_limit.
load_document_text_and_layer's S3 fetch + PAWLS parse dominates per-document cost (~230ms measured) and touches no ORM state, so it's safe to prefetch across a thread pool while writes stay single- threaded on the caller. Also adds --limit for a quick, fully-complete pass over a deterministic subset of a corpus, useful for evaluating output quality/UX without enriching a corpus end-to-end in one sitting; citation resolution still considers every document's title in the corpus, not just the scanned subset.
…lution title_index was keyed on the raw document title, but ~98% of corpus 95's real titles retain a file extension (A83482.doc) that the ruling-citation regex never produces, so every citation into those documents resolved as unresolved regardless of how many candidates were found.
get_or_create's lookup key (source_annotation, reference_type, canonical_key) never included the resolution outcome, so a row written UNRESOLVED before its target existed (or before a resolution bug was fixed) was never upgraded by a later run that DID find the target — the run's in-memory resolved count silently diverged from what persisted. Shared EnrichmentWriter fix, so it also applies to the general authority/law-reference discovery path, not just customs rulings.
Replace the standalone CustomsRulingCitationService + enrich_customs_rulings management command with two Tier-2a grammar families that run automatically through every existing enrichment trigger (corpus_reference_enrichment analyzer, ADD_DOCUMENT corpus action, runCorpusEnrichment mutation, agent tools): - HTS tariff codes (grammars.py::_hts) -> REF_LAW citations keyed htsus:<code> (new prefix in authority_mappings.yaml), gated on a document-level HTSUS cue with anchored-vs-contextual per-mention confidence so ordinary corpora's dotted decimals are never mined. - Title-identifier document citations (CBP ruling numbers, grammars.py::_document_identifier_citations) -> REF_DOCUMENT citations resolved against sibling document titles by ReferenceResolver. Activates only on corpora whose titles are predominantly identifier-shaped (DOC_IDENTIFIER_TITLE_GATE_*); titles canonicalized via document_identifier_from_title (extension-stripped, so A83482.doc resolves); self-identifying header mentions dropped at resolution (ReferenceResolver.resolve may now return None); citations to absent siblings persist UNRESOLVED and heal to RESOLVED on the automatic re-apply when the sibling lands. Deleted: customs_ruling_citation_service.py, enrich_customs_rulings command, their test file, and the two changelog fragments describing removed features. New tests in test_customs_trade_grammars.py (36) cover the regex shapes, both gates, resolver semantics, and end-to-end apply() including the sibling-later heal; docs updated in docs/architecture/reference-web-enrichment.md.
ReviewWent through the diff in detail (grammars.py, resolver.py, writer.py, enrichment_service.py, ingest_corpus.py, config/settings/base.py, local.yml, and the new test file). Overall this is a clean, well-documented integration — the design (gate-then-grammar, forward-only heal, 1.
|
…ices
- test_enrichment_resolver.py: ReferenceResolver.resolve/resolve_document now
return Resolution | None (self-identifying header mentions are dropped), so
the pre-existing assertions need explicit 'assert r is not None' narrowing —
the same pattern the rest of the suite uses. No test logic changed.
- local.yml: mem_limit + restart for privacy_filter (2g), celerybeat (1g),
and the profile-gated frontend (2g) — review feedback: the remaining-services
changelog fragment claimed every Docker-managed service had a ceiling while
these three were still uncapped; privacy_filter (CPU model inference) was the
most plausible reproducer of the host-OOM scenario the caps exist to prevent.
- Reworded the remaining-services fragment: the earlier heavy-services cap is
a commit on this same branch, not a prior release ('a prior pass'), and the
covered-services list now matches reality.
Review SummaryThis is a clean, well-scoped PR that integrates the CBP CROSS enrichment work as two ordinary Tier-2a grammar families instead of bespoke standalone machinery. The architecture choice (grammar candidates that flow through the existing I traced the Code quality / correctness
Writer heal logic (
|
…ircuit Review follow-ups: - ReferenceResolver: the self-mention drop now checks the SOURCE document's own title identifier (via a new doc_id -> identifier reverse map) instead of 'resolved target == source'. With duplicate identifier titles (the same ruling ingested as .doc and .pdf), the copy that lost the first-writer-wins index slot previously had its own header resolved as a citation pointing at the other copy; now every duplicate's header mention drops. Regression test added (test_duplicate_title_self_mentions_both_dropped). - GenericCitationExtractor: comment that the gate's condition order is load-bearing (MIN_DOCS short-circuit guards the fraction's division).
Review: Ship CBP enrichment as grammar in the standard enrichment engineOverviewThis PR retires the standalone CBP CROSS customs enrichment approach in favor of expressing it as two ordinary Tier-2a grammar candidates ( Strengths
Issues / things worth double-checking
Test coverageVery thorough — unit tests for SecurityNo user-controlled input reaches SQL or shell; regexes are bounded (no ReDoS risk); no new permission-relevant code paths — resolution and writing continue to go through the existing |
…-mention drop Review follow-ups (comments only, no behavior change): - DOC_IDENTIFIER_RE: note the uppercase-only match is intentional on both the title and text-mining sides (lowercase tokens are prose/serial noise; CROSS text prints ruling numbers uppercase). - document_identifier_from_title: note the single-extension strip is an accepted tradeoff — a multi-suffix title degrades to 'not identifier-titled', never to a wrong link. - ReferenceResolver._resolve_document_identifier: note the self-mention drop is deliberately document-wide, not header-scoped (no positional signal exists, and a self-citation only yields a useless self-loop edge).
|
Re: the description-vs-diff mismatch (review point 1) — the diff showing zero deleted files is expected, not a sign of leftover code. Verified there's nothing orphaned:
Points 3–5 (single-extension strip, uppercase-only matching, document-wide self-mention drop) are all deliberate tradeoffs — now documented as such in code comments as of 3a05fbe. Point 2 (scope bundling): correct read — the queue routing, mem caps, and extension-gate fixes are prior work consolidated on this branch; each carries its own changelog fragment so they stay individually traceable. Generated by Claude Code |
Review: Ship CBP enrichment as grammar in the standard enrichment engineOverviewSolid integration work: the two customs/trade grammars (HTS tariff codes, CBP ruling-number document citations) are wired into the existing Tier-2a Bug:
|
…egression tests Review follow-ups: - docs/upload_methods/worker_celery_setup.md contradicted the new CELERY_TASK_ROUTES: it still said there are only two queues, listed extract_thumbnail under 'celery', and told self-hosted operators that '-Q celery,worker_uploads' was sufficient — which under the doc_parse routing would leave every document silently stuck mid-chain (converted but never thumbnailed/parsed/unlocked). Rewrote the queue table, TL;DR command, coverage warning, scaling notes, verification, and symptom->cause rows for three queues; also fixed the stale '-Q' lists in worker_uploads.md, remote_ingest_worker.md, and upload_methods/index.md, and noted the doc sweep in the doc-parse changelog fragment. - test_ingest_corpus_command.py: regression tests for the convertible extension gate — a .doc file is ingested exactly when the configured converter reports it convertible and dropped when no converter is configured (the silently-dropped-.doc production bug this branch fixes).
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Codecov flagged the one uncovered patch line: grammars.py's continue when the HTS text shape matches (its middle group allows 2-4 digits) but _normalize_hts rejects the digit count — e.g. a 9-digit '1234.56.789'. Added a test pinning that such tokens are skipped, not emitted with a malformed key.
Review: Ship CBP enrichment as grammar in the standard enrichment engineReviewed the diff (grammar/resolver/writer changes, ingest_corpus extension gate, Celery queue split, and Correctness -- looks solid
Minor nits (non-blocking)
Test coverageStrong -- 36 new tests cover the regex shapes, both gates (including exact-threshold and below-threshold cases), resolver self-mention/duplicate-title semantics, and full Infra changes (queue split,
|
The prior pytest run progressed to 99% with zero test failures, then '[gw3] node down: Not properly terminated' (worker OOM/hard-crash on the runner) hung the session until the workflow's 100-minute timeout. No test in the suite failed; main's backend workflow is consistently green. Empty commit to re-trigger the suite (the CI token available to this session cannot re-run failed jobs directly).
Review SummaryThis PR reworks CBP CROSS enrichment (HTS tariff codes + ruling-number document citations) into the standard Tier-2a grammar tier, plus a handful of unrelated-but-bundled ops fixes (Celery Correctness -- looks solid
Minor observations (non-blocking)
Test coverageCoverage is thorough: SecurityNo user-controlled input reaches raw SQL, template rendering, or shell execution in this diff. Document titles and text flow only into regex matching and ORM lookups ( Overall: nice, well-documented change with strong test coverage. Nothing here blocks merge. |
Review follow-up: titles are user-editable, and Path(...).stem treats '/' as a path separator — a title like 'Reports/N301234' would have its leading segments silently discarded and come out identifier-shaped. A title containing '/' is not a bare materialized filename, so it is now returned whole; the identifier fullmatch then rejects it and it stays out of the gate and title index. Regression test added.
|
Reviewed the diff (grammar/resolver/writer core changes, Strengths
Minor observations / nits (none blocking)
Security/permissions: No Tier-0 permission-layer concerns — the resolver/extractor only consume an already permission-scoped Test coverage: Strong — 40 new tests plus updates to 3 existing suites, covering unit-level grammar/regex correctness, resolver semantics, and full No blocking issues found. |
Ports every fix from feature/customs-ruling-corpus-enrichment (PR 2153) that the grammar rework forked away from (fork point d8b34be), so this branch handles the real official CROSS export end to end. On that 10K benchmark the pre-port state resolved zero citations: the corpus's titles are human-readable SUBJECTS (so the title-only gate never opened and the title index never matched), and the legacy slice cites by series token + bare number, which the prefixed grammar cannot see. Architecture-dependent (reimplemented for grammars/resolver/service): - Durable document identity: per-document canonical identifiers derive from DocumentPath.external_id (cross: namespace, case-insensitive) > active corpus-path basename stem > title stem (resolver.document_identity_candidates), computed once in EnrichmentService._build_detection and shared by the resolver index, the self-mention drop (now a per-document SET of identities), and the grammar's corpus-shape gate. Ambiguous identities are reported and left unresolved instead of first-writer-wins; the IdentifierResolverTests duplicate-title expectation is updated accordingly (intentional behavior change, documented in the test). - Series-token legacy citation grammar (constants.LEGACY_DOC_IDENTIFIER_CITE_RE): "HRL 087392"-style cites with a six-digit + series-token guard (5 digits after "NY" is a ZIP code in 148/149 sampled instances). Identities and citations meet on one canonical key (constants.canonical_document_identifier: prefixed verbatim, bare digits zero-stripped). Recovers 9,377 candidates / 5,007 resolved edges on the 10K benchmark. Architecture-independent (taken from the 2153 branch verbatim; none of these files changed on this branch since the fork): - Writer span fallback: canonical {start, end, text} + page=0 sentinel (span_projection.span_annotation_payload, constants SPAN_NO_PAGE), raw-text-first slice sourcing (no per-mention storage refetch), and in-place healing of pre-fix span mentions on re-apply. - meta.csv external_id column -> DocumentPath.external_id stamping (metadata_file_parser, import_zip_with_folder_structure, external_ids_applied counter, 512-char guard) + docs. - Versioning: the upversion branch now inherits ingestion_source / external_id / ingestion_metadata like move/delete/restore already do. - relationships.csv import idempotency (get_or_create on edge identity; duplicates count as relationships_skipped). - Lint/typing debt fixes (signals/doc_tasks partial-based on_commit dispatch, settings CELERY override removal in local.py, mypy fixes) and the coverage test suites for doc_tasks / bulk upload status / metadata parser / frontend upload+bulk-import modals. New tests: test_import_identity_contract.py (external_id lifecycle, relationship-import dedupe, label coercion, and the official-export ZIP -> import -> EnrichmentService.apply contract test) plus legacy grammar + path/external_id identity coverage in test_customs_trade_grammars.py. 536 tests green across the affected suites; manage.py check and pre-commit pass.
|
Ported the PR 2153 infra fixes into this branch (commit b8d43af) and re-ran the 10K official-export smoke test against the same benchmark corpus that PR 2153's runs enriched. Output equivalence, measured against 2153's persisted results:
The port: durable path/external_id-derived document identity shared by the resolver index, self-mention drop, and grammar gate (official-export titles are subjects, so the previous title-only gate never opened on the real export); series-token legacy citation grammar ("HRL 087392" — 9,377 of the 9,677 candidates on this corpus); meta.csv external_id contract + upversion lineage inheritance; relationships.csv import idempotency; writer span-fallback page=0/text canonical shape with in-place healing. One pre-existing expectation updated: duplicate document identities now resolve to UNRESOLVED + a warning instead of first-writer-wins (documented in IdentifierResolverTests). 536 tests green across the affected suites; manage.py check and pre-commit pass. Wall time for the full-corpus run: 96 minutes (sequential engine). |
…ed tests b8d43af ported PR 2153's frontend test files (UploadProgress.test.tsx, upload-modal.ct.tsx, bulk-import-modal.ct.tsx) but not the 9 frontend/src files they exercise, so 'yarn build' failed TypeScript compilation (UploadProgressProps has no progressPercent) and took the Build / Build Docker Image / Unit Tests / production-stack jobs down with it. Checked out the missing files verbatim from feature/customs-ruling-corpus-enrichment (no local drift since the fork point, so this is a pure port): the streamed-archive UploadProgress / UploadModal / useUploadMutations changes, BulkImportModal, importHttp, IntelligencePanel + its ct test and wrapper, and the queries.ts additions. Verified: tsc clean, full vitest suite 2531 tests green.
Review: Ship CBP enrichment as grammar in the standard enrichment engineReviewed the full diff (62 files, +4083/-256) across four areas in parallel: the new grammar/enrichment core, ingestion/infra carry-overs, frontend upload/intelligence UI, and docs/changelog. Verified the notable findings directly against the checked-out merge commit rather than relying solely on the diff. Overall this is a well-structured integration, with solid targeted test coverage on the core grammar logic, resolver, and writer changes, and the celery queue/compose/idempotency carry-overs check out. Two real (verified) bugs and a few doc/test-gap issues are noted below. Bugs (verified against source)
Docs
Test coverage gaps
What checked out well
Scope note The |
…ranch - docs/upload_methods/bulk_zip_import.md: the external_id consumer pointer referenced the deleted customs_ruling_citation_service module; now points at resolver.py::document_identity_candidates (the live implementation). - docs/architecture/reference-web-enrichment.md: the customs family section described resolution as title-only; it now documents the layered identity derivation (cross: external_id > corpus-path basename stem > title stem), the legacy series-token citation grammar, and the duplicate-identity UNRESOLVED semantics. - doc-parse-queue-starvation fragment + worker_celery_setup.md: the routed set includes remap_pending_annotations (four tasks, not three). - test_customs_trade_grammars.py: gate boundary tests now also exercise the PRODUCTION branch (identity_candidates passed, considered=len(documents), including identity-less/untitled docs) plus exact-50% fraction boundaries away from the min-docs edge on both branches. - test_doc_tasks.py: the routing test now pins all four doc_parse routes (extract_thumbnail/ingest_doc/remap_pending_annotations/set_doc_lock_state) and that convert_document_to_pdf deliberately stays on the default queue. The review's two UploadModal/BulkImportModal close-mid-job findings and the scope-split suggestion are left for the author of that ported code — both require UX/architecture decisions (lifting job polling out of the modals).
|
Disposition of the latest review, as of 6343fc8: Fixed (items 3–8): the dead Left for @JSv4 (items 1–2 + scope note): the Generated by Claude Code |
What
Reworks the CBP CROSS enrichment features from this branch so they 1) run automatically inside the existing enrichment workflows and 2) ship as additional grammar for the existing enrichment system. The standalone
CustomsRulingCitationService, theenrich_customs_rulingsmanagement command, and their bespoke analyzer/label are deleted — no custom commands or parallel machinery remain. (Note for reviewers: those files were added and removed on this same branch, so the net diff vsmainnever contains them — you won't see file deletions in the diff, and a repo-wide grep confirms zero remaining references.)How it integrates
Both features are now ordinary Tier-2a grammar candidates inside
GenericCitationExtractor(opencontractserver/enrichment/grammars.py), so they execute whereverEnrichmentService.applyalready executes — thecorpus_reference_enrichmentanalyzer task, the ADD_DOCUMENT corpus action installed bysetupCorpusIntelligence/ingest_corpus --enrich(i.e. every new document automatically re-enriches the corpus), therunCorpusEnrichmentmutation, and the agent tools. Detection tier, confidence, dedup, PDF token projection,CorpusReferencepersistence,DocumentRelationshiprollup, and the provisional/finalize lifecycle are all inherited from the existing engine.grammars.py::_hts) →REF_LAWcitations keyedhtsus:<code>, with thehtsusprefix declared inauthority_mappings.yaml(classification +AuthorityNamespaceseeding,discover()inventory, governance-graph ghost nodes, and future cross-corpus linking for free). Gated on a document-level HTSUS cue — the bare acronym "HTS" deliberately does not open the gate — with anchored (0.9) vs contextual (0.7) per-mention confidence so ordinary corpora's dotted decimals are never mined.grammars.py::_document_identifier_citations) →REF_DOCUMENTcitations resolved against sibling document titles byReferenceResolver. The grammar activates only on corpora whose titles are predominantly identifier-shaped (constants.DOC_IDENTIFIER_TITLE_GATE_*— ≥2 identifier titles and ≥50% of titles), so serial/order/patent numbers in unrelated corpora are never mined. Titles are canonicalized viaconstants.document_identifier_from_title(extension-stripped, path-separator-guarded, preserving the earlierA83482.docresolution fix); a ruling's self-identifying header mention is dropped at resolution (ReferenceResolver.resolvemay now returnNone), keyed on the source document's own title identifier so duplicate-titled siblings are handled; citations to rulings not yet ingested persistUNRESOLVEDand are healed toRESOLVEDby the writer's forward-only heal on the automatic re-apply when the sibling lands.Kept from the branch as-is: the
EnrichmentWriterUNRESOLVED→RESOLVED heal (+references_resolvedcounter), thedoc_parseCelery queue routing (ops docs updated to match), compose memory caps (now covering everylocal.ymlservice), and theingest_corpusconvertible-extension gate (now with regression tests) — these integrate with the existing ingestion system rather than bypassing it.Testing
opencontractserver/tests/test_customs_trade_grammars.py(40 tests): HTS normalization/gates/confidence tiers (incl. the odd-digit-grouping rejection branch), ruling-citation shapes + crossfeed's documented false-positive guards (bare 6-digit numbers,NY 10022), corpus-shape gate open/closed at thresholds, resolver semantics (extension-carrying titles, path-like titles, self-mention drop incl. duplicate-titled siblings, unknown → UNRESOLVED), and end-to-endapply()including sibling-ingested-later healing,DocumentRelationshiprollup, idempotency, and the non-rulings-corpus negative case. Plusingest_corpusextension-gate regression tests.test_generic_grammars,test_enrichment_writer,test_enrichment_resolver,test_reconcile,test_enrichment_extractor,test_enrichment_discovery,test_authority_mappings_file,test_enrichment_classification_constants,test_authority_pack_taxonomy,test_corpus_reference_model,test_enrichment_linking,test_enrichment_backfill,test_enrichment_in_flight,test_governance_graph,test_enrichment_analyzer_integration,test_enrichment_tools,test_graph_navigation_tools,test_intelligence_setup,test_enrichment_llm_integration,test_enrichment_run_mutation,test_authority_frontier,test_authority_pack,test_authority_pack_config,test_ingest_corpus_command.manage.py check(incl.opencontracts.E001) passes; changelog fragments validate.Docs / changelog
docs/architecture/reference-web-enrichment.md: new "Customs / trade grammar family" subsection under the detection tiers.docs/upload_methods/worker_celery_setup.md(+worker_uploads.md,remote_ingest_worker.md,upload_methods/index.md): updated for thedoc_parsequeue — the previous text told self-hosted operators-Q celery,worker_uploadswas sufficient, which under the new routing would leave documents silently stuck mid-chain.changelog.d/customs-ruling-citation-enrichment.added.mdrewritten for the grammar integration; the two fragments describing the removed command features deleted; the writer-heal fragment trimmed of references to the deleted service.Generated by Claude Code